220 app/api/routers/accounts.py
759 app/api/routers/campaigns.py
186 app/api/routers/test_send.py
118 app/api/routers/unsubscribe.py
125 app/api/routers/dashboard.py
1408 total
=====
"""Public unsubscribe endpoints: tokenized link from sent emails (/unsubscribe/{token}).
Adds the email to the suppression list, records an EmailEvent, and never exposes
any account data. No authentication required (token IS the credential)."""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, rate_limit_user
from app.db.session import AsyncSessionLocal
from app.models import Campaign, CampaignRecipient, EmailEvent, Recipient, SuppressionEntry
router = APIRouter(tags=["unsubscribe"])
UNSUB_PAGE = """
Unsubscribe — {title}
"""
def _page(icon: str, heading: str, message: str) -> HTMLResponse:
html = UNSUB_PAGE.format(title=heading, icon=icon, heading=heading, message=message)
return HTMLResponse(html, status_code=200)
@router.get("/unsubscribe/{token}", response_class=HTMLResponse)
async def unsubscribe_page(token: str, db: AsyncSession = Depends(get_db)):
"""Public unsubscribe landing page. Shows confirmation form (one-click safe)."""
if len(token) < 20 or len(token) > 64:
return _page("❌", "Invalid link",
"This unsubscribe link is invalid or malformed.")
rec = (await db.execute(
select(Recipient).where(Recipient.unsub_token == token))).scalar()
if not rec:
return _page("❌", "Invalid link",
"We could not find this unsubscribe link. It may have expired "
"or the recipient list was removed.")
email = rec.email
already = (await db.execute(
select(SuppressionEntry).where(SuppressionEntry.email == email))).scalar()
if already:
return _page("✅", "Already unsubscribed",
f'{email} is already on our '
'do-not-contact list. You will not receive further emails.')
return _page("📮", "Unsubscribe",
f'Do you want to stop receiving emails at '
f'{email}? Press the button to confirm. '
f'')
@router.post("/unsubscribe/{token}", response_class=HTMLResponse)
async def unsubscribe_confirm(token: str, db: AsyncSession = Depends(get_db)):
"""Confirm unsubscribe: add to suppression list + record event. Idempotent."""
if len(token) < 20 or len(token) > 64:
return _page("❌", "Invalid link", "This unsubscribe link is invalid.")
rec = (await db.execute(
select(Recipient).where(Recipient.unsub_token == token))).scalar()
if not rec:
return _page("❌", "Invalid link",
"We could not find this unsubscribe link.")
email = rec.email
existing = (await db.execute(
select(SuppressionEntry).where(SuppressionEntry.email == email))).scalar()
if not existing:
db.add(SuppressionEntry(user_id=rec.list.user_id if rec.list else None,
email=email, reason="unsubscribed",
source="unsubscribe-link"))
await db.commit()
# record event on the most recent campaign_recipient for this email (if any)
cr = (await db.execute(
select(CampaignRecipient).where(CampaignRecipient.email == email)
.order_by(CampaignRecipient.id.desc()).limit(1))).scalar()
if cr:
db.add(EmailEvent(campaign_id=cr.campaign_id,
campaign_recipient_id=cr.id,
event_type="unsubscribed",
detail=f"Unsubscribed via token link from {email}"))
rec.status = "unsubscribed"
await db.commit()
return _page("✅", "Unsubscribed",
f'{email} has been added to our '
'do-not-contact list. You will not receive further emails from us. '
'This took effect immediately.')
@router.get("/unsubscribe-status/{token}")
async def unsubscribe_status(token: str, db: AsyncSession = Depends(get_db)):
"""JSON status endpoint (used by List-Unsubscribe-Post one-click handlers)."""
rec = (await db.execute(
select(Recipient).where(Recipient.unsub_token == token))).scalar()
if not rec:
return {"subscribed": False, "error": "invalid token"}
suppressed = (await db.execute(
select(SuppressionEntry).where(SuppressionEntry.email == rec.email))).scalar()
return {"subscribed": not bool(suppressed), "email_domain": rec.email.split("@")[-1]}