3.1 KiB
3.1 KiB
Postal
Ollama-personalized email campaign sender for drjones' homelab. Windows-native (VPS), SQLite-backed, LLM personalization via gemma4 on nightmare over Tailscale, delivery via SMTP (Proton) or Amazon SES.
Architecture
[VPS 64.224.17.129: C:\Mailer] [nightmare 100.103.34.0]
Flask GUI :8899 (localhost) <--TS--> ollama-shim :11440 (non-chunked proxy)
engine.py (send loop, SQLite) └─> Ollama :11434 mgraffam/gemma4-heretic:12b
delivery: SMTP (Proton) or SES (boto3)
- ollama-shim exists because Ollama's chunked streaming stalls over this tailnet path;
the shim returns one plain body. systemd unit
ollama-shim.serviceon nightmare. - All state in
mailer.db(subscribers, sends, campaigns, ab_variants, suppression).
Features
- CSV import:
email,firstname,interestper line - Per-subscriber intro written by gemma4 (thinking-model aware: extracts quoted greeting from CoT)
- A/B subject variants — gemma4 rewrites the subject 2×; rotated evenly; per-variant sent counts
- Send-hours window (
send_hours: 9-18server-local) — sleeps outside the window - Segment targeting (
segment_interestLIKE match) - Suppression list (imports + public
/unsub/<email>endpoint) - Rate limiting (emails/min), batch pauses, STOP switch
- Crash resume: queued sends persist in SQLite; auto-resume on boot/startup
- Watchdog task (
PostalWatchdog, every 5 min) restarts the app if :8899 stops answering - Test-send button (personalizes + sends one real email)
- Tunables UI with hover explanations: model, rate, batch, hours, send mode, SMTP creds, prompt
Setup (Windows VPS)
cd C:\Mailer
python -m venv --system-site-packages venv
venv\Scripts\pip install flask requests boto3
schtasks /create /tn MailerApp /tr "C:\Mailer\start.bat" /sc onstart /ru SYSTEM /rl HIGHEST /f
schtasks /create /tn PostalWatchdog /tr "C:\Mailer\watchdog.bat" /sc minute /mo 5 /ru SYSTEM /rl HIGHEST /f
Config (Tunables in the UI)
| key | default | note |
|---|---|---|
| ollama_url | http://100.103.34.0:11440 | nightmare via shim |
| ollama_model | mgraffam/gemma4-heretic:12b | thinking model, handled |
| send_hours | 9-18 | server-local window |
| rate_per_minute | 60 | keep ≤60 while warming reputation |
| send_mode | smtp | smtp (Proton, ~2000/day paid) or ses (needs AWS IAM key + verified domain) |
| smtp_host/user/pass | proton | pass = Proton SMTP token (Settings → SMTP tokens), paid plan required |
Scaling to 100K+
- Use SES (~$1/10K emails), verify the sending domain (SPF/DKIM via Cloudflare), start in sandbox, request production access, then warm up: 50/day → double daily → cap ~10K/day before the big push.
- Bounce/complaint webhook handling is the next build item; until then monitor SES dashboard daily.
Files
| file | purpose |
|---|---|
| app.py | Flask GUI + API |
| engine.py | send loop, Ollama client, Sender (smtp/ses), SQLite schema |
| start.bat | launcher (opens browser, runs app) |
| watchdog.bat | health-check + auto-restart |
| ollama_shim.py | deploys to nightmare — non-chunked Ollama proxy on tailnet |