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>
114 lines
4.5 KiB
Markdown
114 lines
4.5 KiB
Markdown
# gdo
|
|
|
|
The transmitter that sends the code through the gate so the iris
|
|
opens. [iris](https://project.uhhm.no/uhhm/iris) keeps bad content
|
|
out of a [portal](https://project.uhhm.no/uhhm/portal) site; gdo
|
|
sends the mail that lets a known person in, and every other mail a
|
|
site's content says to send.
|
|
|
|
Portal decides *when* a person hears something and renders the mail:
|
|
`mail:` on a state in `aggregates.yaml` sends when a record enters
|
|
that state, and an invite mails its one-time link built in. It
|
|
publishes each rendered mail on `portal.mail.send` in the `mail`
|
|
JetStream stream. gdo holds a durable consumer there and hands each
|
|
message to the host's SMTP. Portal never talks SMTP; gdo never reads
|
|
content; a message survives either of them being down, because the
|
|
stream keeps it.
|
|
|
|
## Run
|
|
|
|
```
|
|
gdo # the consumer; one per host
|
|
gdo probe kari@example.no # one message through, reports delivery
|
|
gdo --version
|
|
```
|
|
|
|
| Env | Default | |
|
|
|---|---|---|
|
|
| `NATS_URL` | `nats://127.0.0.1:4222` | credentials in the URL, as portal's own |
|
|
| `SMTP_HOST`, `SMTP_PORT` | `127.0.0.1`, `25` | the host's postfix; no TLS, no auth, localhost only |
|
|
| `SMTP_HELO` | the host's full name | what gdo says in EHLO; a strict server refuses a bare hostname |
|
|
| `GDO_CONSUMER` | `gdo` | the durable consumer's name |
|
|
| `GDO_TYPST_THEME` | unset | a `.typ` file; set, every mail carries a PDF rendered from it |
|
|
|
|
Install on a host from the [uhhm] Arch registry on project.uhhm.no
|
|
(the repo and its key: see uhhm/corp's README), as root:
|
|
|
|
```
|
|
pacman -Sy gdo
|
|
<edit /etc/gdo/env: NATS_URL with the host's credentials>
|
|
systemctl enable --now gdo
|
|
```
|
|
|
|
The package puts the binary in `/usr/bin`, the env template in
|
|
`/etc/gdo/env` (mode 0600, the one file with a secret), and the unit
|
|
`gdo.service`. Then `gdo probe <you>`, with the env loaded.
|
|
|
|
## The message
|
|
|
|
One JSON object per message, `v: 1`:
|
|
|
|
```json
|
|
{ "v": 1, "id": "trafikkmeldinger:<record>:ordnet", "site": "Tomter Vel",
|
|
"from": "Tomter Vel <vel@example.no>", "reply_to": "post@example.no",
|
|
"to": "kari@example.no", "subject": "Ordnet: Svingen ved skolen",
|
|
"text": "…markdown as plain text…", "html": "<!doctype html>…" }
|
|
```
|
|
|
|
`id` is the JetStream message id: a transition that fires twice sends
|
|
once. Sent as `multipart/alternative`, text first, so the plain text
|
|
is what every client can read and the HTML is the courtesy. A message
|
|
gdo cannot parse is acked and logged; one with a `v` it does not know
|
|
is left in the stream for a gdo that does; one SMTP refuses is retried
|
|
after a minute, for as long as the stream keeps it (a week).
|
|
|
|
## Typst themes
|
|
|
|
Plain text is the default and needs nothing. With `GDO_TYPST_THEME`
|
|
set to a `.typ` file and `typst` on the host, gdo runs
|
|
`typst compile` with `--input site=… subject=… text=… to=…` and
|
|
attaches the PDF as `<site>.pdf`. Typst's PDF is dependable; its HTML
|
|
export is not yet mail-grade, so the HTML alternative stays the plain
|
|
one and the theme is the attachment. A theme that fails to render
|
|
costs the attachment, never the mail.
|
|
|
|
A minimal theme:
|
|
|
|
```typst
|
|
#let site = sys.inputs.at("site", default: "")
|
|
#set page(width: 148mm, height: auto, margin: 18mm)
|
|
#set text(font: "Libertinus Serif", size: 11pt)
|
|
#text(size: 9pt, fill: gray)[#site]
|
|
#v(6mm)
|
|
#heading(sys.inputs.at("subject", default: ""))
|
|
#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.
|