"""Suppression list manager: manual add, CSV/TXT import, search, delete, Clear All, bulk check. Suppressed addresses are skipped at import AND at send time.""" import csv import io from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile from pydantic import BaseModel, EmailStr from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, get_db, require_csrf from app.core.security import log_audit from app.models import SuppressionEntry, User from app.services.recipient_service import is_valid_email router = APIRouter(prefix="/suppression", tags=["suppression"]) REASON_CHOICES = ("unsubscribed", "bounced", "complaint", "manual", "other") ===SER=== def _ser_attempt(a: SendAttempt, cr_id: int = None) -> dict: return { "id": a.id, "campaign_id": a.campaign_id, "campaign_recipient_id": cr_id if cr_id is not None else (a.send_job.campaign_recipient_id if a.send_job else None), "account_id": a.gmail_account_id, "attempt_number": (a.retry_no or 0) + 1, "outcome": a.outcome, "error_class": a.error_class, "error_message": (a.failure_reason or "")[:300], "message_id": a.message_id, "thread_id": getattr(a, "thread_id", "") or "", "method": a.method, "subject": (a.subject or "")[:160], "template_name": (a.template_name or "")[:160], "provider_response": (a.provider_response or "")[:300], "created_at": a.created_at.isoformat() if a.created_at else None, } ===CONVERT=== @router.post("/convert-pdf") async def convert_html_to_pdf(template_id: int = Query(..., description="HTML template to render"), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Convert one of the user's saved HTML templates into a real PDF attachment using WeasyPrint. The generated PDF is stored and can be attached to campaigns.""" tpl = await db.get(EmailTemplate, template_id) if not tpl or tpl.user_id != user.id: raise HTTPException(status_code=404, detail="Template not found") problems = validate_html(tpl.body_html) if problems.get("errors"): raise HTTPException(status_code=400, detail=f"Template has errors: {'; '.join(problems['errors'])}") pdf_bytes = await asyncio.get_event_loop().run_in_executor(None, html_to_pdf, tpl.body_html) if not pdf_bytes: raise HTTPException(status_code=500, detail="PDF rendering failed") verdict, reason = att_svc.scan_for_malware(pdf_bytes, "converted.pdf") if not verdict: raise HTTPException(status_code=500, detail=f"PDF failed safety scan: {reason}") base = re.sub(r"\.[^.]+$", "", tpl.name or "attachment") or "attachment" fname = f"{base}.pdf" stored = att_svc.save_upload(fname, pdf_bytes) att = Attachment( user_id=user.id, name=f"{tpl.name} (PDF)"[:300], filename=fname, stored_path=stored["stored_path"], mime_type="application/pdf", size_bytes=len(pdf_bytes), kind="pdf", description=f"Generated from template '{tpl.name}' via WeasyPrint", html_source_template_id=tpl.id, file_sha256=hashlib.sha256(pdf_bytes).hexdigest(), ) db.add(att) await db.commit() await log_audit(db, user.id, "attachment.convert_pdf", "html_template", tpl.id, {"pdf_attachment_id": att.id, "size": len(pdf_bytes)}) return {"id": att.id, **_serialize(att)}