Email Configuration
Flo has two independent email paths, each with its own provider configuration:
| Path | Used for | Provider selection |
|---|---|---|
| Transactional | Activation, OTP, password recovery, booking and subscription notifications, e-invoice forwarding | EMAIL_SENDER_EMAIL_PROVIDER environment variable |
| Bulk | Newsletters and campaigns (send queue) | NewsletterSettings.BulkProvider in the database (Communication > settings), with the env default as fallback |
Transactional sending is gated by the enable_email_sender feature flag (see Feature Flags).
Transactional Providers
Cloudflare Email (API-based)
Sends through the Cloudflare Email Service API, not SMTP. Requires:
EMAIL_SENDER_EMAIL_PROVIDER=Cloudflare
CF_EMAIL_ACCOUNT_ID=your-cloudflare-account-id
CF_EMAIL_API_TOKEN=your-cloudflare-api-token
EMAIL_SENDER_EMAIL_ALIAS=newsletter@yourdomain.com
The token is verified against /user/tokens/verify during the provider probe; it must be active. The same credentials are reused by the Cloudflare bulk sender.
Gmail with Alias
Use Gmail's "Send As" feature to send from a custom domain.
Prerequisites:
- 2FA enabled on the Gmail account
- App Password generated (not the regular password)
- Gmail "Send As" alias configured and verified
EMAIL_SENDER_EMAIL_PROVIDER=Gmail
EMAIL_SENDER_EMAIL_USERNAME=info@gmail.com
EMAIL_SENDER_EMAIL_ALIAS=newsletter@yourdomain.com
EMAIL_SENDER_EMAIL_PSW=your-gmail-app-password
Setting up Gmail "Send As":
- Gmail Settings > Accounts and Import > "Send mail as" > Add another email address
- Enter the alias email, uncheck "Treat as an alias"
- Complete email or DNS verification
- Optionally set as default sender
Outlook
Named provider preconfigured in appsettings.json (smtp.office365.com:587). Set EMAIL_SENDER_EMAIL_PROVIDER=Outlook plus username/password/alias.
Custom SMTP (BYO relay)
When EMAIL_SENDER_SMTP_HOST is set, the provider is built directly from environment variables instead of the named list. This is how Amazon SES, Mailgun, Postmark or any custom relay is configured per tenant:
EMAIL_SENDER_EMAIL_PROVIDER=Smtp
EMAIL_SENDER_SMTP_HOST=smtp.example.com
EMAIL_SENDER_SMTP_PORT=587
EMAIL_SENDER_SMTP_SSL=false
EMAIL_SENDER_SMTP_REQUIRE_TLS=true
EMAIL_SENDER_EMAIL_USERNAME=your-username
EMAIL_SENDER_EMAIL_ALIAS=sender@yourdomain.com
EMAIL_SENDER_EMAIL_PSW=your-password
EMAIL_SENDER_SMTP_REQUIRE_TLS is opt-out: TLS is required unless explicitly set to false (for an internal relay or a mail catcher).
In development, if EMAIL_SENDER_EMAIL_PROVIDER is empty the backend runs in console mode: messages are logged instead of sent.
Bulk (Newsletter) Providers
The newsletter send queue drains through one of four senders. The operator picks the provider in the Communication settings UI; the resolver reads the DB choice at drain time and falls back to the env default when the selected provider's credentials are missing.
| Provider | Credentials | Notes |
|---|---|---|
| Default | none extra | Rides the transactional SMTP dispatcher; no metered tier |
| Cloudflare | CF_EMAIL_ACCOUNT_ID, CF_EMAIL_API_TOKEN | Cloudflare Email Service bulk API |
| Amazon SES | SES_REGION, SES_ACCESS_KEY_ID, SES_SECRET_ACCESS_KEY (optional SES_CONFIGURATION_SET) | SES v2; daily-quota exhaustion parks the job until the next UTC midnight |
| Sweego | SWEEGO_API_KEY | European provider; one message per recipient via POST https://api.sweego.io/send |
Additional variables:
BULK_EMAIL_SENDER_ALIAS— from-address used only by the bulk queue (falls back toEMAIL_SENDER_EMAIL_ALIAS). Needed when the bulk provider verifies a different domain than the transactional transport.EMAIL_SENDER_EMAIL_PROVIDER=cloudflarekeeps the historical behaviour of defaulting the queue to the Cloudflare bulk sender.
All bulk senders honour the test-email sink (Communication settings): when the recipient allow-list is non-empty, every message is redirected to those addresses only.
Email Templates
Templates are MJML files rendered through Handlebars. They are brand-specific folders under Flo.BE/Services/EmailSender/Templates/:
| Folder | Brand |
|---|---|
flo/ | Default Flo brand (EMAIL_TEMPLATE_BRAND unset) |
pf/ | Pietro Franceschini |
rs/ | RespiraStudio |
Each transactional template ships in Italian and English (*.it.mjml, *.en.mjml); newsletters also have a *.neutral.mjml variant. Shared header/footer/css partials are inlined at render time.
Coverage includes (flo folder): activation, email-confirmation, password-recovery, otp-verification, booking-confirmation, booking-cancellation, booking-cancellation-closure, subscription-purchase, subscription-reminder, client-subscription-created/suspended/reactivated/cancelled, commerce-payment-failed, commerce-order-refunded, einvoice-forward, immobile-access-confirmation, newsletter-confirmation, newsletter-double-opt-in, blog-newsletter, real-estate-newsletter, element-newsletter, custom-newsletter.
Brand variables resolved at render time: AppUrl, FEAppUrl, CustomerNameShort (uppercased for the logo), CustomerNameFull, year, and the legal links below.
| Variable | Effect |
|---|---|
EMAIL_LEGAL_BASE_URL | Base for /privacy-policy and /terms-conditions links (default FEAppUrl) |
EMAIL_PRIVACY_POLICY_URL | Full privacy-policy URL override |
EMAIL_TERMS_URL | Full terms URL override |
Delivery Log & Replay
Every transactional and bulk send writes an EmailDeliveryLog row (provider, channel, status, from/to, subject, error, optional MIME payload). Rows are pruned after 180 days.
- Viewer: Settings > Sent emails, SuperAdmin-only (
GET /api/v1/email-delivery-logs). The grid merges the local log with live Cloudflare analytics (datasetemailSendingAdaptive, 31-day retention) whenCF_EMAIL_ZONE_IDand an analytics token are configured; without them it still runs on the local log. - Replay:
POST /api/v1/maintenance/email/replay-failed, authenticated with theX-Maintenance-Keyheader (Maintenance__ApiKey). It replays failed transactional messages:- exact mode re-dispatches the stored MIME payload with a new
Message-Id; - legacy mode reconstructs messages that predate payload storage (password recovery, password creation, email confirmation) from current data, minting a new token only at send time.
- Filters:
from/to,recipients,userIds,limit;dryRunreports what would be replayed; requiresenable_email_senderto actually send.
- exact mode re-dispatches the stored MIME payload with a new
Testing
Verify Configuration on VPS
# Check environment variables
sudo docker exec flo env | grep EMAIL
# Restart container after changes
sudo docker restart flo
# Monitor logs for email sending
sudo docker logs -f flo
Expected Email Headers
From: newsletter@yourdomain.com
Reply-To: newsletter@yourdomain.com
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Emails show Gmail address as sender | Gmail alias not verified | Complete "Send As" verification |
| SMTP authentication failed | Wrong app password or 2FA not enabled | Generate a new app password with 2FA |
| Container ignores new env vars | Env loaded at container start | Restart: sudo docker restart flo |
| Newsletter falls back to another provider | Selected bulk provider's credentials are missing | Set the provider's env vars or re-select in the UI |
| Bulk job parked | Provider quota exhausted (429/402) | Wait for the next UTC midnight; check the provider plan |
| Delivery log has no live Cloudflare rows | No CF_EMAIL_ZONE_ID / analytics token, or token lacks Account Analytics: Read | Configure the analytics token; the local log still works |
| Replay answers 503 | Replay service unavailable (no email provider resolvable) | Configure a transactional provider |
| Replay refuses to send | enable_email_sender is off | Enable the flag (it returns errors.emailDeliveryLog.replayEmailDisabled) |
Code Reference
| File | Purpose |
|---|---|
Flo.BE/Services/EmailSender/EmailSenderService.cs | Transactional sending, template rendering, delivery logging |
Flo.BE/Services/EmailSender/Core/EmailProviderResolver.cs | Provider selection (named, API-based, BYO SMTP, console) |
Flo.BE/Services/EmailSender/Core/CredentialsProvider.cs | Credentials and alias resolution |
Flo.BE/Services/EmailSender/Core/CloudflareEmailDispatcher.cs | Cloudflare Email Service API dispatcher |
Flo.BE/Services/EmailSender/Bulk/BulkEmailSenderResolver.cs | DB-backed bulk provider resolution |
Flo.BE/Services/EmailSender/Bulk/* | Cloudflare / SES / Sweego / legacy bulk senders |
Flo.BE/Services/EmailSender/Core/EmailReplayService.cs | Failed-message replay |
Flo.BE/Services/EmailSender/Core/EmailTemplateRenderer.cs | MJML + Handlebars rendering, brand folders, legal links |
Flo.BE/Services/EmailSender/Templates/{flo,pf,rs}/ | Brand template sets |