Files
Bendik Aagaard LynghaugandClaude Opus 5.5 6b91258ebe One set of mail variables for every mail, and {link} in state mails
{site} and {site_name} everywhere; a state mail adds {self_link} (the
submitter's own page, when their record's state allows a move there),
{desk_link} and {link}, which picks by who the mail is to. gullberg's
{link} mails went out blank until now. Documented in docs/mail.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MznPTC31w5mSUK4XMehcpi
2026-10-02 11:25:49 +02:00

2.9 KiB

Mail

Portal writes every mail itself and hands it to gdo to deliver (portal.mail.send); a reply comes back as a note beside the record (portal.mail.received). A mail is a subject and a markdown body with {placeholders}, sent as text with an HTML alternative. The kinds of mail are the ones content declares on a state (see State machines), the invite and its rewording (invite: { group, mail }, mail.invite in site.yaml), the note to a person named responsible for a question, and the answers to a suggested change.

Variables

A placeholder is { letters, digits, _, - or . }. One that names nothing renders as nothing; a brace that opens no name stays a brace. The variables come in layers, so a template written for one kind of mail reads the same in another.

On every mail

{site} the instance's public URL, no trailing slash
{site_name} site.yaml's title

On a mail about a record (every state mail)

{field} any field of the record, as the form sent it
{chain} the record's id
{email} the record's email field, trimmed
{bucket}, {state} where it is and the state it just entered
{notice.field} a field of the record this one belongs to, through its type: record field notice
{self_link} the record's own page for the person it belongs to: a page offering a self-service move (self_transition) on the bucket from the state the record is in, with the ?chain=&email= that lets them act without signing in. Empty when there is no such page or no address
{desk_link} the desk that moves the record: the page whose list over the bucket has transitions, else one that reads it. Empty when no page reads the bucket
{link} where this reader acts: {self_link} in a mail to: submitter, {desk_link} in one to anyone else, and {site} when that link is empty

{link} is the one to reach for. "Se meldingen og avtal befaring: {link}" sent to the caretaker opens the desk the report is on; "Feilen du meldte om er utbedret. {link}" sent to the tenant opens their own case where they can still act on it, or the site when there is nothing left for them to do.

On an invite and the note to someone responsible, beside every field of the invite's form: {name}, {username}, {email}, {group} (invite), {questions} (responsible), {expires}, and {link}, which here is the one-time sign-in link.

What the lint holds a mail to

A state mail that sends from a state no record reaches warns; one that names a field no form recording into the bucket collects fails, since it would go out with a blank where the field should be. The lint knows every variable above, so {link} and the rest are never flagged. to: submitter requires a required email on every form filling the bucket, and to: "{notice.email}" requires the notice field.