diff --git a/README.md b/README.md index d6a655d..b33ad80 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Dry run locally without Docker: ```bash 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 + HEARTH_PASSWORD_MAILBOX_A=... .venv/bin/python -m hearth.main --once --dry-run ``` Run the test suite (includes the allow-list guardrail tests): diff --git a/docs/intro-email.md b/docs/intro-email.md index f29c705..d5a5ad0 100644 --- a/docs/intro-email.md +++ b/docs/intro-email.md @@ -8,6 +8,9 @@ the individual messages themselves will just read as normal short emails. Don't add anyone to `hearth.yml` until they've actually seen this and said yes. +Real participant addresses live only in the gitignored `hearth.yml` on the +deploy host — never commit them. + --- ## Copy-paste template @@ -16,10 +19,10 @@ yes. > > Hey [name], > -> Random favor to ask. I run my own email server for `levkine.ca` (the -> "IT nerd homelab" thing), and small self-hosted mail servers like mine -> have a real problem: Gmail/Outlook/Yahoo don't trust them by default, so -> my emails sometimes land in spam even for real messages. +> Random favor to ask. I run my own email server (the "IT nerd homelab" +> thing), and small self-hosted mail servers like mine have a real problem: +> Gmail/Outlook/Yahoo don't trust them by default, so my emails sometimes +> land in spam even for real messages. > > The fix is basically just... using it normally. The more genuine > back-and-forth email traffic my addresses have — people actually opening, @@ -28,8 +31,8 @@ yes. > > So here's the ask: every so often (probably a couple times a week at > most, starting slower), you might get a short, casual email from one of -> my addresses (`ilia@levkine.ca`, possibly others later). Nothing weird — -> just a "hey, checking in, how's it going" type message. If you could: +> my addresses. Nothing weird — just a "hey, checking in, how's it going" +> type message. If you could: > > - **Reply normally**, like you would to a text — doesn't need to be long > - **Mark it "Not Spam"** if it ever lands in your Spam/Junk folder instead @@ -42,7 +45,7 @@ yes. > > Let me know if you're in! > -> — Ilia +> — [your name] --- @@ -55,8 +58,8 @@ yes. text message, not a newsletter. - **Timing:** business hours only, your local time zone, spread out rather than bursts. -- **Senders:** real mailboxes I actually control (`@levkine.ca` addresses, - or my own personal Gmail) — never a spoofed/unrelated identity. +- **Senders:** real mailboxes I actually control — never a spoofed/unrelated + identity. - **Never:** links, attachments, requests for money/info/action, anything that reads like marketing. diff --git a/env.example b/env.example index 20be2ec..5a3c713 100644 --- a/env.example +++ b/env.example @@ -31,8 +31,8 @@ HEARTH_MAX_REPLY_DELAY_MIN=180 HEARTH_LLM_URL= HEARTH_LLM_MODEL=llama3.1 -# Optional: daily summary via your existing ntfy instance -NTFY_URL=https://push.levkin.ca +# Optional: daily summary via ntfy +NTFY_URL= NTFY_PUBLISH_TOKEN= NTFY_TOPIC=hearth-digest @@ -40,5 +40,5 @@ NTFY_TOPIC=hearth-digest # One HEARTH_PASSWORD_ per full_duplex participant in hearth.yml # (KEY uppercased, matching the participant's `key:`). send_only participants # need no password — Hearth never authenticates as them. -HEARTH_PASSWORD_ILIA= -HEARTH_PASSWORD_PERSONAL_GMAIL= +HEARTH_PASSWORD_MAILBOX_A= +HEARTH_PASSWORD_PERSONAL_INBOX= diff --git a/hearth.yml.example b/hearth.yml.example index ccbd4a5..1c5a8f4 100644 --- a/hearth.yml.example +++ b/hearth.yml.example @@ -9,7 +9,7 @@ # pairs before turning on the full pool. Omit `active` entirely (default) to # make everyone below active. # -# active: [ilia, personal_gmail] +# active: [mailbox_a, personal_inbox] # # tier: full_duplex # Your own mailboxes. Hearth logs in via IMAP+SMTP, sends new threads, @@ -17,34 +17,34 @@ # username + a HEARTH_PASSWORD_ env var (see env.example). # # tier: send_only -# A friend/coworker who explicitly agreed to help warm up levkine.ca. +# A friend/coworker who explicitly agreed to help warm up the domain. # Hearth only ever sends TO this address from one of your full_duplex # mailboxes — it never has credentials for this inbox, and never logs # into it. The participant replies/marks-not-spam manually, whenever # they feel like it, from their own mail client. participants: - ilia: + mailbox_a: tier: full_duplex - email: ilia@levkine.ca - display_name: "Ilia" - imap_host: mail.levkine.ca + email: mailbox-a@example.com + display_name: "You" + imap_host: mail.example.com imap_port: 993 - smtp_host: mail.levkine.ca + smtp_host: mail.example.com smtp_port: 587 - username: ilia@levkine.ca - password_env: HEARTH_PASSWORD_ILIA + username: mailbox-a@example.com + password_env: HEARTH_PASSWORD_MAILBOX_A - personal_gmail: + personal_inbox: tier: full_duplex - email: idobkin@gmail.com - display_name: "Ilia" - imap_host: imap.gmail.com + email: you@example.com + display_name: "You" + imap_host: imap.example.com imap_port: 993 - smtp_host: smtp.gmail.com + smtp_host: smtp.example.com smtp_port: 587 - username: idobkin@gmail.com - password_env: HEARTH_PASSWORD_PERSONAL_GMAIL + username: you@example.com + password_env: HEARTH_PASSWORD_PERSONAL_INBOX # Example consenting third party — replace with a real person who has # actually agreed to this. Delete this entry if you only want to warm up diff --git a/tests/test_config.py b/tests/test_config.py index a0c75ea..9c6fac3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,11 +9,11 @@ VALID_YAML = textwrap.dedent( participants: alerts: tier: full_duplex - email: alerts@levkine.ca - display_name: "Ilia" - imap_host: mail.levkine.ca - smtp_host: mail.levkine.ca - username: alerts@levkine.ca + email: alerts@example.com + display_name: "Ops" + imap_host: mail.example.com + smtp_host: mail.example.com + username: alerts@example.com password_env: TEST_PASSWORD_ALERTS friend: tier: send_only @@ -34,7 +34,7 @@ def test_load_valid_config(tmp_path): config = load_config(path) assert set(config.all_keys()) == {"alerts", "friend"} assert config.full_duplex_keys() == ["alerts"] - assert config.is_known_email("ALERTS@levkine.ca") == "alerts" + assert config.is_known_email("ALERTS@example.com") == "alerts" assert config.is_known_email("nobody@nowhere.com") is None @@ -49,14 +49,14 @@ def test_duplicate_email_rejected(tmp_path): participants: alerts: tier: full_duplex - email: alerts@levkine.ca - imap_host: mail.levkine.ca - smtp_host: mail.levkine.ca - username: alerts@levkine.ca + email: alerts@example.com + imap_host: mail.example.com + smtp_host: mail.example.com + username: alerts@example.com password_env: TEST_PASSWORD_ALERTS duplicate: tier: send_only - email: alerts@levkine.ca + email: alerts@example.com display_name: "Dup" """ ) @@ -71,7 +71,7 @@ def test_full_duplex_requires_creds_fields(tmp_path): participants: broken: tier: full_duplex - email: broken@levkine.ca + email: broken@example.com """ ) path = write_yaml(tmp_path, content) @@ -100,10 +100,10 @@ def test_active_list_restricts_to_subset(tmp_path): participants: alerts: tier: full_duplex - email: alerts@levkine.ca - imap_host: mail.levkine.ca - smtp_host: mail.levkine.ca - username: alerts@levkine.ca + email: alerts@example.com + imap_host: mail.example.com + smtp_host: mail.example.com + username: alerts@example.com password_env: TEST_PASSWORD_ALERTS friend: tier: send_only @@ -127,10 +127,10 @@ def test_active_list_with_unknown_key_raises(tmp_path): participants: alerts: tier: full_duplex - email: alerts@levkine.ca - imap_host: mail.levkine.ca - smtp_host: mail.levkine.ca - username: alerts@levkine.ca + email: alerts@example.com + imap_host: mail.example.com + smtp_host: mail.example.com + username: alerts@example.com password_env: TEST_PASSWORD_ALERTS """ ) @@ -146,10 +146,10 @@ def test_active_list_must_keep_a_full_duplex_participant(tmp_path): participants: alerts: tier: full_duplex - email: alerts@levkine.ca - imap_host: mail.levkine.ca - smtp_host: mail.levkine.ca - username: alerts@levkine.ca + email: alerts@example.com + imap_host: mail.example.com + smtp_host: mail.example.com + username: alerts@example.com password_env: TEST_PASSWORD_ALERTS friend: tier: send_only diff --git a/tests/test_mailer_guard.py b/tests/test_mailer_guard.py index e2c2fbd..89ecc89 100644 --- a/tests/test_mailer_guard.py +++ b/tests/test_mailer_guard.py @@ -12,17 +12,17 @@ YAML = textwrap.dedent( participants: alerts: tier: full_duplex - email: alerts@levkine.ca - imap_host: mail.levkine.ca - smtp_host: mail.levkine.ca - username: alerts@levkine.ca + email: alerts@example.com + imap_host: mail.example.com + smtp_host: mail.example.com + username: alerts@example.com password_env: TEST_PASSWORD_ALERTS gmail: tier: full_duplex - email: idobkin@gmail.com - imap_host: imap.gmail.com - smtp_host: smtp.gmail.com - username: idobkin@gmail.com + email: you@example.com + imap_host: imap.example.com + smtp_host: smtp.example.com + username: you@example.com password_env: TEST_PASSWORD_GMAIL friend: tier: send_only @@ -83,5 +83,5 @@ def test_cannot_send_as_send_only_participant(config): def test_is_known_email_is_exact_match_only(config): # A lookalike/subdomain address must never resolve to a real participant. - assert config.is_known_email("alerts@levkine.ca.evil.com") is None + assert config.is_known_email("alerts@example.com.evil.com") is None assert config.is_known_email("not-friend@example.com") is None