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()