Skip to content

darkzOGx/darkzwarmer

Repository files navigation

darkzwarmer

Self-hosted email warmup network. Give it a set of domains; it generates realistic inboxes on each, sends natural-looking mail between them across domains, and auto-replies on human-like delays. Volume ramps over 28 days, then holds steady. The goal: build sender reputation on fresh domains and mailboxes before you send anything that matters.

No SaaS, no per-seat fees, no handing your reputation data to a third party. It's a small Python runtime backed by SQLite that you run on any always-on machine (or a cheap VPS / mini PC).

┌──────────────────────────────────────────────────────────────────┐
│                          darkzwarmer                               │
│                                                                    │
│   config.yaml ─┐                                                   │
│   .env ────────┤── main.py ── scheduler (APScheduler)              │
│                │      │                                            │
│        ┌───────┴──────┼─────────────────────┐                     │
│        ▼              ▼                       ▼                    │
│    sender.py      replier.py            resend_source.py           │
│  (outbound       (IMAP receive +       (Resend webhook queue       │
│   SMTP send)      auto-reply)           drain → same reply path)   │
│        │              │                       │                    │
│        └──────── database.py (SQLite: inboxes, logs, ramp state)   │
│                                                                    │
│   fleet.py  → inbox generation     content.py → message templates  │
│   schedule_table.py → the 28-day volume ramp                       │
└──────────────────────────────────────────────────────────────────┘

Responsible use. This tool warms domains and mailboxes you own and control. It only ever emails its own network — it does not send to third parties. Use it to build legitimate sender reputation for opt-in mail that complies with CAN-SPAM, GDPR, and your provider's terms. Don't use it to facilitate spam. You are responsible for how you use it.


How warmup works here

  • A fleet of inboxes. For each domain you list, the tool generates N inboxes named firstname.lastname@domain (e.g. emma.johnson@…).
  • Cross-domain traffic only. An inbox on domain-a only ever emails inboxes on other domains. Same-domain pairs are never used — that mirrors how real mail flows and avoids self-referential patterns.
  • Spread sends. Each day, every inbox's quota of sends is scheduled at randomized times across a daytime window (default 07:00–19:00 local), with a 2–8 minute jitter between an inbox's consecutive sends.
  • Human-delayed replies. When the network receives one of its own messages, it waits a random 30 min – 4 h, then replies in-thread (proper In-Reply-To / References headers). Only the network's own mail is answered; outsider mail is ignored.
  • The 28-day ramp. Per-inbox volume walks 5 → 35 messages/day over 28 days, then holds at the steady-state value. Each inbox tracks its own day counter, so inboxes added later start at their own Day 1.
  • Restart-safe. All state lives in warmup.db (SQLite). Crash or reboot mid-run and it picks up where it left off; in-flight replies are re-armed and a unique index prevents double-replies.

What you need

  1. One or more domains you own. Fresh domains are the whole point. Two domains is the practical minimum (cross-domain needs somewhere to send).
  2. An outbound SMTP provider to send through — Amazon SES, SMTP.com, Postmark, Mailgun, Brevo, SendGrid, etc. (provider-agnostic; see below).
  3. A way for each domain to receive mail, so the network can reply to itself. Two supported architectures — IMAP or Resend inbound — covered in detail below. You can mix both across domains.
  4. Python 3.11+ and an always-on machine to run it.

Quick start

# 1. Install dependencies
python -m pip install -r requirements.txt

# 2. Configure
cp .env.example .env                       # then edit: SMTP_USERNAME / SMTP_PASSWORD
#   edit config.yaml in place             # your domains + smtp host
cp catchall_credentials.example.yaml catchall_credentials.yaml   # IMAP domains only

# 3. One-time bootstrap — generates inboxes
python setup.py

# 4. Run the warmup (runs indefinitely)
python main.py

On Windows PowerShell, swap cp for Copy-Item and load .env however you prefer — main.py also auto-loads .env via python-dotenv.


Step 1 — Outbound SMTP (provider-agnostic)

darkzwarmer sends through whatever SMTP server you configure. Set the transport in config.yaml:

smtp:
  host: smtp.your-provider.example
  port: 587
  tls_mode: starttls        # starttls | ssl | plain

and the credentials in .env:

SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password

Common providers:

Provider Host (example) Port tls_mode
Amazon SES email-smtp.us-east-1.amazonaws.com 587 starttls
SMTP.com send.smtp.com 587 starttls
Postmark smtp.postmarkapp.com 587 starttls
Mailgun smtp.mailgun.org 587 starttls
Brevo smtp-relay.brevo.com 587 starttls

Authenticate your sending domains at the provider regardless of how you receive. Add the SPF and DKIM records the provider gives you for each domain (and a DMARC record), so your warmup mail passes alignment. Unsigned warmup traffic teaches the wrong reputation.

Note on Amazon SES: SMTP credentials are not your AWS API keys — create them under SES → SMTP settings → Create SMTP credentials. A new SES account is in sandbox mode (can only send to verified addresses) until you request production access. Optionally, add an aws: block to config.yaml and install boto3 to have setup.py print SES domain-verification status.


Step 2 — Choose a receiving architecture

The network must be able to receive its own mail to reply to it. Pick per domain:

Option A — Mailbox host + IMAP Option B — Resend inbound
You need A mailbox host with a catch-all A domain + a Resend account + a tiny serverless webhook
Good when You already host email, or want real mailboxes You don't want to pay for / manage mailboxes
How replies arrive Poll one catch-all mailbox over IMAP Resend webhook → queue → local drain
Config imap_host + imap_port per domain source: resend per domain
Extra setup Catch-all forwarding DNS + Resend webhook + Upstash + Vercel deploy

The examples below use Namecheap for the domains (placeholder domain names like example-a.com — substitute your own). Namecheap conveniently supports both options: it sells the domains, offers Private Email hosting for Option A, and lets you point DNS to Resend for Option B.


Option A — Mailbox host + IMAP

Use this when each domain has a real mailbox host. Works with Namecheap Private Email, Google Workspace, Zoho Mail, Fastmail, IONOS, Migadu — any host that exposes IMAP and supports a catch-all mailbox.

How it works

Each domain gets one catch-all mailbox (e.g. warmup@example-a.com) that receives all mail addressed to any address on that domain. darkzwarmer logs into that single mailbox per poll cycle and replies to the network's own traffic. You don't create a real mailbox per generated inbox — the catch-all collects everything.

Setup (Namecheap Private Email example)

  1. Buy the domain(s) at Namecheap.
  2. Add Private Email to each domain (Namecheap dashboard → the email product of your choice), or point the domain's MX records at whichever host you use. Apply the host's MX / SPF / DKIM records.
  3. Create one mailbox per domain named warmup@yourdomain.tld.
  4. Enable catch-all and route it to that mailbox:
    • Namecheap Private Email: Mailbox → Settings → Catch-all → forward to warmup@yourdomain.tld.
    • Google Workspace: Routing → Default routing → Add → catch-all → deliver to warmup@….
    • Zoho: Mail Admin → Email Routing / Catch-all.
  5. Find the IMAP host/port for your provider (e.g. Namecheap Private Email mail.privateemail.com:993, Zoho imap.zoho.com:993, Workspace imap.gmail.com:993).
  6. Enable IMAP access on the mailbox. Many providers (Google especially) require an app password rather than the account password.

Configure

In config.yaml, list each IMAP domain with its host/port:

domains:
  - name: example-a.com
    imap_host: mail.privateemail.com
    imap_port: 993
  - name: example-b.com
    imap_host: mail.privateemail.com
    imap_port: 993

In catchall_credentials.yaml (copied from the .example, gitignored), put the catch-all password for each domain:

example-a.com:
  imap_user: warmup@example-a.com     # optional; defaults to warmup@<domain>
  imap_password: "your-catchall-mailbox-password"
example-b.com:
  imap_password: "your-catchall-mailbox-password"

That's it — no entry is needed in .env for IMAP receiving.


Option B — Namecheap + Resend inbound

Use this when you don't want to run mailboxes. The domain's DNS lives at Namecheap; Resend receives inbound mail and pushes a webhook; a tiny Vercel function drops the event into an Upstash Redis queue; darkzwarmer drains that queue on its normal poll cycle and replies through the same logic as the IMAP path.

Inbound mail ─▶ Resend (email.received) ─▶ Vercel webhook (webhook/)
                                                 │  LPUSH email_id
                                                 ▼
                                          Upstash Redis queue
                                                 ▲
                                                 │  RPOP (poll cycle)
                                    darkzwarmer  ─┘ resend_source.py
                                          └▶ fetch email via Resend API ▶ reply

Everything in webhook/ is the deployable serverless function. It's already generic — no secrets in the repo; you supply them as environment variables.

Setup

  1. Buy the domain(s) at Namecheap and keep DNS on Namecheap (BasicDNS / their nameservers).

  2. Add the domain in Resend and follow Resend's verification. Resend shows you the exact records to add at Namecheap:

    • Sending: SPF (TXT) + DKIM (TXT/CNAME) records.
    • Receiving (inbound): an MX record pointing at Resend's inbound host (Resend gives you the precise value).

    In Namecheap: Domain List → Manage → Advanced DNS → Add New Record for each one. (Tip: you can send from a subdomain like mail.yourdomain.tld while receiving on the apex, or use the apex for both — follow what Resend shows you.)

  3. Create an Upstash Redis database (free tier is plenty). Copy its UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.

  4. Deploy the webhook (from the webhook/ directory):

    cd webhook
    npm install
    vercel --prod

    Set these environment variables on the Vercel project (all environments):

    Var Value
    RESEND_WEBHOOK_SECRET Resend webhook signing secret (whsec_…)
    UPSTASH_REDIS_REST_URL from Upstash
    UPSTASH_REDIS_REST_TOKEN from Upstash
  5. Create the Resend webhook: point it at your deployed Vercel URL and subscribe to the email.received event. Resend signs requests (Svix); the function verifies the signature and enqueues only the email_id.

  6. Give the local runtime its keys. In .env:

    RESEND_API_KEY=re_...
    UPSTASH_REDIS_REST_URL=...
    UPSTASH_REDIS_REST_TOKEN=...
    

Configure

In config.yaml, mark these domains source: resend (no IMAP fields):

domains:
  - name: example-d.com
    source: resend
  - name: example-e.com
    source: resend

On each poll cycle, resend_source.py drains the queue, fetches each email's headers from the Resend API, and runs it through the shared reply handler. If the Resend/Upstash env vars are absent, the drain is simply a no-op — handy for running IMAP-only without Resend configured.


Configuration reference

config.yaml

Key Meaning
domains[].name A domain to warm up
domains[].imap_host / imap_port IMAP receiving (Option A)
domains[].source: resend Resend receiving (Option B)
smtp.host / port / tls_mode Outbound transport
warmup.inboxes_per_domain Inboxes generated per domain at bootstrap
warmup.send_window_start_hour / end_hour Daily local send window
warmup.min/max_send_delay_seconds Jitter between an inbox's sends
warmup.reply_min/max_delay_minutes Random reply delay band
warmup.imap_poll_interval_minutes How often to poll for inbound
warmup.steady_state_emails_per_day Per-inbox volume after day 28
warmup.max_sends_per_inbox_per_day Optional hard ceiling (null = off)
wave_cohorts Optional automated fleet expansion (see below)

Files

File Purpose Committed?
config.yaml domains, SMTP, warmup tuning yes (yours)
.env SMTP_*, Resend/Upstash keys no (gitignored)
catchall_credentials.yaml IMAP catch-all passwords no (gitignored)
warmup.db SQLite state no (gitignored)

Running

python setup.py     # one-time: generate inboxes (re-runnable; only adds new)
python main.py      # start the runtime (Ctrl+C to stop)

Unattended operation (Windows)

Two options ship in tools/:

# A) Task Scheduler — runs at logon, restarts on crash, disables sleep on AC
powershell -ExecutionPolicy Bypass -File tools\install.ps1
schtasks /Run /TN "Darkzwarmer Warmup"      # start now
powershell -ExecutionPolicy Bypass -File tools\stop.ps1
powershell -ExecutionPolicy Bypass -File tools\uninstall.ps1

# B) Windows Service via NSSM — survives reboot with no one logged in
#    (run from an ELEVATED PowerShell)
powershell -ExecutionPolicy Bypass -File tools\install_service_nssm.ps1

Both auto-detect Python on PATH (override with $env:DARKZWARMER_PYTHON).

On Linux/macOS, run main.py under systemd, supervisord, pm2, or a tmux/screen session — it just needs to stay up.


Inspecting state

# Quick per-domain snapshot
python status.py

# Verify SMTP connectivity / auth
python tools/smtp_check.py                      # auth only
python tools/smtp_check.py --send you@you.com   # auth + send a test message

# Poke at the DB directly
sqlite3 warmup.db
> SELECT inbox_email, current_day, emails_sent_today FROM warmup_state;
> SELECT COUNT(*), day_number FROM send_log GROUP BY day_number;

Logs go to warmup.log (rotating). Tail it:

Get-Content warmup.log -Wait -Tail 50

The ramp and expansion waves

schedule_table.py holds the 28-day ramp (5 → 35 per inbox/day), then holds steady. max_sends_per_inbox_per_day clamps the top of the ramp if you need to keep total volume under a provider plan limit — rough monthly volume is:

messages/month ≈ inboxes × cap × 2 (reply doubling) × 30.4

wave_cohorts (optional) grows the fleet automatically: each cohort's domains add step_per_domain inboxes every interval_days from an anchor_date up to max_per_domain. New inboxes always enter at Day 1; existing inboxes are never disturbed; it's idempotent and catch-up-safe across restarts. Delete the block for a fixed fleet.


How it works internally

Module Responsibility
main.py Entry point: load config + DB, start scheduler, park
scheduler.py APScheduler jobs: daily roll, send planner, inbound poll, wave reconcile, orphan-reply recovery
fleet.py Generate unique first.last@domain inboxes
sender.py Outbound SMTP transport + single-send logic
replier.py IMAP poll + the shared inbound→reply decision logic
resend_source.py Drain the Resend/Upstash queue into that same logic
content.py Message + reply templates with no-repeat tracking
schedule_table.py The 28-day volume ramp
database.py SQLite schema + helpers (inboxes, logs, state, dedup)
webhook/ Vercel function: verify Resend webhook → enqueue to Upstash

Tests live in tests/ (python -m pytest).


Troubleshooting

  • IMAP login fails — most hosts need an app password, not the account password. Check imap_user matches what the host expects (some want the full address). If blank, it defaults to warmup@<domain>.
  • [IMAP-CONNECT-FAIL] every poll — wrong host/port, IMAP not enabled, or the catch-all isn't actually routing mail to the polled mailbox.
  • No replies on a Resend domain — confirm the MX record points at Resend, the webhook is subscribed to email.received, RESEND_WEBHOOK_SECRET matches, and the local .env has the Resend/Upstash keys (the drain no-ops without them).
  • [SEND-FAIL] — usually the provider's rate/quota, or the sending domain isn't authenticated (SPF/DKIM) at the provider. Lower inboxes_per_domain, set max_sends_per_inbox_per_day, or raise your provider quota.
  • Nothing sends — did you run python setup.py? Is it inside the 07:00–19:00 local window? Weekends are intentionally paused.

License

MIT. Provided "as is", without warranty. See the responsible-use note at the top — warm only domains and mailboxes you own, for mail that complies with applicable anti-spam law and your provider's terms.

About

Self-hosted email warmup network — cross-domain warmup mail on a 28-day ramp with human-delayed auto-replies. IMAP or Resend inbound; provider-agnostic SMTP.

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors