83 lines
3.9 KiB
Markdown
83 lines
3.9 KiB
Markdown
# 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.service` on nightmare.
|
||
- All state in `mailer.db` (subscribers, sends, campaigns, ab_variants, suppression).
|
||
|
||
## Features
|
||
|
||
- CSV import: `email,firstname,interest` per 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-18` server-local) — sleeps outside the window
|
||
- Segment targeting (`segment_interest` LIKE 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 |
|
||
|
||
|
||
## Obfuscation layer (v1.2)
|
||
|
||
- **ollama-shim v2** (nightmare): binds tailnet-only `100.103.34.0:28443` — nonstandard port,
|
||
token-gated (`X-Auth-Token`, constant-time compare), paths `/tags` + `/gen` (nothing fingerprints
|
||
as Ollama), replies carry a fake `nginx` server header, generic 404 on everything else.
|
||
- **Postal remote dashboard**: second Flask listener on the VPS tailnet IP only
|
||
(`100.120.108.13:47077`), token-gated via `?t=<operator_token>` or `X-Auth-Token` header —
|
||
open Postal from any tailnet device without RDP. Localhost :8899 stays token-free for RDP use.
|
||
- Secrets live in `C:\Mailer\shared_secret.txt` (shim) and `C:\Mailer\operator_token.txt`
|
||
(dashboard gate); shim copy at `/opt/ollama-shim/shared_secret.txt` on nightmare.
|
||
- Wire-level encryption is WireGuard (Tailscale) end-to-end; this layer hides *what* the services
|
||
are and who may talk to them.
|