mail

package
v0.2.41 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MPL-2.0 Imports: 42 Imported by: 0

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

View Source
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

func ParseMessage(raw []byte) (jsonOut string, err error)

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.

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

func (m *Mailer) Send(ctx context.Context, tenant string, in []byte) (event.Payload, error)

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

func (m *Mailer) SetContentStore(fcas filecas.Store, ix blob.Index)

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.

type SubmitFunc

type SubmitFunc func(ctx context.Context, from string, rcpts []string, msg []byte) error

SubmitFunc hands one rendered RFC 5322 message to the relay for `from` → `rcpts` (the envelope recipients: the To recipient plus any Cc/Bcc). Injectable so tests can substitute a fake.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL