Files
gdo/README.md
T
Bendik Aagaard LynghaugandClaude Fable 5.1 983491295e
Test / test (push) Successful in 4s
Publish release / publish (push) Successful in 18s
gdo: the code that opens the iris
A durable consumer on portal's WORMHOLE stream, each message handed to
the host's SMTP as text with an HTML alternative; acked on acceptance,
retried after a minute otherwise, left for a newer gdo when the
contract version is unknown. `gdo probe <to>` sends one through and
waits for the consumer to drain. GDO_TYPST_THEME attaches a Typst PDF.
A systemd unit and an installer for one gdo per host.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
2026-09-23 19:47:57 +02:00

3.3 KiB

gdo

The transmitter that sends the code through the gate so the iris opens. iris keeps bad content out of a 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 WORMHOLE 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 a release, as root:

curl -sfLO https://project.uhhm.no/uhhm/gdo/raw/branch/main/deploy/install.sh
sudo sh install.sh v0.1.0 'nats://infra:<token>@127.0.0.1:4222'

That puts the binary in /usr/local/bin, the env in /etc/gdo.env (mode 0600, the one file with a secret), and the unit deploy/gdo.service enabled and running. Then gdo probe <you>.

The message

One JSON object per message, v: 1:

{ "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:

#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: "")