[object Object]

← back to Rentv Sheet Enrich

Finish send mechanics: real Purelymail SMTP send (smtplib SSL) with idempotent sent-log, warmup cap, env-only password; all guards verified (dry-run default, placeholder-abort, from/pass-required). Sends nothing without explicit --send+message+creds

704267696228fc4a74b6afa6f7d882db912b9bc0 · 2026-08-14 15:19:24 -0700 · Steve Abrams

Files touched

Diff

commit 704267696228fc4a74b6afa6f7d882db912b9bc0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 14 15:19:24 2026 -0700

    Finish send mechanics: real Purelymail SMTP send (smtplib SSL) with idempotent sent-log, warmup cap, env-only password; all guards verified (dry-run default, placeholder-abort, from/pass-required). Sends nothing without explicit --send+message+creds
---
 .../rentv_send_purelymail.cpython-314.pyc          | Bin 0 -> 10174 bytes
 outreach/rentv_send_purelymail.py                  | 139 +++++++++++++--------
 2 files changed, 89 insertions(+), 50 deletions(-)

diff --git a/outreach/__pycache__/rentv_send_purelymail.cpython-314.pyc b/outreach/__pycache__/rentv_send_purelymail.cpython-314.pyc
new file mode 100644
index 0000000..113521f
Binary files /dev/null and b/outreach/__pycache__/rentv_send_purelymail.cpython-314.pyc differ
diff --git a/outreach/rentv_send_purelymail.py b/outreach/rentv_send_purelymail.py
index 638985c..4da884c 100644
--- a/outreach/rentv_send_purelymail.py
+++ b/outreach/rentv_send_purelymail.py
@@ -1,76 +1,115 @@
 #!/usr/bin/env python3
 """
 rentv_send_purelymail.py — STAGED $0 send path for the RENTV commercial-RE audience
-via Purelymail SMTP. DRY-RUN BY DEFAULT. Sending is a hard-gated bulk action — this
-script NEVER sends unless run with --send AND --i-am-steve AND a from-address is set.
+via Purelymail SMTP. DRY-RUN BY DEFAULT. Sending is a hard-gated bulk action.
 
-Guardrails baked in (Steve's standing rules + the deliverability panel):
+Guardrails (Steve's standing rules + the deliverability panel):
   * DRY-RUN default: prints exactly what it WOULD send, sends nothing.
-  * WARMUP-BATCHED: --per-day caps the daily volume (default 40) — you do NOT blast
-    2,636 cold emails at once from a low-volume domain; that gets the domain spam-flagged
-    before the good leads ever land. Ramp over ~2 months.
-  * VERIFIED-ONLY: refuses to load the inferred/guessed list — only the verified audience.
-  * CAN-SPAM footer is MANDATORY (physical address + one-click opt-out) or it aborts.
-  * FROM address must be an explicit real RENTV/Steven-Bloom mailbox you control — no default.
+  * --send requires --i-am-steve too (a human-intent handshake, not an accident).
+  * VERIFIED-ONLY: loads rentv_outreach_READY_verified.csv only (never the guessed list).
+  * WARMUP-BATCHED: --per-day caps daily volume (default 40); a low-volume domain that
+    blasts thousands cold gets spam-flagged before the good leads land.
+  * IDEMPOTENT: every send is appended to data/sent.log; re-runs SKIP already-sent
+    recipients and count today's sends against the cap — safe to re-run, never doubles.
+  * CAN-SPAM footer (physical address + opt-out) is MANDATORY or it aborts.
+  * MESSAGE must not be the DRAFT placeholder; FROM must be an explicit real mailbox.
+  * SMTP password comes from an env var (never a CLI arg / never printed).
 
 Usage:
-  python3 rentv_send_purelymail.py                      # dry-run, shows plan + sample
-  python3 rentv_send_purelymail.py --send --i-am-steve --from steven@rentv... --per-day 40
+  python3 rentv_send_purelymail.py                                   # dry-run plan + sample
+  SMTP_PASS_ENV=PURELYMAIL_..._PASSWORD \\
+  python3 rentv_send_purelymail.py --send --i-am-steve \\
+      --from steven@rentv.example --per-day 40                       # real, capped, gated
 """
-import csv, sys, os
+import csv, sys, os, ssl, smtplib, datetime
+from email.message import EmailMessage
 
-READY = os.path.join(os.path.dirname(__file__), "..", "rentv_outreach_READY_verified.csv")
-FROM = None
-for i,a in enumerate(sys.argv):
-    if a == "--from" and i+1 < len(sys.argv): FROM = sys.argv[i+1]
-SEND = "--send" in sys.argv and "--i-am-steve" in sys.argv
-PER_DAY = 40
-for i,a in enumerate(sys.argv):
-    if a == "--per-day" and i+1 < len(sys.argv): PER_DAY = int(sys.argv[i+1])
+HERE = os.path.dirname(os.path.abspath(__file__))
+READY = os.path.join(HERE, "..", "rentv_outreach_READY_verified.csv")
+SENTLOG = os.path.join(HERE, "data", "sent.log")
+SMTP_HOST, SMTP_PORT = "smtp.purelymail.com", 465
+
+def arg(name, default=None):
+    if name in sys.argv:
+        i = sys.argv.index(name)
+        return sys.argv[i+1] if i+1 < len(sys.argv) else True
+    return default
+
+FROM = arg("--from")
+SEND = ("--send" in sys.argv) and ("--i-am-steve" in sys.argv)
+PER_DAY = int(arg("--per-day", 40))
+PASS_ENV = os.environ.get("SMTP_PASS_ENV")  # name of the env var holding the mailbox password
 
-# ---- MANDATORY CAN-SPAM footer (physical address + opt-out) ----
 FOOTER = ("\n\n—\nSteven Bloom · Founder & CEO, RENTV\n"
           "5757 W. Century Blvd, Suite 700, Los Angeles, CA 90045 · (310) 213-6409\n"
           "You're receiving this as a commercial-real-estate professional in the RENTV network. "
           "If you'd prefer I not email you, just reply STOP and I won't follow up.")
-
-# ---- DRAFT template (SUBJECT + BODY) — Steve must review/replace the message ----
 SUBJECT = "[DRAFT — replace me] Quick note from RENTV"
-BODY = ("Hi {first},\n\n[DRAFT PLACEHOLDER — this is not the real message. Steve must define the "
-        "actual purpose: RENTV platform intro / event invite / sublease-desk / newsletter — then "
-        "personalize per-recipient. Do not send this placeholder.]\n\nBest,\nSteven")
+BODY = ("Hi {first},\n\n[DRAFT PLACEHOLDER — not the real message. Define the purpose "
+        "(RENTV platform intro / event invite / sublease-desk / newsletter), then personalize.]\n\nBest,\nSteven")
 
 def load_verified():
     with open(READY) as f:
-        rows = list(csv.DictReader(f))
-    # hard guard: every recipient must have an email; this file is verified-only by construction
-    return [r for r in rows if r.get("Email")]
+        return [r for r in csv.DictReader(f) if r.get("Email")]
+
+def already_sent():
+    s = set()
+    if os.path.exists(SENTLOG):
+        for line in open(SENTLOG):
+            parts = line.strip().split("\t")
+            if len(parts) >= 2: s.add(parts[1].lower())
+    return s
+
+def sent_today():
+    if not os.path.exists(SENTLOG): return 0
+    today = datetime.date.today().isoformat()
+    return sum(1 for line in open(SENTLOG) if line.startswith(today))
+
+def render(r):
+    first = (r["Contact Name"].split() or ["there"])[0]
+    return SUBJECT, BODY.format(first=first) + FOOTER
 
 def main():
     rows = load_verified()
-    print(f"Audience: {len(rows)} VERIFIED commercial-RE leads (from rentv_outreach_READY_verified.csv)")
-    print(f"Warmup cap: {PER_DAY}/day  ->  ~{-(-len(rows)//PER_DAY)} days to send the full list")
-    print(f"Footer (CAN-SPAM): present ✔  |  From: {FROM or 'NOT SET (required)'}")
-    print(f"Subject: {SUBJECT}")
-    if "[DRAFT" in SUBJECT or "[DRAFT" in BODY:
-        print("\n⚠ MESSAGE IS A PLACEHOLDER — Steve must define/approve the real subject+body before any send.")
-    # show a sample rendered email
-    r = rows[0]; first = (r['Contact Name'].split() or ['there'])[0]
-    print("\n--- SAMPLE (rendered, top-warmth recipient) ---")
-    print(f"To: {r['Email']}  ({r['Contact Name']} · {r['Company']})")
-    print(f"Subject: {SUBJECT}")
-    print(BODY.format(first=first) + FOOTER)
-    print("--- END SAMPLE ---")
+    done = already_sent()
+    todo = [r for r in rows if r["Email"].lower() not in done]
+    placeholder = ("[DRAFT" in SUBJECT) or ("[DRAFT" in BODY)
+    print(f"Audience: {len(rows)} verified CRE | already sent: {len(done)} | remaining: {len(todo)}")
+    print(f"Warmup cap: {PER_DAY}/day | sent today: {sent_today()} | From: {FROM or 'NOT SET'} | SMTP pass env: {PASS_ENV or 'NOT SET'}")
+    if placeholder:
+        print("⚠ MESSAGE IS A PLACEHOLDER — replace SUBJECT/BODY before any real send.")
+    if todo:
+        s, b = render(todo[0]); r = todo[0]
+        print(f"\n--- SAMPLE ---\nTo: {r['Email']} ({r['Contact Name']} · {r['Company']})\nSubject: {s}\n{b}\n--- END ---")
+
     if not SEND:
-        print("\nDRY-RUN — nothing sent. To actually send you must: (1) Steve replaces the DRAFT message,")
-        print("(2) set --from to a real RENTV mailbox you control, (3) re-run with --send --i-am-steve,")
-        print("(4) it will send AT MOST --per-day today. Sending is Steve's gated call.")
+        print("\nDRY-RUN — nothing sent. Real send needs: replace the DRAFT message, --from a real mailbox,")
+        print("SMTP_PASS_ENV=<the mailbox's Purelymail password env var>, then --send --i-am-steve.")
         return
-    if "[DRAFT" in SUBJECT or "[DRAFT" in BODY:
-        print("\nABORT: refusing to send a placeholder message."); return
-    if not FROM:
-        print("\nABORT: --from not set."); return
-    print("\n[send path would run here — intentionally not implemented in this staged scaffold]")
+    # --- real send path (all guards must pass) ---
+    if placeholder: print("\nABORT: refusing to send the placeholder message."); return
+    if not FROM:    print("\nABORT: --from not set."); return
+    if not PASS_ENV or not os.environ.get(PASS_ENV):
+        print("\nABORT: SMTP_PASS_ENV not set or that env var is empty (need the mailbox's Purelymail password)."); return
+    password = os.environ[PASS_ENV]
+    remaining_today = max(0, PER_DAY - sent_today())
+    batch = todo[:remaining_today]
+    if not batch:
+        print(f"\nDaily cap reached ({sent_today()}/{PER_DAY}). Re-run tomorrow to continue."); return
+    print(f"\nSending {len(batch)} now (cap {PER_DAY}/day, {len(todo)-len(batch)} deferred)...")
+    os.makedirs(os.path.dirname(SENTLOG), exist_ok=True)
+    ctx = ssl.create_default_context()
+    with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ctx) as smtp:
+        smtp.login(FROM, password)
+        with open(SENTLOG, "a") as log:
+            for r in batch:
+                s, b = render(r)
+                msg = EmailMessage(); msg["From"] = FROM; msg["To"] = r["Email"]
+                msg["Subject"] = s; msg.set_content(b)
+                smtp.send_message(msg)
+                log.write(f"{datetime.datetime.now().isoformat()}\t{r['Email']}\t{r['Company']}\n"); log.flush()
+                print(f"  sent -> {r['Email']}")
+    print(f"Done. {len(batch)} sent, logged to {SENTLOG}.")
 
 if __name__ == "__main__":
     main()

← 0f44288 Stage $0 Purelymail send path for 2636 verified CRE audience  ·  back to Rentv Sheet Enrich  ·  Draft real RENTV outreach messages: 3 grounded variants (net e1e9fbe →