Skip to content

Commit e77b114

Browse files
update
1 parent 0d68313 commit e77b114

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed

docs/staging-email-options.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# Staging Email Provider Options
2+
3+
## Current State
4+
5+
The staging environment has no working email delivery. `MAIL_HOST` defaults to `localhost:1025` (Mailpit dev sink), which silently fails inside the Azure Container App. Affected flows:
6+
7+
- Password reset (forgot-password sends a tokenized link)
8+
- Email verification (signup sends a confirmation link)
9+
- Payment reminders and overdue notifications (Celery worker jobs)
10+
11+
Regardless of provider chosen, the following code changes are required:
12+
13+
| File | Change |
14+
|------|--------|
15+
| `backend/app/services/email_service.py` | Add TLS + auth to SMTP, or swap to provider SDK |
16+
| `backend/app/services/email_service.py` | Replace hardcoded `http://localhost:5173` reset URL with config value |
17+
| `backend/app/services/email_service.py` | Replace `noreply@lendq.local` sender with real address |
18+
| `backend/app/config.py` | Add `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `FRONTEND_URL` config |
19+
| `ops/azure/modules/container-apps.bicep` | Add mail env vars to container app |
20+
| `.github/workflows/deploy-staging.yml` | Set mail env vars or secrets |
21+
22+
---
23+
24+
## Option 1: Resend
25+
26+
**What**: Developer-focused transactional email API. REST/SDK-based, no SMTP needed.
27+
28+
| | |
29+
|---|---|
30+
| **Free tier** | 100 emails/day, 1 domain |
31+
| **Setup effort** | ~30 min |
32+
| **Domain verification** | Required (DNS TXT record) OR use `onboarding@resend.dev` for testing |
33+
| **SDK** | `pip install resend` — 5-line send call |
34+
| **Human in the loop** | DNS record for domain verification (unless using test sender) |
35+
36+
**Pros**
37+
- Excellent developer experience, minimal code
38+
- Built-in delivery tracking and logs in dashboard
39+
- Generous free tier for staging
40+
- Webhook support for bounce/complaint handling
41+
- No SMTP configuration complexity
42+
43+
**Cons**
44+
- Third-party dependency outside Azure ecosystem
45+
- Requires Resend account creation (manual signup)
46+
- Domain verification needs DNS access
47+
- Vendor lock-in on API (though easy to swap)
48+
49+
**Can be fully automated (no human)?** Partially. Code changes and deployment can be automated. Account signup and DNS verification require a human. Using the `onboarding@resend.dev` test sender avoids DNS but limits the sender address.
50+
51+
**Code change**:
52+
```python
53+
import resend
54+
resend.api_key = os.environ["RESEND_API_KEY"]
55+
56+
def send_email(self, to, subject, body):
57+
resend.Emails.send({
58+
"from": os.environ.get("MAIL_FROM", "noreply@lendq.com"),
59+
"to": to,
60+
"subject": subject,
61+
"html": body,
62+
})
63+
```
64+
65+
---
66+
67+
## Option 2: Gmail SMTP with App Password
68+
69+
**What**: Use a Gmail account as an SMTP relay via App Password.
70+
71+
| | |
72+
|---|---|
73+
| **Free tier** | 500 emails/day |
74+
| **Setup effort** | ~15 min |
75+
| **Domain verification** | None (sends from @gmail.com) |
76+
| **SDK** | None — uses stdlib `smtplib` already in code |
77+
| **Human in the loop** | Must create App Password in Google Account settings (requires 2FA enabled) |
78+
79+
**Pros**
80+
- Zero cost
81+
- No new dependencies — uses existing `smtplib` code with minor changes
82+
- No domain verification needed
83+
- Familiar and well-documented
84+
- Fast to set up
85+
86+
**Cons**
87+
- Sends from a @gmail.com address (not branded)
88+
- Google can throttle or block if flagged as spam
89+
- App Password must be created manually in Google Account
90+
- Not suitable for production (rate limits, deliverability)
91+
- Tied to a personal/shared Google account
92+
93+
**Can be fully automated (no human)?** No. Creating the App Password requires logging into Google Account with 2FA and generating the password manually. Once created, the rest (code + deploy) is automatable.
94+
95+
**Code change** (minimal — add 2 lines to existing `send_email`):
96+
```python
97+
with smtplib.SMTP(host, port) as server:
98+
server.starttls()
99+
server.login(username, password) # MAIL_USERNAME, MAIL_PASSWORD from env
100+
server.send_message(msg)
101+
```
102+
103+
**Env vars**:
104+
```
105+
MAIL_HOST=smtp.gmail.com
106+
MAIL_PORT=587
107+
MAIL_USERNAME=your-gmail@gmail.com
108+
MAIL_PASSWORD=xxxx-xxxx-xxxx-xxxx # App Password
109+
MAIL_FROM=your-gmail@gmail.com
110+
```
111+
112+
---
113+
114+
## Option 3: Azure Communication Services (ACS) Email
115+
116+
**What**: Azure-native email service. Supports SMTP relay or REST API.
117+
118+
| | |
119+
|---|---|
120+
| **Free tier** | 100 emails/day on ACS free tier |
121+
| **Setup effort** | ~45 min |
122+
| **Domain verification** | Required (Azure-managed subdomain available as shortcut) |
123+
| **SDK** | `pip install azure-communication-email` or use SMTP relay |
124+
| **Human in the loop** | Portal provisioning of ACS resource + email domain |
125+
126+
**Pros**
127+
- Native to Azure — same billing, same portal, same IAM
128+
- Azure-managed subdomain option (`xxxxxxxx.azurecomm.net`) avoids custom DNS
129+
- SMTP relay option means zero code change (just set env vars)
130+
- Enterprise-grade deliverability and compliance
131+
- Managed Identity auth possible (no API keys)
132+
133+
**Cons**
134+
- More complex initial setup (ACS resource → Communication Service → Email → Domain → Sender)
135+
- Azure portal steps cannot be fully scripted via Bicep (email domain provisioning is partially manual)
136+
- Verbose SDK compared to Resend
137+
- Overkill for staging-only use
138+
139+
**Can be fully automated (no human)?** No. The ACS resource can be created via Bicep, but email domain provisioning and sender address setup require portal interaction. The Azure-managed subdomain shortcut reduces DNS work but still needs portal clicks.
140+
141+
**SMTP relay approach** (zero code change):
142+
```
143+
MAIL_HOST=smtp.azurecomm.net
144+
MAIL_PORT=587
145+
MAIL_USERNAME=<ACS-resource-name>.<entra-app-id>.<tenant-id>
146+
MAIL_PASSWORD=<entra-client-secret>
147+
MAIL_FROM=DoNotReply@xxxxxxxx.azurecomm.net
148+
```
149+
150+
---
151+
152+
## Option 4: Mailpit Container (catch-all, no real delivery)
153+
154+
**What**: Deploy Mailpit as a sidecar container in the staging Container App environment. Catches all emails in a web UI without delivering them.
155+
156+
| | |
157+
|---|---|
158+
| **Free tier** | Free (open-source container) |
159+
| **Setup effort** | ~30 min |
160+
| **Domain verification** | None |
161+
| **SDK** | None — existing SMTP code works as-is |
162+
| **Human in the loop** | None |
163+
164+
**Pros**
165+
- **Fully automatable** — no accounts, no DNS, no secrets
166+
- Zero code changes to email service (just point MAIL_HOST to sidecar)
167+
- Web UI to inspect all caught emails (HTML rendering, headers, attachments)
168+
- Perfect for E2E testing — verify email content without real delivery
169+
- No risk of accidentally emailing real users
170+
171+
**Cons**
172+
- Emails are not actually delivered — cannot test real inbox delivery
173+
- Requires an additional container (small resource cost)
174+
- Mailpit web UI needs to be exposed (ingress config) or accessed via port-forward
175+
- Not useful if the goal is to test deliverability to real inboxes
176+
177+
**Can be fully automated (no human)?** **Yes.** Add a Mailpit container to the Bicep template, set `MAIL_HOST` to its internal hostname, and deploy. No accounts, no DNS, no manual steps.
178+
179+
**Bicep addition**:
180+
```bicep
181+
resource mailpit 'Microsoft.App/containerApps@2023-05-01' = {
182+
name: 'lendq-mailpit-${environmentName}'
183+
properties: {
184+
configuration: {
185+
ingress: { external: true, targetPort: 8025 } // Web UI
186+
}
187+
template: {
188+
containers: [{
189+
name: 'mailpit'
190+
image: 'axllent/mailpit:latest'
191+
resources: { cpu: json('0.25'), memory: '0.5Gi' }
192+
}]
193+
}
194+
}
195+
}
196+
```
197+
198+
**Env var**: `MAIL_HOST=lendq-mailpit-staging` (internal DNS within Container App Environment)
199+
200+
---
201+
202+
## Option 5: Mailtrap
203+
204+
**What**: Email testing platform with a fake SMTP inbox. Catches emails in a shared inbox with a web UI. Also offers a sending API for real delivery.
205+
206+
| | |
207+
|---|---|
208+
| **Free tier** | 100 test emails/month (inbox), 1,000 emails/month (sending API) |
209+
| **Setup effort** | ~15 min |
210+
| **Domain verification** | None for test inbox; required for sending API |
211+
| **SDK** | None — standard SMTP credentials |
212+
| **Human in the loop** | Account signup only |
213+
214+
**Pros**
215+
- Purpose-built for staging/testing
216+
- SMTP inbox catches emails — great for E2E testing email content
217+
- Also has a real sending mode if needed later
218+
- Team inbox sharing (multiple people can view caught emails)
219+
- Spam score analysis on caught emails
220+
221+
**Cons**
222+
- Free tier is limited (100 test emails/month)
223+
- Third-party account required
224+
- Not real delivery in inbox mode (same limitation as Mailpit)
225+
- Less generous than Mailpit for high-volume test runs
226+
227+
**Can be fully automated (no human)?** No. Account signup is manual. After that, code + deploy is automatable.
228+
229+
**Env vars** (from Mailtrap inbox credentials):
230+
```
231+
MAIL_HOST=sandbox.smtp.mailtrap.io
232+
MAIL_PORT=587
233+
MAIL_USERNAME=<from-mailtrap-dashboard>
234+
MAIL_PASSWORD=<from-mailtrap-dashboard>
235+
```
236+
237+
---
238+
239+
## Comparison Matrix
240+
241+
| Criteria | Resend | Gmail SMTP | Azure ACS | Mailpit | Mailtrap |
242+
|----------|--------|------------|-----------|---------|----------|
243+
| **Real delivery** | Yes | Yes | Yes | No | No (inbox) / Yes (sending) |
244+
| **Free tier** | 100/day | 500/day | 100/day | Unlimited | 100/month |
245+
| **Code changes** | Replace SMTP with SDK | Add 2 lines | None (SMTP relay) or SDK | None | Add 2 lines |
246+
| **DNS/domain work** | Yes (or use test sender) | No | Partial (managed subdomain) | No | No |
247+
| **No human needed** | No | No | No | **Yes** | No |
248+
| **Azure-native** | No | No | **Yes** | Partial (container) | No |
249+
| **Best for** | Real delivery + DX | Quick & dirty | Production path | E2E test verification | Team email testing |
250+
| **Setup time** | 30 min | 15 min | 45 min | 30 min | 15 min |
251+
252+
---
253+
254+
## Recommendation
255+
256+
| Goal | Recommended option |
257+
|------|--------------------|
258+
| **Verify email content in E2E tests (no human)** | **Mailpit** — fully automatable, zero accounts |
259+
| **Quick test with real delivery** | **Gmail SMTP** — fastest setup, needs 1 App Password |
260+
| **Production-ready path** | **Azure ACS** — stays in ecosystem, scales to production |
261+
| **Best developer experience** | **Resend** — cleanest API, best docs, easy to swap later |

0 commit comments

Comments
 (0)