7:from datetime import datetime, timezone
8:from sqlalchemy import (JSON, Boolean, Column, DateTime, Enum, Float, ForeignKey,
10:from sqlalchemy.orm import DeclarativeBase, relationship
12:from app.core import encryption
19:class Base(DeclarativeBase):
23:class TimestampMixin:
28:class User(Base, TimestampMixin):
42:class OAuthClientConfig(Base, TimestampMixin):
64:class GmailAccount(Base, TimestampMixin):
106:class OAuthToken(Base, TimestampMixin):
141:class RecipientList(Base, TimestampMixin):
153:class Recipient(Base, TimestampMixin):
176:class SuppressionEntry(Base, TimestampMixin):
186:class SubjectLine(Base, TimestampMixin):
195:class EmailTemplate(Base, TimestampMixin):
208:class Campaign(Base, TimestampMixin):
256:class CampaignRecipient(Base, TimestampMixin):
288:class Attachment(Base, TimestampMixin):
307:class CompanyPhoneNumber(Base, TimestampMixin):
317:class SendJob(Base, TimestampMixin):
335:class SendAttempt(Base, TimestampMixin):
352:    error_class = Column(String(60), default="")  # temporary|permanent|auth|quota|unknown
357:class EmailEvent(Base, TimestampMixin):
370:class AuditLog(Base, TimestampMixin):
383:class ApplicationSettings(Base, TimestampMixin):
===ATTACH SVC===
2:import hashlib
3:import os
4:import re
5:import secrets
7:from app.core.config import settings
19:def allowed_ext(filename: str, allowed: str = None) -> bool:
25:def safe_join(rel_path: str) -> str:
34:def save_upload(filename: str, content: bytes) -> dict:
50:def read_attachment_bytes(att) -> bytes | None:
58:def delete_attachment_file(att) -> None:
65:def scan_for_malware(content: bytes, filename: str) -> tuple:
===CAMP SVC===
14:async def start_campaign(db: AsyncSession, campaign: Campaign) -> dict:
46:async def pause_campaign(db: AsyncSession, campaign: Campaign) -> dict:
52:async def resume_campaign(db: AsyncSession, campaign: Campaign) -> dict:
56:async def cancel_campaign(db: AsyncSession, campaign: Campaign) -> dict:
===CE SVC===
19:def utcnow():
23:class RotationAssigner:
26:    def __init__(self, accounts: list, strategy: str, weights: dict = None, max_per_account: int = 200):
37:    def _available(self):
47:    def next_account(self, account_id=None):
85:    def status(self):
90:class SubjectSelector:
91:    def __init__(self, subjects: List[SubjectLine], strategy: str = "round_robin"):
96:    def next_subject(self) -> Optional[SubjectLine]:
108:class TemplateSequencer:
110:    def __init__(self, templates: List[EmailTemplate], mode: str = "repeat"):
116:    def next_template(self) -> Optional[EmailTemplate]:
133:    def stopped(self):
137:class PhoneSelector:
138:    def __init__(self, phones: List[CompanyPhoneNumber], mode: str = "round_robin"):
143:    def next_phone(self) -> str:
155:async def get_setting(db: AsyncSession, key: str, default):
163:def get_setting_sync(key: str, default):
176:async def get_campaign_entities(db: AsyncSession, campaign: Campaign):
201:async def materialize_campaign(db: AsyncSession, campaign: Campaign, recipient_rows: Sequence[Recipient],
