All docs

Docs · Channels

Outbound email

txco://sendmail

The chassis sends mail as well as receiving it (lmtp.md): a rule assembles a sendmail contract on the envelope and dispatches EXEC "txco://sendmail"._

WHEN .resolution == "credited"
SET ._sendmail.subject = "Your credit has been applied",
    ._sendmail.from    = "billing@ops.example.com",
    ._sendmail.to      = .customer.email,
    ._sendmail.body    = "<p>Hi {{.name}}, your credit for invoice {{.invoice}} is in.</p>"
EXEC "txco://sendmail"

The _sendmail contract

Required: subject, body (HTML), from.

FieldMeaning
toOne address, a list, or a list of {address, vars} objects for per-recipient personalization
varsShared template variables; {{.name}}-style markers in subject/body render per recipient (missing keys render empty)
textExplicit plaintext part; omitted, it’s derived from the HTML body
cc / bccFlat address lists added to every message (cc visible, bcc envelope-only)
reply_toDedicated Reply-To field
headersExtra headers map — structural/signing/loop-guard headers are denylisted (use reply_to, not a raw header)
envelope_fromMAIL FROM / Return-Path override. Defaults to from. Set "<>" for a null reverse-path — the RFC 3834 posture for auto-replies (no bounce loops)
campaignLabel for rate-limit and audit grouping

The HTML body is wrapped in a responsive, CSS-inlined default shell, and messages are DKIM-signed.

Anti-spoof: the from domain must be a verified hostname of the sending tenant (ingress.md) — a rule cannot send as a domain its tenant doesn’t own.

What comes back

The op merges a result under _sendmail.result:

  • success: {sent, skipped, failed, recipients: […]} — per-recipient outcomes; rate-limited recipients are skipped with reason rate_limited
  • error: {status: "error", reason, error} — reasons include no_relay, missing_field, invalid_from, from_not_verified, no_recipients, too_many_recipients

Operator configuration

Sending is off until a relay is configured. The chassis is a submitter, not an MTA: it hands rendered messages to your edge Postfix (same trust posture as the LMTP inlet — private network, no auth).

FlagDefaultMeaning
--mail-relay-addr(empty = disabled)SMTP submission address (host:port)
--mail-relay-tlsnonenone (trusted private net) or starttls
--mail-dial-timeout-ms5000Dial + submit deadline; a down relay fails fast
--mail-max-recipients50Per-call cap; over it the op errors rather than truncating
--mail-rate-limits(empty = off)Per-tenant caps, e.g. "100/2m,200/4h" — every rule must be under its cap. Per node, in memory: a runaway-loop valve, not fleet-wide accounting

Edit this page · View as markdown