"""FastAPI dependencies: DB session, current user (JWT via cookie or bearer), CSRF, rate limiting.""" from typing import Optional from fastapi import Depends, Header, Request, Response, Cookie, HTTPException, status from fastapi.security.utils import get_authorization_scheme_param from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.security import decode_token, rate_limit from app.db.session import AsyncSessionLocal async def get_db() -> AsyncSession: async with AsyncSessionLocal() as session: yield session async def get_current_user(request: Request, response: Response, authorization: Optional[str] = Header(None), mailer_session: Optional[str] = Cookie(None), db: AsyncSession = Depends(get_db)) -> "User": # noqa: F821 """Authenticate via JWT in HttpOnly cookie (web UI) or Authorization header (API/tests).""" from app.models import User token = None if authorization: scheme, param = get_authorization_scheme_param(authorization) if scheme.lower() == "bearer": token = param if not token and mailer_session: token = mailer_session if not token: raise HTTPException(status_code=401, detail="Not authenticated") payload = decode_token(token) if not payload: raise HTTPException(status_code=401, detail="Invalid or expired session") user = await db.get(User, int(payload["sub"])) if not user or not user.is_active: raise HTTPException(status_code=401, detail="User not found or inactive") return user async def get_optional_user(request: Request, db: AsyncSession = Depends(get_db)): from app.models import User token = request.cookies.get(settings.SESSION_COOKIE_NAME) authz = request.headers.get("authorization", "") if authz.lower().startswith("bearer "): token = authz[7:] if not token: return None payload = decode_token(token) if not payload: return None return await db.get(User, int(payload["sub"])) def require_csrf(request: Request, x_csrf_token: Optional[str] = Header(None)) -> None: ===MSG BUILDER=== 26:def build_mime_message( 27- from_email: str, 28- to_email: str, 29- subject: str, 30- html_body: str = "", 31- text_body: str = "", 32- attachments: Optional[list] = None, # list of {filename, content(bytes), mime_type} 33- unsubscribe_url: str = "", 34- unsubscribe_mailto: str = "", 35-) -> MIMEMultipart: 36- """Build a real MIME multipart message. From header always equals the authenticated account.""" 37- attachments = attachments or [] 38- if html_body and text_body: 39- msg = MIMEMultipart("alternative") 40- else: 41- msg = MIMEMultipart("mixed") 42- msg["From"] = from_email # MUST be the authenticated Gmail account (never spoofed) 43- msg["To"] = to_email 44- msg["Subject"] = subject 45- msg["Date"] = formatdate(localtime=True) 46- msg["Message-ID"] = make_msgid() ===CAMPAIGN MODEL=== class Campaign(Base, TimestampMixin): __tablename__ = "campaigns" id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) name = Column(String(250), nullable=False) status = Column(String(30), default="draft") # draft|queued|running|paused|completed|cancelled|failed # selections recipient_list_id = Column(Integer, ForeignKey("recipient_lists.id", ondelete="SET NULL")) subject_set_ids = Column(JSON, default=list) # list of subject line ids template_ids = Column(JSON, default=list) # ordered template ids for sequencing message_type = Column(String(40), default="html") # attachment behaviour attach_pdf = Column(Boolean, default=False) attach_pdf_template_id = Column(Integer, ForeignKey("html_templates.id", ondelete="SET NULL")) attachment_id = Column(Integer, ForeignKey("attachments.id", ondelete="SET NULL")) attach_mode = Column(String(40), default="none") # none|pdf|file|image # dynamic variables config variable_config = Column(JSON, default=dict) # invoice date, due date, ranges etc. # rotation / limits account_ids = Column(JSON, default=list) rotation_strategy = Column(String(30), default="sequential") # sequential|weighted rotation_weights = Column(JSON, default=dict) # account_id -> weight subject_rotation = Column(String(30), default="round_robin") # round_robin|random|fixed_first template_sequence_mode = Column(String(30), default="repeat") # repeat|stop|random phone_selection = Column(String(30), default="round_robin") # round_robin|random|fixed max_per_account = Column(Integer, default=200) send_interval_seconds = Column(Float, default=1.0) rate_per_minute = Column(Integer, default=60) max_retries = Column(Integer, default=3) retry_delay_seconds = Column(Integer, default=60) # counters total_recipients = Column(Integer, default=0) sent_count = Column(Integer, default=0) failed_count = Column(Integer, default=0) skipped_count = Column(Integer, default=0) suppressed_count = Column(Integer, default=0) bounced_count = Column(Integer, default=0) queued_count = Column(Integer, default=0) started_at = Column(DateTime(timezone=True)) finished_at = Column(DateTime(timezone=True)) preview_sample_size = Column(Integer, default=5) # compliance unsubscribe_enabled = Column(Boolean, default=True, nullable=False) recipient_list = relationship("RecipientList") send_jobs = relationship("SendJob", back_populates="campaign", cascade="all, delete-orphan") class CampaignRecipient(Base, TimestampMixin): """Materialized campaign×recipient rows with generated values & audit info.""" __tablename__ = "campaign_recipients" id = Column(Integer, primary_key=True) campaign_id = Column(Integer, ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True)