"""Send Test Email: REAL send through a connected Gmail account with provider-confirmed results (message ID from Gmail), full variable substitution and optional attachments. This is the compliance-friendly way to verify end-to-end delivery before a campaign.""" import asyncio import base64 from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, EmailStr, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, get_db, require_csrf from app.core.config import settings as cfg from app.core.security import log_audit from app.models import EmailTemplate, GmailAccount, OAuthToken, User from app.services import message_builder from app.services.campaign_engine import get_setting from app.services.gmail_service import build_gmail_client_from_account from app.services.variable_engine import GeneratedValues, VariableEngine from app.db.session import SyncSession router = APIRouter(prefix="/test-send", tags=["test-send"]) class TestSendIn(BaseModel): account_id: int to_email: EmailStr subject: str = Field(min_length=1, max_length=500) template_id: Optional[int] = None body_html: str = "" body_text: str = "" include_pdf: bool = False use_random_variables: bool = True @router.post("/send") async def send_test(payload: TestSendIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """REAL test send through Gmail API or SMTP XOAUTH2 using an authorized account. Returns the provider's message ID — never a fake success.""" import time t0 = time.monotonic() account = await db.get(GmailAccount, payload.account_id) if not account or account.user_id != user.id: raise HTTPException(status_code=404, detail="Gmail account not found") if account.status != "authorized" or not account.is_active: raise HTTPException(status_code=409, detail=f"Account is {account.status}; re-authorize first") if account.cooldown_until and account.cooldown_until > datetime.now(timezone.utc): raise HTTPException(status_code=409, detail="Account is in cooldown") # resolve body: template (with real variable substitution) or raw body template = None if payload.template_id: template = await db.get(EmailTemplate, payload.template_id) if not template or template.user_id != user.id: