"""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 === db/session === """Async SQLAlchemy engine/session and sync engine for Celery workers.""" import os from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from app.core.config import settings from app.models import Base DB_URL = settings.DATABASE_URL if DB_URL.startswith("postgres://"): DB_URL = DB_URL.replace("postgres://", "postgresql://", 1) sync_engine = create_engine(DB_URL, pool_pre_ping=True, pool_size=5, max_overflow=10) SyncSession = sessionmaker(bind=sync_engine, expire_on_commit=False) async_engine = create_async_engine(DB_URL, pool_pre_ping=True, pool_size=5, max_overflow=10) AsyncSessionLocal = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) async def init_db() -> None: """Create tables (dev convenience; production uses Alembic migrations).""" async with async_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def close_engines() -> None: await async_engine.dispose() sync_engine.dispose() def check_db_health() -> bool: try: with sync_engine.connect() as c: c.execute(text("SELECT 1")) return True except Exception: return False