Sistema Email Completo per FastAPI con SendGrid
- Backend
- 05 Mar, 2026
Come ho costruito un sistema email affidabile per FastAPI
Sistema email completo per app FastAPI, da template HTML a deliverability in produzione
โฑ๏ธ 12 minuti di lettura | ๐ป Codice completo su GitHub โ | ๐ฏ Livello: Intermedio
TL;DR
Cosa imparerai: Sistema email completo per app FastAPI, da template HTML a deliverability in produzione
Stack:
- โ SMTP con SendGrid/AWS SES (setup in 2 minuti)
- โ Template HTML responsive (Jinja2)
- โ Background tasks (non blocca request)
- โ Retry automatico
- โ SPF/DKIM per evitare spam folder
Quando ti serve:
- Welcome email dopo registrazione
- Password reset
- Notifiche transazionali
- Digest settimanali
Cosa NON copre:
- Marketing campaigns massive (usa Mailchimp)
- SMS/Push notifications
- Email parsing/receiving
Quick Start in 5 minuti | Template Examples | Anti-Spam Setup
Il Problema: Email in unโApp Async
Integrare email in unโapp FastAPI tocca tre problemi distinti che non sono ovvi allโinizio.
Il primo approccio naif:
@router.post("/register")
async def register(user_data: UserCreate):
user = await user_repo.create(user_data)
# TODO: Send welcome email
print(f"Should send email to {user.email}")
return user
Poi si implementa davvero, con SMTP sincrono:
@router.post("/register")
async def register(user_data: UserCreate):
user = await user_repo.create(user_data)
msg = MIMEText("Welcome!")
msg['To'] = user.email
smtp = smtplib.SMTP('smtp.gmail.com', 587)
smtp.login('me@gmail.com', 'mypassword') # hardcoded
smtp.send_message(msg) # blocca per 2-3 secondi
smtp.quit()
return user
I tre problemi strutturali:
- Latenza: ogni request aspetta che lโemail venga consegnata. SMTP sincrono in un handler async blocca lโevent loop.
- Deliverability: email da IP senza SPF/DKIM finiscono in spam. Gmail e Outlook applicano filtri aggressivi.
- Resilienza: se il server SMTP รจ down, la request fallisce con 500. Nessun retry, email persa.
Lโarchitettura corretta separa questi tre aspetti:
- invio asincrono via background tasks โ zero blocking
- provider dedicato (SendGrid, SES) con SPF/DKIM โ deliverability garantita
- retry automatico con exponential backoff โ resilienza ai failure temporanei
Anatomia Sistema Email Moderno
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FastAPI Endpoint โ
โ - Registrazione โ
โ - Password reset โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Trigger background task
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ EmailService โ
โ - Render template (Jinja2) โ
โ - Queue non-blocking send โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Send async
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SMTP Provider โ
โ - SendGrid / AWS SES โ
โ - SPF/DKIM validation โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ
[User Inbox] ๐ง
Componenti chiave:
- SMTP Provider - SendGrid/SES, non Gmail
- Jinja2 Templates - HTML professionale
- Background Tasks - FastAPI native, zero blocking
- Retry Logic - 3 tentativi con backoff
- DNS Records - SPF/DKIM per deliverability
Quick Start (5 Minuti)
Step 1: SMTP Provider (2 minuti)
Raccomandazione: SendGrid (100 email/giorno free)
# 1. Signup su sendgrid.com
# 2. Create API Key (Settings โ API Keys)
# 3. Aggiungi a .env
# .env
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=SG.your_actual_api_key_here
SMTP_FROM_EMAIL=noreply@yourdomain.com
SMTP_FROM_NAME="Your App"
Alternative:
- AWS SES - economico a volume (โฌ0.10 per 1000 email)
- Mailgun - ottimo per transazionali
Step 2: Install Dependencies (30 secondi)
pip install fastapi aiosmtplib jinja2 pydantic-settings beautifulsoup4
Step 3: EmailService Base (2 minuti)
# app/services/email_service.py
import aiosmtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from app.config import settings
class EmailService:
def __init__(self):
# Setup Jinja2 templates
template_dir = Path(__file__).parent.parent / "templates" / "emails"
self.jinja_env = Environment(
loader=FileSystemLoader(str(template_dir)),
autoescape=True
)
async def send_email(
self,
to_email: str,
subject: str,
html_content: str
):
"""Send HTML email via SMTP."""
message = MIMEMultipart("alternative")
message["Subject"] = subject
message["From"] = f"{settings.SMTP_FROM_NAME} <{settings.SMTP_FROM_EMAIL}>"
message["To"] = to_email
# Attach HTML
html_part = MIMEText(html_content, "html")
message.attach(html_part)
# Send async
await aiosmtplib.send(
message,
hostname=settings.SMTP_HOST,
port=settings.SMTP_PORT,
username=settings.SMTP_USER,
password=settings.SMTP_PASSWORD,
start_tls=True
)
Step 4: Use in Endpoint (30 secondi)
# app/routers/auth.py
from fastapi import BackgroundTasks
from app.services.email_service import EmailService
email_service = EmailService()
@router.post("/register")
async def register(
user_data: UserCreate,
background_tasks: BackgroundTasks
):
# Create user
user = await user_repo.create(user_data)
# Send email in background (non-blocking!)
background_tasks.add_task(
email_service.send_welcome_email,
user
)
return user # Risposta immediata!
Done! Request ritorna subito, email inviata in background.
Template HTML con Jinja2
Base Template Responsive
<!-- templates/emails/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: #f4f4f4;
}
.email-container {
max-width: 600px;
margin: 20px auto;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
text-align: center;
color: #ffffff;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.content {
padding: 30px 25px;
color: #333333;
line-height: 1.6;
}
.button {
display: inline-block;
padding: 12px 24px;
margin: 15px 0;
background-color: #667eea;
color: #ffffff !important;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
}
.footer {
background-color: #f8f9fa;
padding: 20px;
text-align: center;
font-size: 12px;
color: #6c757d;
}
@media only screen and (max-width: 600px) {
.content { padding: 20px 15px !important; }
.header h1 { font-size: 20px !important; }
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
{% block header %}
<h1>Your App</h1>
{% endblock %}
</div>
<div class="content">
{% block content %}{% endblock %}
</div>
<div class="footer">
{% block footer %}
<p>ยฉ 2025 Your App. Tutti i diritti riservati.</p>
<p><a href="{{ unsubscribe_url }}">Cancellati</a></p>
{% endblock %}
</div>
</div>
</body>
</html>
Welcome Email
<!-- templates/emails/welcome.html -->
{% extends "base.html" %}
{% block header %}
<h1>๐ Benvenuto, {{ username }}!</h1>
{% endblock %}
{% block content %}
<h2>Il tuo account รจ pronto!</h2>
<p>Ciao <strong>{{ username }}</strong>,</p>
<p>
Grazie per esserti registrato! Il tuo account รจ stato creato con successo.
</p>
<p>
Per iniziare, conferma il tuo indirizzo email:
</p>
<center>
<a href="{{ confirm_url }}" class="button">
Conferma Email
</a>
</center>
<p style="font-size: 14px; color: #6c757d;">
Se non hai richiesto questa registrazione, ignora questa email.
</p>
<p>
Buon divertimento! ๐ฎ
</p>
{% endblock %}
Password Reset
<!-- templates/emails/password_reset.html -->
{% extends "base.html" %}
{% block header %}
<h1>๐ Reset Password</h1>
{% endblock %}
{% block content %}
<h2>Richiesta reset password</h2>
<p>Ciao <strong>{{ username }}</strong>,</p>
<p>
Abbiamo ricevuto una richiesta per resettare la password del tuo account.
</p>
<center>
<a href="{{ reset_url }}" class="button">
Reset Password
</a>
</center>
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<strong>โ ๏ธ Importante:</strong> Questo link รจ valido per <strong>1 ora</strong>.
</div>
<p>
Se non hai richiesto tu questa operazione, ignora questa email.
</p>
{% endblock %}
EmailService con Templates
# app/services/email_service.py
class EmailService:
def render_template(self, template_name: str, **context) -> str:
"""Render Jinja2 template."""
template = self.jinja_env.get_template(template_name)
return template.render(**context)
async def send_welcome_email(self, user: User):
"""Send welcome email con conferma account."""
# Generate confirm token
confirm_token = generate_token(user.id)
# Render template
html_content = self.render_template(
"welcome.html",
username=user.username,
confirm_url=f"{settings.APP_URL}/confirm/{confirm_token}",
unsubscribe_url=f"{settings.APP_URL}/unsubscribe/{user.id}"
)
# Send
await self.send_email(
to_email=user.email,
subject=f"Benvenuto, {user.username}! ๐",
html_content=html_content
)
async def send_password_reset(self, user: User, reset_token: str):
"""Send password reset email."""
html_content = self.render_template(
"password_reset.html",
username=user.username,
reset_url=f"{settings.APP_URL}/reset-password/{reset_token}",
unsubscribe_url=f"{settings.APP_URL}/unsubscribe/{user.id}"
)
await self.send_email(
to_email=user.email,
subject="๐ Reset Password Richiesto",
html_content=html_content
)
Background Tasks & Retry
Problema: Email Blocking
# BAD: Request waits for email (3 seconds!)
@router.post("/register")
async def register(user_data: UserCreate):
user = await user_repo.create(user_data)
await email_service.send_welcome_email(user) # BLOCCA!
return user
# GOOD: Background, risposta immediata
@router.post("/register")
async def register(user_data: UserCreate, background_tasks: BackgroundTasks):
user = await user_repo.create(user_data)
background_tasks.add_task(email_service.send_welcome_email, user)
return user # Risponde subito!
Retry Logic con Exponential Backoff
# app/services/email_service.py
import asyncio
import logging
logger = logging.getLogger(__name__)
class EmailService:
async def send_email_with_retry(
self,
to_email: str,
subject: str,
html_content: str,
max_retries: int = 3
) -> bool:
"""Send email con retry automatico."""
for attempt in range(max_retries):
try:
await self.send_email(to_email, subject, html_content)
logger.info(
f"โ
Email sent to {to_email}",
extra={"attempt": attempt + 1}
)
return True
except aiosmtplib.SMTPException as e:
logger.warning(
f"โ ๏ธ Email failed (attempt {attempt + 1}/{max_retries}): {e}",
extra={"to": to_email}
)
# Last attempt failed
if attempt == max_retries - 1:
logger.error(f"โ Email failed after {max_retries} attempts")
return False
# Exponential backoff: 1s, 2s, 4s
wait_time = 2 ** attempt
await asyncio.sleep(wait_time)
return False
# Use retry for all send methods
async def send_welcome_email(self, user: User):
html_content = self.render_template("welcome.html", ...)
success = await self.send_email_with_retry(
to_email=user.email,
subject="Benvenuto!",
html_content=html_content
)
if not success:
# Log for monitoring
logger.error(f"Failed to send welcome email to {user.email}")
Perchรฉ funziona:
- 1ยฐ tentativo fallisce โ aspetta 1s โ riprova
- 2ยฐ tentativo fallisce โ aspetta 2s โ riprova
- 3ยฐ tentativo fallisce โ log error, stop
Risolve 90% dei failure temporanei (SMTP timeout, network glitch).
Deliverability: Evitare lo Spam
Il Problema
Le tue email finiscono nello spam perchรฉ:
- โ Dominio non verificato
- โ Niente SPF/DKIM
- โ IP reputation bassa
- โ Contenuto โspammyโ
SPF/DKIM Setup (15 minuti)
SPF (Sender Policy Framework)
Dice a Gmail/Outlook: โSolo SendGrid puรฒ inviare email per mio contoโ
# Add TXT DNS record for your domain
Name: @
Type: TXT
Value: v=spf1 include:sendgrid.net ~all
TTL: 3600
DKIM (DomainKeys Identified Mail)
Firma digitale che verifica autenticitร email.
# SendGrid provides these CNAME records
Name: s1._domainkey.yourdomain.com
Type: CNAME
Value: s1.domainkey.u123456.wl.sendgrid.net
Name: s2._domainkey.yourdomain.com
Type: CNAME
Value: s2.domainkey.u123456.wl.sendgrid.net
Verifica Setup:
- Vai su mxtoolbox.com/SuperTool.aspx
- Inserisci
yourdomain.com - Check SPF/DKIM records
Risultato: Da 60% delivery โ 98%+ delivery.
Best Practices Anti-Spam
# app/services/email_service.py
class EmailService:
# 1. Sempre unsubscribe link (passare user_id esplicitamente)
async def send_email(self, to_email: str, subject: str, html: str, user_id: str = ""):
# Include in footer
unsubscribe = f"{settings.APP_URL}/unsubscribe/{user_id}"
# 2. Plain text alternative (migliora deliverability)
def create_message(self, html_content: str):
message = MIMEMultipart("alternative")
# Plain text version
text_content = self.html_to_text(html_content)
message.attach(MIMEText(text_content, "plain"))
# HTML version
message.attach(MIMEText(html_content, "html"))
return message
def html_to_text(self, html: str) -> str:
"""Convert HTML to plain text (basic)."""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
return soup.get_text()
Checklist:
- โ SPF/DKIM records configurati
- โ Unsubscribe link in ogni email
- โ Physical address in footer (GDPR)
- โ Plain text alternative
- โ Evita parole spam (FREE!!! CLICK NOW!!!)
- โ Ratio testo/immagini > 60% testo
- โ Non usare URL shortener
Testing
Mock per Development
# app/services/email_service.py
class EmailService:
def __init__(self):
self.mock = settings.ENVIRONMENT == "development"
async def send_email(self, to_email: str, subject: str, html: str):
if self.mock:
# Save to file instead of sending
self._save_to_file(to_email, subject, html)
logger.info(f"๐ง [MOCK] Email saved to file: {to_email}")
return
# Real send in production
await aiosmtplib.send(...)
def _save_to_file(self, to_email: str, subject: str, html: str):
"""Save email as HTML file for review."""
import os
from datetime import datetime
os.makedirs("emails_debug", exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"emails_debug/{timestamp}_{to_email.replace('@', '_')}.html"
with open(filename, "w") as f:
f.write(f"<!-- To: {to_email} -->\n")
f.write(f"<!-- Subject: {subject} -->\n\n")
f.write(html)
Apri emails_debug/ e vedi le email generate. Perfetto per development.
Unit Test Base
# tests/test_email_service.py
import pytest
from unittest.mock import patch, AsyncMock
from app.services.email_service import EmailService
@pytest.fixture
def email_service():
return EmailService()
@pytest.mark.asyncio
async def test_render_welcome_template(email_service):
"""Test template rendering."""
html = email_service.render_template(
"welcome.html",
username="TestUser",
confirm_url="https://example.com/confirm/token"
)
assert "Benvenuto, TestUser!" in html
assert "confirm/token" in html
@pytest.mark.asyncio
@patch('aiosmtplib.send')
async def test_send_email(mock_send, email_service):
"""Test email sending (mocked)."""
mock_send.return_value = None
await email_service.send_email(
to_email="test@example.com",
subject="Test",
html_content="<h1>Test</h1>"
)
# Verify SMTP called
mock_send.assert_called_once()
Errori Comuni (Che Ho Fatto)
1. Password Hardcoded
# NEVER
SMTP_PASSWORD = "mypassword123"
# ALWAYS
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
Mi รจ costato: Account compromesso, spam inviate dal mio IP.
2. Niente Rate Limiting
# No rate limiting
@router.post("/send-email")
async def send_email(email: str):
await email_service.send(email)
Risultato: 10,000 email in 1 minuto โ account bannato.
โ Fix:
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
@router.post("/send-email")
@limiter.limit("5/minute")
async def send_email(request: Request, email: str):
await email_service.send(email)
3. Dimenticare Unsubscribe
# GDPR violation, ends up in spam
# No way to unsubscribe
# Required in every email
<a href="{{ unsubscribe_url }}">Cancellati</a>
4. SMTP Sincrono
# Blocks request
await email_service.send(email) # 3 seconds!
# Background
background_tasks.add_task(email_service.send, email)
5. Niente Logging
# If it fails, you won't know
await email_service.send(email)
# Always log
logger.info(f"Sending email to {email}")
try:
await email_service.send(email)
logger.info(f"โ
Email sent to {email}")
except Exception as e:
logger.error(f"โ Email failed: {email}, error: {e}")
Conclusione
Next Steps
Quick Start:
git clone https://github.com/layerbylayer-blog/fastapi-email-system
cd fastapi-email-system
# Copia .env.example e configura
cp .env.example .env
# Run
uvicorn app.main:app --reload
Nel Tuo Progetto:
Week 1:
- Setup SendGrid account
- Configura SPF/DKIM DNS records
- Implementa EmailService base
- Crea 1-2 template essenziali
Week 2: 5. Background tasks tutti gli endpoint 6. Retry logic 7. Testing mock 8. Deploy e monitor
Risorse
GitHub: fastapi-email-system
Include:
- โ EmailService completo
- โ 5+ template pronti
- โ Testing suite
- โ Docker compose
- โ Environment setup
Tools:
- SendGrid - SMTP provider (100 email/day free)
- AWS SES - Production scalable
- Mail Tester - Test spam score
- MXToolbox - Verifica DNS/SPF/DKIM
Prossimo articolo: Logging strutturato con context e correlation IDs