Matt Novak logo Matt Novak

Blog

Warmcraft: a Pragmatic, Closed-Box Email Warmup Engine (Tech Notes)

2025-08-30 • 5 min

Warmcraft: a Pragmatic, Closed-Box Email Warmup Engine (Tech Notes)🔗

Warmcraft is my Go implementation of a closed-box warmup system: internal MX senders email a fleet of Google Workspace inboxes; the Gmail side performs realistic interactions (move from spam → inbox, star, mark important, reply, and thread correctly), and every action is quota-aware, logged, and spread out across the day. This post walks through the design from the wire up.


1) The shape of the data🔗

Everything starts with a tiny SQLite schema that keeps just what we need—no more, no less:

  • GmailAccount: address, password, and the WarmupStart timestamp that drives each account's daily limit curve.
  • MxAccount: credentials for both IMAP and SMTP (MX Suite has separate creds), including the rule that port 2525 uses plain auth while other ports are TLS.
  • Creative: subject + text/HTML bodies with spintax to keep content varied.
  • InteractionLog: append-only log of sends, moves, stars, replies, etc., with timestamps and message_id for threading analytics.

This keeps the runtime simple and lets me compute daily limits and summaries with straight SQL.


2) The control loop🔗

The program boots, starts a scheduler, and then begins long-running inbox monitoring. It's intentionally boring: one process, goroutines per account, and blocking loops so crashes are obvious.

  • StartWarmupScheduler(db) periodically fans out work to Gmail and MX workers.
  • MonitorGmailInboxes(db) keeps Gmail connections active and reacts to new messages.

3) Quota logic: linear ramp, per-account🔗

Each Gmail account ramps from 5/day up to 500/day over ~30 days. The only limiter is the Gmail side—MX senders never throttle directly; they look at aggregate available Gmail capacity and only send what can be handled. The core calculation is tiny and deterministic (based on WarmupStart):

  • On every pass, the engine computes (limit, used) for the account, skipping work when used >= limit.
  • "Used" is just COUNT(*) of that Gmail's interactions for today in interaction_logs. Simple and robust.

This design makes it trivial to reason about behavior and report "day N / limit L" in daily summaries.


4) MX → Gmail sending path🔗

MX accounts act as the "speakers" that reach out to Gmail. For each Gmail that still has quota:

  1. Connect SMTP to the MX host—TLS unless port 2525, which is plain TCP with PLAIN auth.
  2. Pick a random creative, apply spintax, and assemble a multipart/alternative MIME.
  3. Generate a unique Message-ID bound to the MX sender's domain (useful for threading later).
  4. Send and append a send row into interaction_logs.

That whole path is a single function—easy to probe, easy to retry.


5) Gmail side: receive, rescue, star, reply, thread🔗

Gmail workers maintain IMAP sessions to imap.gmail.com:993 (TLS), scan for new UNSEEN, and act:

  • Move out of Spam (when applicable) to INBOX to simulate real remediation.
  • Star and mark important using Gmail's \Important vendor flag for realistic signals.
  • Reply via Gmail SMTP with a thread-correct MIME message: In-Reply-To + References carry the original Message-ID and a new one is generated for the reply. This keeps threads perfectly intact.

Message parsing uses go-imap + go-message so headers like Subject and Message-ID are extracted correctly before responding.


6) MIME building & spintax🔗

I keep MIME construction explicit so it's obvious what goes across the wire:

  • Replies are multipart/alternative with text and HTML parts; boundaries and dates are set by hand; In-Reply-To/References are always present.
  • Spintax is expanded with a single-pass {a|b|c} resolver to keep subjects and bodies non-deterministic (but still human).

This avoids "clever" MIME abstractions and keeps deliverability experiments traceable.


7) Logging is the product🔗

Every action writes a row: who (gmail_id), optionally which MX (mx_id), when (timestamp), what (action, folder), and which message (message_id). With this:

  • Daily caps become a simple COUNT(*) WHERE DATE(timestamp)=DATE('now').
  • Threading/debugging is easy because message IDs are searchable.
  • Summaries (per account, per domain) are cheap to compute.

Because logs are append-only, archiving/purging >30d can be a nightly DELETE by date partition.


8) Fault tolerance & realism🔗

  • Connection model: one goroutine per account keeps sessions warm; reconnects are straightforward because state is mostly in SQLite, not memory.
  • Time distribution: the scheduler spaces work throughout the day; nothing "batch dumps" at midnight. (The ramp/limit logic enforces this indirectly; further Poisson spacing is trivial to add on top.)
  • Safety valves: if Gmail quota is exhausted for the day, MX senders gracefully skip that target; if no Gmail has capacity, MX logs a no-op and waits for the next cycle.

9) Why Go, why this stack🔗

  • go-imap / go-message are mature and predictable for IMAP/MIME edge cases.
  • Single binary + SQLite keeps deployment brain-dead simple while still giving me SQL power for limits and reports.
  • Explicit SMTP (MX + Gmail): I avoid heavy client frameworks so I have total control over STARTTLS/TLS/plain and header ordering.

10) Roadmap (near-term)🔗

  • Daily summaries (per Gmail: "Day N / Limit L / Used U / sends, moves, stars, replies"), posted to Telegram and email.
  • Per-domain health overlays once I wire in Postmaster data (badges + prioritization).
  • Split-hour scheduling to deliberately spread actions across friendly windows.
  • CSV/Sheets export for logs and aggregate warmup stats.

All of this falls naturally out of the current data model + logs.


Closing🔗

Warmcraft is deliberately narrow: do fewer things, but do them predictably and with wire-level control. The system is small enough to audit and reason about, but expressive enough to simulate the human touches (rescues, flags, replies, threads) that actually move the needle on warmup.

If you want to peek at a specific part of the code I referenced above, here are the anchors I think are most illustrative:

  • Data structures (Gmail/MX/Creative/Log).
  • MX SMTP connect (TLS vs 2525 plain) + Message-ID.
  • Warmup scheduler + per-day limit checks.
  • Gmail IMAP actions (star, important) + reply builder.
  • Spintax + calculateDailyLimit.
  • Logging helpers and counts.

AI-assisted writing

I draft and edit all articles myself, and I use AI as an assistant for outlining, phrasing, and cleanup. Curious how I use it—and where I draw the lines?