You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Email channel: implement EmailChannel against the Channel protocol #2
Implement EmailChannel — the first delivery rail, chosen because email has universal reach.
Self-contained: needs no node, credentials, or host access. The Channel protocol
(docs/architecture.md §5) is the whole interface.
Scope
validate_dest(raw) — normalise and validate an address; raise on junk.
send(dest, alert) — deliver one Alert, return a DeliveryResult.
Templates per AlertKind: outgoing is an alarm, incoming is calm and informational.
Unit tests against a local SMTP sink. No real mail server required.
Constraints (these are not negotiable — see CONTRIBUTING.md §1)
The subject line is a constant. Subjects are logged and indexed at every hop. It must not
vary with the alert, because variation is itself a signal.
The body carries the nickname, chain and status. Nothing else. No address, no amount, no
txid. The Alert dataclass is frozen and deliberately lacks those fields — if you find
yourself wanting one, that is invariant I2 doing its job. Open a discussion, don't add a field.
TLS enforced. No opportunistic downgrade.
privacy_ack is required before an email channel can be used: email is PROVIDER_READS, the
weakest privacy class, and the user must be told so in plain words.
DeliveryResult.detail must never contain the destination or the alert content.
Out of scope
Postfix hardening and the mail log/queue purge — separate issue, on me, since it is my
infrastructure.
Definition of done
Tests cover a successful send, a retriable failure, a permanent failure, and a proof that no
template can emit an address, amount or txid.
dest_ct encrypts the destination at rest, and postfix then writes it in plaintext to two places that encryption does not reach: the mail log and the deferred queue. So an EmailChannel that works perfectly still leaves a plaintext list of who is watching, on the same box, in files nothing in this design accounts for.
#22 has to land in the same slice rather than as a follow-up, because every alert sent in between is in the log permanently. Nothing about the Python here changes — this comment exists so that "the channel is done" and "the channel is safe to turn on" do not get confused for each other.
The related half already in scope here: the privacy_ack text has to say that email egress is CLEARNET. Tor exits are SMTP-blocklisted, so a smarthost-over-Tor was considered and rejected — our egress hides us, never the user.
#32 is merged (528ab9c) and it delivers everything this issue asked of the module. Keeping #2 open anyway, and I want to be explicit about why, because an open issue under a merged PR reads like an unfinished job and this is not one.
Done, and done to the definition of done written above:validate_dest, send, a template per AlertKind, and tests covering a successful send, a retriable failure, a permanent failure, and the proof that no template can emit an address, amount or txid. The three constraints that were called non-negotiable all hold, and hold structurally rather than by convention — the subject is a module constant, the body can only reach label/chain because Alert carries nothing else, and TLS is wrapped from the first byte so there is no STARTTLS phase for a downgrade to attach to. The sweep is at 74 and every email mutation is caught by a test that exercises the actual hole.
One thing that changed shape, worth recording here rather than discovering later.#22 is worded as blocking #2 — "ships in the same slice as EmailChannel, or the channel's privacy guarantee is not real." That framing has been overtaken: the channel is in main and postfix is untouched, and the guarantee is nonetheless intact, because nothing can call the channel. There is no enrolment API, so no destination has ever reached postfix.
That makes the ordering constraint #22 before #23, not #22 before #2. The moment enrolment wires a real destination to this module, the first alert writes a recipient address into the mail log permanently — which is exactly what #22 exists to prevent, and #23 is the next thing in the queue. So the dependency is live now rather than hypothetical, and I am moving it onto myself before enrolment starts rather than after.
Nothing in here is waiting on you. #3 is your lane and it is unblocked.
Closing: shipped in #32 and live in channels/email.py.
The definition of done in this issue was a successful send, a retriable failure, a permanent failure, and a proof that no template can emit an address, amount or txid — all four are covered in tests/test_email_channel.py, and the last one is a test that renders every AlertKind and both Directions and greps the result. Implicit TLS from the first byte (SMTP_SSL, 465), a constant subject, and a DeliveryResult.detail that never carries the destination are all in place, with 7 mutations in the sweep holding them there.
Postfix hardening and the mail log/queue purge are out of scope here by this issue's own wording and stay open as #22.
This has been done since 2026-08-09 and the issue was simply never closed.
Activity
Wired4ncer commentedon Aug 9, 2026
dest_ctencrypts the destination at rest, and postfix then writes it in plaintext to two places that encryption does not reach: the mail log and the deferred queue. So anEmailChannelthat works perfectly still leaves a plaintext list of who is watching, on the same box, in files nothing in this design accounts for.#22 has to land in the same slice rather than as a follow-up, because every alert sent in between is in the log permanently. Nothing about the Python here changes — this comment exists so that "the channel is done" and "the channel is safe to turn on" do not get confused for each other.
The related half already in scope here: the
privacy_acktext has to say that email egress is CLEARNET. Tor exits are SMTP-blocklisted, so a smarthost-over-Tor was considered and rejected — our egress hides us, never the user.Fix three review defects: the fallback direction, a -1 reply, and no-…
Say 74, and note that the number is the part that rots
Give email TLS it cannot lose, and a subject it cannot vary (#32)
Wired4ncer commentedon Aug 10, 2026
#32 is merged (
528ab9c) and it delivers everything this issue asked of the module. Keeping #2 open anyway, and I want to be explicit about why, because an open issue under a merged PR reads like an unfinished job and this is not one.Done, and done to the definition of done written above:
validate_dest,send, a template perAlertKind, and tests covering a successful send, a retriable failure, a permanent failure, and the proof that no template can emit an address, amount or txid. The three constraints that were called non-negotiable all hold, and hold structurally rather than by convention — the subject is a module constant, the body can only reachlabel/chainbecauseAlertcarries nothing else, and TLS is wrapped from the first byte so there is no STARTTLS phase for a downgrade to attach to. The sweep is at 74 and every email mutation is caught by a test that exercises the actual hole.What is left is not in the module, and not yours:
privacy_ack— listed as a constraint here, but it is an enrolment-time gate and there is no enrolment API yet. It belongs to Enrolment API, capability tokens, and the ARMING state machine #23.EmailChanneldoes its half already by declaringPROVIDER_READShonestly, so Enrolment API, capability tokens, and the ARMING state machine #23 has something true to check rather than a promise to trust.One thing that changed shape, worth recording here rather than discovering later. #22 is worded as blocking #2 — "ships in the same slice as
EmailChannel, or the channel's privacy guarantee is not real." That framing has been overtaken: the channel is inmainand postfix is untouched, and the guarantee is nonetheless intact, because nothing can call the channel. There is no enrolment API, so no destination has ever reached postfix.That makes the ordering constraint #22 before #23, not #22 before #2. The moment enrolment wires a real destination to this module, the first alert writes a recipient address into the mail log permanently — which is exactly what #22 exists to prevent, and #23 is the next thing in the queue. So the dependency is live now rather than hypothetical, and I am moving it onto myself before enrolment starts rather than after.
Nothing in here is waiting on you. #3 is your lane and it is unblocked.
Wired4ncer commentedon Aug 13, 2026
Closing: shipped in #32 and live in
channels/email.py.The definition of done in this issue was a successful send, a retriable failure, a permanent failure, and a proof that no template can emit an address, amount or txid — all four are covered in
tests/test_email_channel.py, and the last one is a test that renders every AlertKind and both Directions and greps the result. Implicit TLS from the first byte (SMTP_SSL, 465), a constant subject, and aDeliveryResult.detailthat never carries the destination are all in place, with 7 mutations in the sweep holding them there.Postfix hardening and the mail log/queue purge are out of scope here by this issue's own wording and stay open as #22.
This has been done since 2026-08-09 and the issue was simply never closed.