deploy/:

docs/:

scripts/:
extract_inline_js.py
---main---
"""FastAPI application entrypoint: routers, static files, security headers, startup."""
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.base import BaseHTTPMiddleware

from app.api.routers import (accounts, attachments, auth, campaigns, dashboard,
                             recipients, settings_router, subjects, suppression,
                             templates, phones, test_send, logs, unsubscribe as unsub_router)
from app.core.config import settings

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(name)s %(message)s")


@asynccontextmanager
async def lifespan(app: FastAPI):
    yield
    from app.db.session import close_engines
    await close_engines()


app = FastAPI(title=settings.APP_NAME, lifespan=lifespan,
              docs_url="/api/docs", openapi_url="/api/openapi.json")


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        response.headers.setdefault("X-Content-Type-Options", "nosniff")
        response.headers.setdefault("X-Frame-Options", "DENY")
        response.headers.setdefault("Referrer-Policy", "no-referrer")
        response.headers.setdefault("Content-Security-Policy",
                                    "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';"
                                    " script-src 'self'; frame-ancestors 'none'")
        if settings.ENVIRONMENT == "production":
            response.headers.setdefault("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
        return response


app.add_middleware(SecurityHeadersMiddleware)

app.include_router(auth.router)
app.include_router(accounts.router)
app.include_router(recipients.router)
app.include_router(subjects.router)
app.include_router(templates.router)
app.include_router(attachments.router)
app.include_router(phones.router)
app.include_router(campaigns.router)
app.include_router(test_send.router)
app.include_router(dashboard.router)
app.include_router(suppression.router)
app.include_router(settings_router.router)
app.include_router(logs.router)
app.include_router(unsub_router.router)

---config---
"""Application configuration loaded from environment variables."""
from functools import lru_cache
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
import secrets


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")

    # Application
    APP_NAME: str = "BulkGmailMailer"
    ENVIRONMENT: str = "development"
    SECRET_KEY: str = "change-me-in-production"
    BASE_URL: str = "http://localhost:8000"
    FRONTEND_BASE_URL: str = "http://localhost:8000"

    # Database / Redis
    DATABASE_URL: str = "postgresql+psycopg://mailer:mailer_dev_pw@localhost:5432/bulkmailer"
    REDIS_URL: str = "redis://localhost:6379/0"
    CELERY_BROKER_DB: int = 1
    CELERY_RESULT_DB: int = 2

    # Security
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 12
    SESSION_COOKIE_NAME: str = "mailer_session"
    CSRF_COOKIE_NAME: str = "mailer_csrf"
    ENCRYPTION_KEY: str = ""  # Fernet key; auto-generated & persisted if empty
    PASSWORD_MIN_LENGTH: int = 10

    # Rate limiting
    RATE_LIMIT_PER_MINUTE: int = 240

    # Uploads
    MAX_UPLOAD_MB: int = 10
    UPLOAD_DIR: str = "uploads"
    ALLOWED_ATTACHMENT_EXTS: str = "pdf,html,htm,txt,png,jpg,jpeg"
    ALLOWED_TEMPLATE_EXTS: str = "html,htm,txt"

    # Recipients / campaign limits
    MAX_RECIPIENTS_PER_CAMPAIGN: int = 2000
    MAX_GMAIL_ACCOUNTS_PER_CAMPAIGN: int = 10
    MAX_SUBJECTS: int = 1000

    # Sending defaults (admin-configurable at runtime via ApplicationSettings table)
    DEFAULT_SEND_INTERVAL_SECONDS: float = 1.0
    DEFAULT_MAX_PER_ACCOUNT_PER_CAMPAIGN: int = 200
    DEFAULT_RETRIES: int = 3
    DEFAULT_RETRY_DELAY_SECONDS: int = 60
    MAX_ATTACHMENT_TOTAL_MB: int = 25
    MAX_ATTACHMENT_PER_FILE_MB: float = 15.0

    # Amount ranges (defaults; admin-configurable)
    AMOUNT_MIN: float = 300.0
    AMOUNT_MAX: float = 1500.0
    SHIPPING_MIN: float = 5.0
    SHIPPING_MAX: float = 50.0

    # Invoice number format tokens:
    #   {prefix}  literal prefix e.g. INV-
    #   {r}       lowercase alphanumeric random block
    #   {R}       uppercase alphanumeric random block
    #   {d}       decimal digits only
    #   {seq4}    4-digit campaign-scoped sequence
    #   {date}    YYYYMMDD
    INVOICE_NUMBER_FORMAT: str = "{prefix}{R4}-{R5}-{R6}-{seq4}"
    INVOICE_PREFIX: str = "INV-"

    # Unsubscribe
    UNSUBSCRIBE_BASE: str = ""  # defaults to BASE_URL

    # Test-mode integration test (real Gmail send) gate
    GMAIL_TEST_ACCOUNT_EMAIL: str = ""
    GMAIL_TEST_ACCOUNT_REFRESH_TOKEN: str = ""
    GMAIL_TEST_ACCOUNT_CLIENT_ID: str = ""
    GMAIL_TEST_ACCOUNT_CLIENT_SECRET: str = ""

    # OAuth client config import default file (optional)
    GOOGLE_OAUTH_CLIENT_JSON_PATH: str = ""

    @field_validator("ENCRYPTION_KEY", mode="after")
    @classmethod
    def _enc(cls, v: str) -> str:
        return v or ""


@lru_cache
def get_settings() -> Settings:
    return Settings()


settings = get_settings()
