Documentation
¶
Overview ¶
Package mail implements the bundled txco://sendmail op: it reads the `_sendmail` envelope contract a rule assembled, renders an email (a shell template wrapping the body — the bundled default, or a caller-supplied `_sendmail.templates.html`), enforces the per-tenant campaign at-most-once guard, verifies the From domain, submits to a relay, and emits a usage line. The common case is to/subject/body/from; `calendar` (an iTIP part) and `attachments[]` are the MIME extras (mime.go); policy caps are later phases (see internal docs/todo-sendmail.md).
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNoRelay = errors.New("sendmail: no mail relay configured (set --mail-relay-addr / TXCO_MAIL_RELAY_ADDR)")
ErrNoRelay is returned by the op when no relay is configured.
Functions ¶
func DomainOwnedByTenant ¶ added in v0.2.25
func DomainOwnedByTenant(ctx context.Context, db *sql.DB, dia registry.Dialect, slug, domain string) (bool, error)
DomainOwnedByTenant is the single ownership rule for "may tenant `slug` act as `domain`": a non-revoked tenant_hostnames row with verified_at set, OR a domain the chassis serves authoritative DNS for (an active dns_zones row — delegation is itself proof of control). Applied by txco://sendmail (the From: guard) and by txco://imap/account (the username's domain); package-level so both read the same tables the same way. db is normally the dbcache mirror snapshot (dialect SQLite).
func ParseMessage ¶ added in v0.2.28
ParseMessage takes RFC 5322 bytes and returns a JSON object describing the message — the shared inbound-message shape used by BOTH the LMTP inlet (under `_txc.lmtp.msg`) and the remote-source watcher (under `_txc.source.msg`), so a mail that arrived by delivery and one fetched from a remote mailbox produce byte-identical JSON. Populates:
id string — Message-ID header
date string (RFC3339, optional) — parsed Date header
subject string — RFC 2047 decoded
from / to / cc [{name, addr}, ...]
text string — text/plain body
html string — text/html body
headers { name: [values...] } — multi-value-safe, keys LOWERCASED
attachments [{name, type, size, sha256, content}, ...] (content = base64)
calendar {method, uid, partstat?, attendee?, start?, end?, sequence?}
— only when an iTIP part is present
The caller is responsible for the `.raw` field (the b64-encoded original bytes) — kept separately as the always-safe escape hatch for rules that want to re-deliver, archive, or re-parse.
enmime's parse is forgiving: missing Subject, bad base64, non-UTF-8 charsets, malformed multipart all produce best-effort output plus non-fatal entries in `env.Errors`. We surface those errors to the caller for logging but do NOT fail — a partly-parsed envelope is more useful to rules than nothing.
A DECLARED CHARSET IS BELIEVED. enmime otherwise runs charset detection on every text part of a hundred characters or more and, when the detector is confident, uses its guess over what the message declared — and a UTF-8 text with a few curly quotes and a dash is confidently "windows-1252", so correct mail arrived as mojibake ("I’m" → "I’m") depending on its length and punctuation. Detection still runs for a part that declares no charset at all.
Types ¶
type Config ¶
type Config struct {
RelayAddr string
RelayTLS string // "none" | "starttls"
DialTimeout time.Duration
MaxRecipients int
RateLimits string // per-tenant send caps, e.g. "100/2m,200/4h"; empty disables
}
Config carries the relay + limits resolved from chassis config.
type Mailer ¶
type Mailer struct {
// contains filtered or unexported fields
}
Mailer is the txco://sendmail handler with its injected deps. db is the REAL, writable runtime *sql.DB (stable — dbcache only swaps its in-memory snapshot, not this handle), used for the campaign claim and the From-domain check. usage is nil-safe. submit is injectable (real SMTP in prod, a fake in tests).
func NewMailer ¶
func NewMailer(db *sql.DB, dialect registry.Dialect, snap func() *sql.DB, u usage.Sink, log *zap.Logger, cfg Config) *Mailer
NewMailer builds a Mailer. db must be the real runtime DB (e.g. pu.RuntimeDB) and dialect its DB dialect (pu.RuntimeDialect) so the runtime queries rebind for Postgres. snap is the dbcache mirror snapshot getter (pu.Dbc.Snapshot) used for the per-send verification READS — pass the method value, never a captured *sql.DB (Reload swaps the handle); nil falls back to db. usage/log may be nil.
func (*Mailer) Relay ¶ added in v0.2.19
func (m *Mailer) Relay(ctx context.Context, tenant, source string, in []byte) (event.Payload, error)
Relay is the txco://relay op body — the `.forward` primitive. Unlike Send it does NOT re-compose or re-DKIM-sign: it ships the ORIGINAL RFC 5322 bytes (`_relay.raw`, base64, straight from `@lmtp.msg.raw`) to a new recipient untouched, so the sender's surviving DKIM signature keeps DMARC aligned to their domain and attachments are preserved. It just rewrites the SMTP envelope: MAIL FROM = `_relay.envelope_from` (a return-path the tenant owns, so SPF aligns and bounces route somewhere catchable), RCPT = `_relay.to`.
source is the request's PINNED originating inlet (processor.SourceScope) — the trusted answer to "which inlet started this", captured at first Run, NOT the mutable `_txc.src` envelope field. Because relay ships arbitrary bytes out under a verified return-path (far more dangerous than sendmail, which forces a verified From + signs), it MUST run only from the inbound-mail path: a coerced web/other pipeline could otherwise relay attacker-supplied bytes as a spam cannon riding the domain's reputation. So the first thing Relay does is refuse unless source == "lmtp".
tenant is the PINNED request tenant (processor.TenantScope), used for the defense-in-depth envelope_from-domain-verified check and the campaign guard.
func (*Mailer) Send ¶
Send is the op body. tenant is the PINNED request tenant (resolved by the caller from the trusted context, NOT the mutable envelope) — load-bearing for the campaign key and the From-domain anti-spoof check.
func (*Mailer) SetContentStore ¶ added in v0.2.26
SetContentStore wires the content store and the tenant blob index so a send with `_sendmail.retain = true` keeps the exact bytes it submitted (per recipient, DKIM-signed) under the tenant's ownership and returns their sha256 — the reference a later op adopts (`txco://imap/append from_sha`, `txco://blob/put from_sha`). Bytes never ride the envelope; the content store is the byte plane between ops, as it is for the IMAP head's parts and for blob/put.