@property def client_secret(self) -> str: return encryption.decrypt(self.client_secret_enc) @client_secret.setter def client_secret(self, value: str) -> None: self.client_secret_enc = encryption.encrypt(value) class GmailAccount(Base, TimestampMixin): __tablename__ = "gmail_accounts" id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) email = Column(String(320), nullable=False) display_name = Column(String(200), default="") status = Column(String(30), default="pending") # pending|authorized|expired|revoked|error sending_method = Column(String(20), default="api") # api|smtp auth_provider = Column(String(60), default="google") google_user_id = Column(String(120), default="") # Per-account config (administrator-configurable) daily_limit = Column(Integer, default=200) # provider/configured daily cap campaign_limit = Column(Integer, default=200) # per-account per-campaign cap sends_today = Column(Integer, default=0) last_send_date = Column(DateTime(timezone=True)) cooldown_until = Column(DateTime(timezone=True)) last_success_at = Column(DateTime(timezone=True)) last_error = Column(Text, default="") error_count = Column(Integer, default=0) token_expiry = Column(DateTime(timezone=True)) # Imported client config reference (optional; may use server-level config) oauth_config_id = Column(Integer, ForeignKey("oauth_client_configs.id", ondelete="SET NULL")) # Encrypted SMTP OAuth2 credentials mirror (kept in oauth_tokens; this is metadata only) is_active = Column(Boolean, default=True, nullable=False) revoked_by_user = Column(Boolean, default=False) revoked_at = Column(DateTime(timezone=True)) user = relationship("User", back_populates="gmail_accounts") # Eager-loaded (joined) so async endpoints/workers can read account.oauth_token # without triggering a lazy-load IO (which raises MissingGreenlet in async sessions). oauth_token = relationship("OAuthToken", back_populates="account", uselist=False, cascade="all, delete-orphan", lazy="joined") ====test send task fake==== def _make_template(db, user_id, body_html=None): from app.models import EmailTemplate t = EmailTemplate(user_id=user_id, name=f"tpl-{uuid.uuid4().hex[:6]}", subject_hint="hint", body_html=body_html or "
Hi %FirstName%, total %RandomTotal%. " "Invoice %RandomInvoiceNumber%.
") db.add(t) db.commit() return t def _make_account(db, user_id, status="authorized", **over): from app.models import GmailAccount, OAuthToken a = GmailAccount(user_id=user_id, email=f"send-acc-{uuid.uuid4().hex[:8]}@gmail.com", status=status, sending_method="api", is_active=(status == "authorized")) for k, v in over.items(): setattr(a, k, v) db.add(a) db.commit() if status == "authorized": db.add(OAuthToken(account_id=a.id, access_token=f"tok-{uuid.uuid4().hex[:6]}", refresh_token=f"ref-{uuid.uuid4().hex[:6]}", expires_at=datetime.now(timezone.utc) + timedelta(hours=1))) db.commit() return a def _seed_sendable(db, **camp_over): """User + list(1) + subject + template + authorized account + campaign + materialized CampaignRecipient (pending) + SendJob. Returns ids dict.""" from app.models import Campaign, CampaignRecipient, SendJob u = _make_user(db) rl, recs = _make_list(db, u.id, n=1) subj = _make_subject(db, u.id) tpl = _make_template(db, u.id) acc = _make_account(db, u.id) kw = dict( user_id=u.id, name=f"send-{uuid.uuid4().hex[:6]}", status="running",