The way back in: replies read over JMAP, handed to portal as notes
Test / test (push) Successful in 21s
Publish release / package (aarch64, , aarch64, , , ) (push) Failing after 2s
Publish release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Failing after 36s

A mail about a record now answers to case+<bucket>.<record>@<domain>
when a mailbox is configured, and gdo reads that mailbox: it cuts the
quoted history off what the person wrote, publishes each reply on
portal.mail.received, and marks it seen only once portal has it. The
address carries portal's own chain hash and opens nothing by itself;
portal checks the sender against the record and never moves a case on
a reply. Unconfigured, gdo sends exactly as before.

Tested: the address built for a case and refused for an invite, the
address parsed back or rejected (wrong domain, wrong prefix, no
record, odd characters), the quote and signature cut in English and
Norwegian, a JMAP mail turned into a reply with its own id, an
HTML-only mail falling back to its preview, and a half-configured
mailbox staying off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-24 21:33:41 +02:00
co-authored by Claude Opus 5
parent 1c1a8f4309
commit 1c75dd5681
8 changed files with 1208 additions and 25 deletions
+27
View File
@@ -84,3 +84,30 @@ A minimal theme:
#v(3mm)
#sys.inputs.at("text", default: "")
```
## The way back in
With a mailbox configured, every mail about a record carries a
Reply-To of its own and replies come back to the case:
| Env | |
|---|---|
| `JMAP_URL` | the session endpoint, e.g. `https://mail.example.no/.well-known/jmap` |
| `JMAP_TOKEN` | a bearer token for that account |
| `MAIL_REPLY_DOMAIN` | the domain the reply addresses live on |
| `MAIL_REPLY_PREFIX` | the local part before the `+`, `case` by default |
| `JMAP_EVERY` | seconds between looks, 60 by default |
All three of the first are needed or inbound stays off: a reply
address nobody reads is worse than none. The address is
`case+<bucket>.<record>@<domain>`, and the record id in it is
portal's own chain hash - the same unguessable capability a decide
link carries. It is half the proof: portal also checks the sender
against the address the record holds, and a reply is only ever a
note, never a decision.
gdo reads what is unread, publishes each reply on
`portal.mail.received`, and marks it seen only once portal has it - a
crash in between costs a repeated note, which portal refuses by id,
rather than a lost one. Mail addressed to anything but a record is
left unread for a person.