Hearth/README.md

4.7 KiB
Raw Blame History

Hearth

A small, consent-only email warmup service for levkine.ca. It exchanges real-looking (but genuinely sent/replied) mail between mailboxes you own and a fixed allow-list of people who've explicitly agreed to help, gradually building the engagement signals (opens, replies, "not spam" marks) that improve sender reputation with Gmail/Outlook/etc.

What this is (and isn't)

  • Is: a private warmup tool, same idea as commercial products like Mailwarm/Lemwarm — send/reply traffic only between mailboxes you control or people who opted in, at a low, gradually-ramping volume.
  • Isn't: a way to email arbitrary/unknown third parties, or to fake engagement to deceive spam filters at scale. There is no code path in this repo that can send to, or read mail from, an address that isn't explicitly listed in hearth.yml.

Guardrails

  1. Hard allow-list (hearth.yml) — the only source of recipients. Config.is_known_email() does an exact (case-insensitive) match; nothing is ever derived, scraped, or guessed. mailer.send() re-validates this itself, independent of any caller.
  2. Two participant tiers:
    • full_duplex — your own mailboxes. Hearth holds IMAP+SMTP credentials (via a HEARTH_PASSWORD_<KEY> env var, sourced from your secrets manager — never hardcoded), sends new threads, auto-replies to incoming pool mail, and un-spams anything a provider misfiled.
    • send_only — a consenting friend/coworker. Hearth only ever sends to their address from one of your full_duplex mailboxes. It never holds credentials for them and never logs into their account — they reply or mark-not-spam manually, on their own, whenever they want.
  3. Volume caps + ramp curve — starts at HEARTH_DAILY_CAP_START messages/day total, steps up by HEARTH_RAMP_STEP_PER_DAY/day to a hard HEARTH_DAILY_CAP_MAX, only during HEARTH_BUSINESS_HOURS_START_END (optionally weekdays only), spread with jitter rather than bursted.
  4. Kill switch + dry runHEARTH_ENABLED=false stops everything immediately; --dry-run logs every intended action without sending or modifying any mailbox.
  5. No bulk/marketing signatures — short, plain-text, varied content; no tracking pixels, no unsubscribe footers, no link-heavy bodies.

Quick start

cp hearth.yml.example hearth.yml   # edit: your mailboxes + consenting participants
cp env.example .env                # edit: HEARTH_PASSWORD_<KEY> per full_duplex mailbox
docker compose up -d --build
docker compose logs -f

Dry run locally without Docker:

python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
HEARTH_CONFIG=./hearth.yml HEARTH_DB_PATH=./data/hearth.db \
  HEARTH_PASSWORD_ALERTS=... .venv/bin/python -m hearth.main --once --dry-run

Run the test suite (includes the allow-list guardrail tests):

.venv/bin/python -m pytest -q

Adding / removing a participant

  • Your own mailbox (full_duplex): add an entry to hearth.yml with tier: full_duplex, IMAP/SMTP host+port, username, and a password_env name; set that env var (or vault key, see the ansible repo's docs/guides/hearth-deploy.md) to an app password.
  • A consenting friend (send_only): confirm with them first, then add tier: send_only with just their email (and optional display_name). No credentials, ever.
  • Removing someone: delete their block from hearth.yml and restart — Hearth re-reads the allow-list on every start, and the code has no cache of "recipients I've seen before" outside of what's in that file.

Architecture

Module Responsibility
hearth/config.py Loads + validates hearth.yml; the only source of recipients
hearth/settings.py Env-based runtime settings (caps, ramp, kill switch, dry-run)
hearth/store.py SQLite: daily counters, ramp start date, message/thread history, reply queue
hearth/composer.py Message content (template bank, optional Ollama/Open WebUI variety)
hearth/mailer.py SMTP send, with its own allow-list re-check
hearth/watcher.py IMAP poll for full_duplex mailboxes: detect pool mail, queue delayed replies, un-spam
hearth/scheduler.py Ramp-curve + business-hours + cap logic for starting new threads
hearth/main.py CLI entrypoint (--loop, --once, --dry-run, --healthcheck)

Deployment

See the ansible homelab repo's docs/guides/hearth-deploy.md for how this fits into the rest of the levkine.ca mail setup (Vault secrets convention, target host, monitoring notes). deploy-hearth.sh in this repo handles the actual rsync + docker compose up once a target host is chosen.