All docs

Docs · Reference

Email Out

Send email with EXEC txco://sendmail

You can send email from Thanks, Computer as well as receive it.

To send:

  • Configure the _sendmail contract on the envelope
  • EXEC "txco://sendmail" to request the email be sent.
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). Also exposed to the shell as {{.Vars.name}} — e.g. a per-send link in a custom templates.html
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
templates.htmlA custom HTML template to wrap the body in, replacing the bundled default. Slots: {{.Subject}}, {{.Body}}, {{.Preheader}}, and {{.Vars.x}} for any vars. Omitted → the default template

The HTML body is wrapped in a responsive, CSS-inlined shell — the bundled default, or a templates.html you supply (see below) — and messages are DKIM-signed.

Custom Template — templates.html

By default the body is dropped into a built-in responsive shell. To control the whole email (your own header, footer, branding, responsive @media styles), pass a complete HTML document as templates.html with the same {{.Body}} / {{.Subject}} / {{.Preheader}} slots the default uses (plus {{.Vars.x}} for any per-send vars, e.g. a {{.Vars.nexturl}} button href):

# load a template shipped in the stack's FILES, then send with it
WITH files = &array(&object("path", "_email/drip.html", "as", "tmpl"))
EXEC "txco://read-file"

SET ._sendmail.to            = .reader,
    ._sendmail.from          = "news@ops.example.com",
    ._sendmail.subject       = "This week",
    ._sendmail.body          = .article_html,
    ._sendmail.templates.html = ._files.tmpl.content
EXEC "txco://sendmail"

Variable substitutions are possible inside a html/template: {{.Subject}} is auto-escaped, {{.Body}} is your _sendmail.body.

Note

The from domain must be a verified hostname of the sending tenant (routing) — a rule can’t send as a domain its tenant doesn’t own. If a send fails with from_not_verified, add and verify the hostname first (txco auth tenant hostnames add … && … verify).

What comes back

The EXEC of the operation 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, invalid_template

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 SMTP.

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