#!/usr/bin/env python3 """ create_ct_annual_report_campaign.py — Connecticut SOS annual-report overdue outreach. Audience : the verified smtp_valid, NON-Google CT leads (annual report overdue), staged CSV from the registry pipeline + verify step. Hook : the business's CT annual report is overdue -> risk of administrative dissolution / loss of good standing. We file it + keep them compliant. Sender : Performance West (the fresh subdomain on the fresh .90 IP, so this warms a clean reputation). Product : CT annual report filing + registered agent + reinstatement (services Performance West already fulfills). Personalization (per-subscriber Listmonk attribs, loaded from the CSV): company, due_date, days_overdue, city DRAFT-only by default. Loads leads to a fresh list, builds a personalized HTML + plaintext campaign. Sending is a separate, throttled, warm-up step. python3 scripts/workers/create_ct_annual_report_campaign.py --csv /tmp/ct_send_nongoogle.csv --limit 100 python3 scripts/workers/create_ct_annual_report_campaign.py --preview-only """ import argparse import csv import os import sys import datetime as dt _REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if _REPO not in sys.path: sys.path.insert(0, _REPO) from scripts._email_plaintext import html_to_text # noqa: E402 SITE = "https://performancewest.net" FROM_EMAIL = "Performance West " REPLY_TO = "info@performancewest.net" EMAIL = "info@performancewest.net" PHONE_TEL = "+18884110383" PHONE_DISPLAY = "(888) 411-0383" FONT = "Inter,system-ui,Arial,sans-serif" # The soft CTA lands on the free compliance-scan tool (warm, no-pressure). UTM = "utm_source=filings&utm_medium=email&utm_campaign=ct-annual-report" def _track(url, content): sep = "&" if "?" in url else "?" return f"{url}{sep}{UTM}&utm_content={content}@TrackLink" # ── HTML helpers (clean, professional, low-spam) ───────────────────────────── def header(): return ( '' f'Performance West' '' ) def P(t): return f'

{t}

' def cta(text, url): return ( '' '
' f'{text}' '
' ) def status_box(): # Uses Listmonk template vars filled from attribs. return ( '' '
' f'

' 'Connecticut annual report — past due

' f'

' '{{ .Subscriber.Attribs.company }}
' 'Annual report was due {{ .Subscriber.Attribs.due_date }} ' '({{ .Subscriber.Attribs.days_overdue }} days ago).

' '
' ) def footer(): return ( '' f'

' f'Questions? Reply to this email, write {EMAIL} ' f'or call {PHONE_DISPLAY}.

' f'

Performance West Inc. · ' '525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 · ' f'performancewest.net

' '

' 'You received this because your business is listed as active in the Connecticut public ' 'business registry. Unsubscribe

' '' ) def build_body(): inner = ( P("Hi {{ .Subscriber.Attribs.company }},") + P("A quick heads-up from Performance West: according to the Connecticut Secretary of the " "State's public business registry, your company’s annual report is past due.") + status_box() + P("In Connecticut, an overdue annual report can put your business out of good standing " "and, if it stays unfiled, can eventually lead to administrative dissolution — " "which can affect your ability to get financing, sign contracts, or renew licenses.") + P("The good news: it’s a quick fix. We can file your Connecticut annual report for you, " "confirm your business is back in good standing, and set a reminder so it never lapses again. If you’d " "like, we can also handle your registered-agent service and any reinstatement paperwork.") + P("Want us to take care of it? Reply to this email, or look up {{ .Subscriber.Attribs.company }} " "in our free Connecticut business-standing checker to see your exact status and what it costs to fix:") + cta("Check my CT business standing →", _track(f"{SITE}/tools/corporation-check?state=CT", "corpcheck")) + P("If you’ve already filed, no action needed — and congratulations on staying current.") + P("Best,
Justin Hannah
Performance West Inc.
" f'Business Compliance Services') ) return ( '' '' '' '
' '' '
' '' + header() + '' + footer() + '
' ) # ── Listmonk API (reuse the shared helper's connection) ────────────────────── def load_leads_and_create(csv_path, limit, do_send_test): import requests from scripts.workers.campaign_helpers import LISTMONK_URL, AUTH s = requests.Session(); s.auth = AUTH # 1. read + cap the CSV rows = [] with open(csv_path) as f: for r in csv.DictReader(f): if (r.get("verify_reason") or "") != "smtp_valid": continue if "google" in (r.get("mx_provider") or ""): continue rows.append(r) if limit: rows = rows[:limit] print(f"leads to load: {len(rows)}") # 2. create the fresh list lname = f"CT Annual Report Overdue — filings warmup {dt.date.today().isoformat()}" lid = s.post(f"{LISTMONK_URL}/api/lists", json={ "name": lname, "type": "private", "optin": "single", "tags": ["ct", "annual-report", "filings"], }, timeout=30).json()["data"]["id"] print(f"list id: {lid}") # 3. load subscribers with attribs ok = 0 for r in rows: company = (r.get("name") or "").strip().title() attribs = { "company": company or "your business", "due_date": r.get("due_date", ""), "days_overdue": r.get("days_overdue", ""), "city": (r.get("city") or "").title(), "entity_id": r.get("entity_id", ""), } try: s.post(f"{LISTMONK_URL}/api/subscribers", json={ "email": r["email"].strip().lower(), "name": company or r["email"], "status": "enabled", "lists": [lid], "attribs": attribs, "preconfirm_subscriptions": True, }, timeout=30) ok += 1 except Exception as e: print(f" skip {r['email']}: {e}", file=sys.stderr) print(f"loaded {ok}/{len(rows)} subscribers") # 4. create the campaign (DRAFT) body = build_body() alt = html_to_text(body) cid = s.post(f"{LISTMONK_URL}/api/campaigns", json={ "name": f"CT Annual Report Overdue — {dt.date.today().isoformat()}", "subject": "Your Connecticut annual report is overdue — we can file it for you", "lists": [lid], "from_email": FROM_EMAIL, "type": "regular", "content_type": "html", "body": body, "altbody": alt, "headers": [{"Reply-To": REPLY_TO}], "messenger": "email", }, timeout=30).json()["data"]["id"] print(f"campaign id: {cid} (DRAFT)") if do_send_test: tp = {"subscribers": [do_send_test]} base = s.get(f"{LISTMONK_URL}/api/campaigns/{cid}", timeout=30).json()["data"] tp.update({"name": base["name"], "subject": base["subject"], "lists": [lid], "from_email": FROM_EMAIL, "type": "regular", "content_type": "html", "body": base["body"], "altbody": base.get("altbody"), "messenger": "email", "headers": base.get("headers")}) rt = s.post(f"{LISTMONK_URL}/api/campaigns/{cid}/test", json=tp, timeout=30) print(f"test send to {do_send_test}: {rt.status_code}") return cid, lid def main(): ap = argparse.ArgumentParser() ap.add_argument("--csv", help="verified CT leads CSV (smtp_valid, non-Google)") ap.add_argument("--limit", type=int, default=100, help="cap leads (warmup: start ~100)") ap.add_argument("--preview-only", action="store_true", help="render HTML to /tmp, no Listmonk") ap.add_argument("--send-test", help="fire a Listmonk test to this address after creating the draft") args = ap.parse_args() body = build_body() out = "/tmp/ct_annual_report_email.html" # fill sample attribs for a realistic preview sample = (body .replace("{{ .Subscriber.Attribs.company }}", "Shoreline Corporation") .replace("{{ .Subscriber.Attribs.due_date }}", "2026-06-29") .replace("{{ .Subscriber.Attribs.days_overdue }}", "2") .replace("{{ UnsubscribeURL }}", "https://lists.performancewest.net/unsub/preview")) with open(out, "w") as f: f.write(sample) print(f"preview -> {out} ({len(body):,} chars HTML / {len(html_to_text(body)):,} plaintext)") if args.preview_only: return 0 if not args.csv: print("no --csv; preview only. Re-run with --csv to load leads + create draft.", file=sys.stderr) return 0 load_leads_and_create(args.csv, args.limit, args.send_test) return 0 if __name__ == "__main__": raise SystemExit(main())