Files
postal-mailer/README.md
2026-10-02 02:21:52 -07:00

3.9 KiB
Raw Blame History

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.