4.7 KiB
4.7 KiB
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
- 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. - Two participant tiers:
full_duplex— your own mailboxes. Hearth holds IMAP+SMTP credentials (via aHEARTH_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 yourfull_duplexmailboxes. 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.
- Volume caps + ramp curve — starts at
HEARTH_DAILY_CAP_STARTmessages/day total, steps up byHEARTH_RAMP_STEP_PER_DAY/day to a hardHEARTH_DAILY_CAP_MAX, only duringHEARTH_BUSINESS_HOURS_START–_END(optionally weekdays only), spread with jitter rather than bursted. - Kill switch + dry run —
HEARTH_ENABLED=falsestops everything immediately;--dry-runlogs every intended action without sending or modifying any mailbox. - 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 tohearth.ymlwithtier: full_duplex, IMAP/SMTP host+port,username, and apassword_envname; set that env var (or vault key, see theansiblerepo'sdocs/guides/hearth-deploy.md) to an app password. - A consenting friend (
send_only): confirm with them first, then addtier: send_onlywith just theiremail(and optionaldisplay_name). No credentials, ever. - Removing someone: delete their block from
hearth.ymland 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.