#!/usr/bin/env python3 """Mailer GUI — Flask web app, localhost:8899 on the VPS (RDP into it and open the browser).""" import os, sys, csv, io, json sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from flask import Flask, request, jsonify, render_template_string, redirect, url_for from engine import (db, init_db, load_config, save_config, get_engine, render_subject) APP_DIR = os.path.dirname(os.path.abspath(__file__)) app = Flask(__name__) init_db() PAGE = r"""Mailer

Postal

Ollama-personalized campaign mailer — subscribers only, SES delivery

Engine

Loading…

Subscribers

Campaign

Tunables

?
?
?
?
?
?
?
?

Suppression list

Subject A/B (current campaign)

no data yet

Recent sends

emailstatussubjecttime
""" @app.route("/") def home(): return render_template_string(PAGE) @app.route("/api/status") def api_status(): eng = get_engine() st = eng.status() st["stopNote"] = load_config().get("stopped", False) return jsonify(st) @app.route("/api/recent") def api_recent(): c = db() rows = c.execute("SELECT email,status,subject,sent_at FROM sends ORDER BY id DESC LIMIT 12").fetchall() c.close() return jsonify([dict(r) for r in rows]) @app.route("/api/config", methods=["GET", "POST"]) def api_config(): if request.method == "POST": cfg = load_config() incoming = request.json or {} for k, v in incoming.items(): if k in cfg: cfg[k] = v # engine stop flag cfg["stopped"] = bool(incoming.get("_stop", cfg.get("stopped"))) save_config(cfg) return jsonify(ok=True) return jsonify(load_config()) @app.route("/api/import", methods=["POST"]) def api_import(): text = (request.json or {}).get("text", "") c = db() added = dupes = 0 for raw in io.StringIO(text).readlines(): raw = raw.strip() if not raw or raw.startswith("#"): continue parts = [p.strip() for p in raw.replace(";", ",").split(",")] email = parts[0].lower() if "@" not in email: continue fn = parts[1] if len(parts) > 1 else "" ln = parts[2] if len(parts) > 2 else "" interest = parts[3] if len(parts) > 3 else (parts[1] if len(parts) > 1 and "@" not in parts[1] and len(parts) <= 2 else "") try: c.execute("INSERT INTO subscribers (email, first_name, last_name, interest) VALUES (?,?,?,?)", (email, fn, ln, interest)) added += 1 except Exception: dupes += 1 c.commit(); c.close() return jsonify(added=added, dupes=dupes) @app.route("/api/subcount") def api_subcount(): c = db() n = c.execute("SELECT COUNT(*) n FROM subscribers WHERE status='active'").fetchone()["n"] c.close() return jsonify(active=n) @app.route("/api/campaign", methods=["POST"]) def api_campaign(): d = request.json or {} c = db() cur = c.execute("INSERT INTO campaigns (name, subject, body, status, segment_interest) VALUES (?,?,?, 'ready', ?)", (d.get("subject", "Campaign")[:40], d.get("subject", ""), d.get("body", ""), (d.get("segment", "") or "").strip())) cid = cur.lastrowid c.commit(); c.close() return jsonify(id=cid) @app.route("/api/start", methods=["POST"]) def api_start(): cfg = load_config() cfg["stopped"] = False save_config(cfg) c = db() row = c.execute("SELECT id FROM campaigns ORDER BY id DESC LIMIT 1").fetchone() c.close() if not row: return jsonify(ok=False, error="save a campaign first") return jsonify(get_engine().run_campaign(row["id"])) @app.route("/api/suppress", methods=["POST"]) def api_suppress(): text = (request.json or {}).get("text", "") c = db() n = 0 for raw in io.StringIO(text).readlines(): e = raw.strip().lower() if "@" in e: c.execute("INSERT OR IGNORE INTO suppression (email, reason) VALUES (?, 'import')", (e,)) n += 1 c.commit() total = c.execute("SELECT COUNT(*) n FROM suppression").fetchone()["n"] c.close() return jsonify(added=n, total=total) @app.route("/api/abstats") def api_abstats(): c = db() rows = c.execute("SELECT subject, sent, opened FROM ab_variants WHERE campaign_id=(SELECT MAX(id) FROM campaigns)").fetchall() c.close() return jsonify([dict(r) for r in rows]) @app.route("/unsub/") def unsub(email): e = email.strip().lower() if "@" not in e: return "bad link", 400 c = db() c.execute("INSERT OR IGNORE INTO suppression (email, reason) VALUES (?, 'unsubscribe')", (e,)) c.execute("UPDATE subscribers SET status='unsubscribed' WHERE email=?", (e,)) c.commit() c.close() return render_template_string("

✓

You're unsubscribed. No hard feelings.

") @app.route("/api/testsend", methods=["POST"]) def api_testsend(): d = request.json or {} to_email = (d.get("email") or "").strip().lower() if "@" not in to_email: return jsonify(ok=False, error="bad email") from engine import OllamaClient, render_template, render_subject, personalize_intro cfg = load_config() sub = {"first_name": d.get("first_name", "Jane"), "last_name": "", "email": to_email, "interest": d.get("interest", "our updates")} intro = "" if cfg.get("personalize"): oc = OllamaClient(cfg["ollama_url"], cfg["ollama_model"]) intro = personalize_intro(oc, sub, cfg) c = db() camp = c.execute("SELECT * FROM campaigns ORDER BY id DESC LIMIT 1").fetchone() c.close() subject = render_subject(camp["subject"] if camp else "Hello {{first_name}}", sub) body = render_template(camp["body"] if camp else "

{{intro}}

", sub, cfg, intro) try: from engine import Sender mid = Sender(cfg).send(to_email, subject, body) return jsonify(ok=True, id=mid, intro=intro[:200]) except Exception as e: return jsonify(ok=False, error=str(e)[:300]) @app.route("/api/stop", methods=["POST"]) def api_stop(): eng = get_engine() eng.state["running"] = False cfg = load_config(); cfg["stopped"] = True; save_config(cfg) return jsonify(ok=True) if __name__ == "__main__": app.run(host="127.0.0.1", port=8899, threaded=True)