Configure SMTP for transactional emails, security notifications, and passwordless login. Test your setup from the Admin UI.
FastCMS sends transactional emails (verification, password reset, OTP) and security alert emails via any standard SMTP provider. Configuration is done through environment variables.
Configuration
Set these variables in your .env file:
# SMTP server
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASSWORD=your-app-password
# Sender identity
SMTP_FROM_EMAIL=noreply@yourapp.com
SMTP_FROM_NAME=YourApp
# Security notifications (see below)
SECURITY_NOTIFICATIONS_ENABLED=true
SECURITY_LOGIN_NOTIFICATIONS=falseEmail is disabled by default. FastCMS checks for SMTP_USER and SMTP_PASSWORD — if either is empty, no email will be sent and all operations that require email will either fail gracefully or be skipped.
Provider examples
Gmail (App Password):
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASSWORD=xxxx-xxxx-xxxx-xxxx # App-specific password, not your account passwordAmazon SES:
SMTP_HOST=email-smtp.us-east-1.amazonaws.com
SMTP_PORT=587
SMTP_USER=AKIAIOSFODNN7EXAMPLE
SMTP_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEYResend / SendGrid / Mailgun:
SMTP_HOST=smtp.resend.com # or smtp.sendgrid.net / smtp.mailgun.org
SMTP_PORT=587
SMTP_USER=resend # provider-specific
SMTP_PASSWORD=re_xxxxxxxxxxxxx # API key used as passwordTransactional Emails
FastCMS automatically sends the following emails when SMTP is configured:
| Event | Trigger | Expiry |
|---|---|---|
| Email verification | User registers | 24 hours |
| Password reset | POST /api/v1/auth/request-password-reset | 1 hour |
| Email change confirmation | POST /api/v1/auth/request-email-change | 24 hours |
| OTP (passwordless login) | POST /api/v1/auth/request-otp | 10 minutes |
All email links include BASE_URL as the base, so set it correctly in production:
BASE_URL=https://yourdomain.comSecurity Notifications
When SECURITY_NOTIFICATIONS_ENABLED=true, FastCMS emails users on security-relevant events:
| Notification | When sent | Default |
|---|---|---|
| Password changed | User changes or resets their password | ✓ On |
| 2FA enabled | Two-factor authentication turned on | ✓ On |
| 2FA disabled | Two-factor authentication turned off | ✓ On |
| Account locked | Too many failed login attempts | ✓ On |
| New login | Every successful sign-in | Off (opt-in) |
Enable the login alert:
SECURITY_LOGIN_NOTIFICATIONS=trueLogin notifications are off by default because they fire on every successful login — suitable for high-security apps but noisy for consumer apps.
Admin UI
Go to Admin → Email (/admin/settings/email) to:
- View your current SMTP configuration (password masked)
- See which security notifications are active
- Send a test email to verify your setup
Send a test email via API
POST /api/v1/admin/email/test
Authorization: Bearer ADMIN_TOKEN
Content-Type: application/json
{"to_email": "you@example.com"}Response:
{"success": true, "message": "Test email sent to you@example.com"}Check SMTP status:
GET /api/v1/admin/email/status
Authorization: Bearer ADMIN_TOKEN{
"smtp_enabled": true,
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_user": "you***ail.com",
"smtp_from_email": "noreply@yourapp.com",
"smtp_from_name": "YourApp",
"security_notifications_enabled": true,
"security_login_notifications": false
}Email Templates
All emails use a consistent branded template with:
- Your
APP_NAMEin the header - HTML + plain-text parts (for accessibility and spam filter compatibility)
- Call-to-action button with a fallback link
- Branded footer
Sending custom emails from a plugin
# plugins/my_plugin/hooks.py
from app.services.email_service import EmailService, _html_wrap, _plain_text
async def notify_user(user_email: str, order_id: str) -> None:
html = _html_wrap(
title="Your order is confirmed!",
body=f"Order <strong>{order_id}</strong> has been placed successfully.",
cta_url=f"https://yourapp.com/orders/{order_id}",
cta_label="View Order",
)
plain = _plain_text(
"Your order is confirmed!",
f"Order {order_id} has been placed.",
f"https://yourapp.com/orders/{order_id}",
)
await EmailService.send_email(
to_email=user_email,
subject=f"Order {order_id} confirmed",
html_content=html,
plain_content=plain,
)Troubleshooting
| Problem | Solution |
|---|---|
| Emails not sent, no error | SMTP_USER or SMTP_PASSWORD is empty — email is silently skipped |
Connection refused | Wrong SMTP_HOST or SMTP_PORT — try port 465 for SSL |
Authentication failed | Wrong credentials. For Gmail, use an App Password, not your account password |
| Links in emails go to wrong domain | Set BASE_URL to your production domain |
| Test email succeeds but real emails don't | Check that SMTP_FROM_EMAIL is a verified sender in your provider |
| Emails end up in spam | Set up SPF, DKIM, and DMARC records for your sending domain |
Notes
- SMTP uses STARTTLS on port 587 (the most compatible mode). SSL on port 465 is not currently supported — use an SMTP relay that supports STARTTLS.
- All emails are sent asynchronously — SMTP runs in a thread-pool executor so it never blocks the request cycle.
- Security notification emails fire as
asyncio.create_task(fire-and-forget) — a delivery failure won't affect the user-facing response.