valis / Reference / API reference
Mail - API reference
Exported surface for the mail subsystem. Part of the API reference.
Package valis/src/mail/delivery
Functions
deliver-local-once
(deliver-local-once store head)
Deliver every active-tier entry the router classifies :local-deliver into the owner Maildir (local-deliver), then record it terminally (release-slot :delivered) so its queue slot frees. An entry whose delivery signals is recorded :deferred so it retries rather than stranding in :active. Returns an alist (entry-name . disposition) for the local entries handled this pass. Relay and secondary entries are left in :active for the outbound drain — this is the inbound local-delivery driver only.
mail-delivery-running-p
(mail-delivery-running-p)
Return T while the standing delivery loop is active, NIL otherwise. The read accessor twin of MAIL-DRAIN-RUNNING-P; safe to call at any time.
promote-incoming-once
(promote-incoming-once store head)
Admit each entry resting on the :incoming tier into the bounded active window (admit-active), and return the list of entry names admitted this pass. The active window bounds how many admit; an entry refused at capacity (admit-active NIL) stays on :incoming for the next pass. This is the standing promotion driver — the runtime that pulls landed mail forward, replacing the test-only single admit.
run-mail-delivery-once
(run-mail-delivery-once store head &key adapter resolver)
Run ONE standing delivery cycle over STORE/HEAD with no manual admit / deliver / drain step, and return a plist summarizing it: (:promoted <names> :local <alist> :drained <dispositions-or-NIL>).
- promote-incoming-once moves :incoming entries into the active window;
- deliver-local-once delivers :local-deliver recipients into the owner Maildir and frees their slots;
- drain-active-once forwards the remaining :relay + :secondary-relay entries through ADAPTER over RESOLVER — but ONLY when a mail adapter is registered. With none (the outbound c3po client is a later chunk) the outbound pass is skipped and :drained is NIL, so local delivery still flows end-to-end.
This is the no-manual-pass unit of work the standing loop calls on an interval; a test calls it directly to prove promote+deliver+drain in a single synchronous pass.
start-mail-delivery
(start-mail-delivery store head &key adapter resolver (interval *mail-delivery-interval-seconds*))
Begin the standing delivery loop over STORE/HEAD and return the loop thread.
Single-owner track-then-unwind, mirroring START-MAIL-DRAIN: an already-running loop is a benign no-op (returns the live thread); otherwise the running bit is set, the loop thread is spawned, and on any failure spawning it the running bit is torn back down so a failed start leaves MAIL-DELIVERY-RUNNING-P NIL. The loop runs run-mail-delivery-once every INTERVAL seconds; the outbound leg lights up when a mail adapter is registered (skipped until then).
stop-mail-delivery
(stop-mail-delivery)
Stop the standing delivery loop, clearing the single-owner running state and joining the loop thread. Idempotent: stopping when nothing runs is a benign no-op. The clear is error-isolated so a teardown fault cannot strand the running flag set. Returns no values.
Variables
*mail-delivery-interval-seconds*
Seconds the standing delivery loop sleeps between cycles. A quiet default: the loop is a safety net that promotes and delivers landed mail without an owner pass, not a low-latency path (a fresh submission is drained promptly by the owner's /mail/ctl drain when immediacy matters).
Package valis/src/mail/drain
Functions
drain-active-once
(drain-active-once store head &key adapter resolver)
Run a single drain step over STORE/HEAD and return the recorded dispositions.
Reads the active tier once and, for each entry, records the resulting disposition via RELEASE-SLOT — the queue's fenced recorder, which owns the head advance; the driver re-implements no head CAS. The disposition keyword maps 1:1 onto release-slot's outcome (:delivered / :bounced terminal, freeing the active slot; :deferred relocated for retry).
RESOLVER is the injected resolver seam and is the ONLY path to delivery. When non-nil, the entry's recipient MX is resolved through runciter and only a deliverable outcome reaches the adapter; a permanent failure or null-MX bounces and a transient failure defers, all decided before the adapter call. When NIL (the resolver unconfigured or runciter absent) the relay entry DEFERS fail-closed — the static smarthost fallback is retired, so an unresolved entry is never handed to the adapter and never delivered over an unresolved path.
Per-step error isolation: the deliver (and, on the resolver path, the resolve) funcall is wrapped so a signalling thunk or resolver is recorded as a transient :deferred rather than stranding the entry in :active — one wedged adapter or resolver frees the slot and lets the entry retry. Returns the list of recorded dispositions, in active-tier order.
mail-drain-running-p
(mail-drain-running-p)
Return T while a drain lifecycle is active, NIL otherwise. The read accessor twin of EDGE-RUNNING-P; safe to call before the lifecycle is implemented.
start-mail-drain
(start-mail-drain store head &key adapter resolver)
Begin the drain lifecycle over STORE/HEAD through ADAPTER and return the dispositions recorded by the initial drain pass.
Twins START-EDGE's track-then-unwind discipline. The adapter is resolved BEFORE the running state is marked, so a misconfigured start never leaves a phantom running flag. Each acquired resource (here, the single-owner running bit) is tracked into a local; the body runs under UNWIND-PROTECT, and on any failure before the lifecycle is fully up the acquired state is torn down — in reverse, each step error-isolated — so a failed start leaves MAIL-DRAIN-RUNNING-P NIL.
The drain runs a single-shot pass this milestone (a live loop thread is deferred); the bounded active window already ships, so one DRAIN-ACTIVE-ONCE pass is the testable unit of work.
stop-mail-drain
(stop-mail-drain)
Stop the running drain, clearing the single-owner running state. Twins STOP-EDGE's idempotent teardown: calling it when no drain is running is a benign no-op, never an error — the clear is error-isolated so a teardown fault cannot strand the running flag set. Returns no values.
Package valis/src/mail/local-deliver
Functions
default-insertion-point
(default-insertion-point rcpt instruction)
The default pre-delivery insertion point: PASS-THROUGH. Returns INSTRUCTION unchanged for any RCPT — it diverts nothing, drops nothing, reads no body content, and exposes no new authority. A future processing/classification/forward handler rebinds delivery-insertion-point to return a different instruction (deliver | divert | drop | forward) within the owner's authorized view; this default is the deliver path.
local-deliver
(local-deliver store head state)
Deliver a :local-deliver-classified transport-state STATE over STORE and the fenced HEAD. Derives the target owner DID from STATE's envelope recipient (local-recipient-did), resolves the delivery instruction for that recipient through the resolver seam (default = the maildirchild write into inbox/<did>/new/), and runs the instruction. Returns the instruction's result (the new head generation on the default path).
The runtime caller (the router's :local-deliver disposition feeding this seam) is exercised live in a later phase; here the seam and its default branch are directly invocable. Fails closed as transport-queue-corrupt when the recipient does not resolve to a local owner DID — a non-local recipient must never reach a local inbox.
local-recipient-did
(local-recipient-did rcpt)
The owner DID that local recipient RCPT delivers into, or NIL when RCPT is not local or no owner identity is configured. Confirms locality through the router's recipient-local-p (the SAME locality resolution routing used, so the target DID derives from the classified recipient, not from body content), then resolves to the owner DID through local-delivery-did-resolver. Fails closed (NIL) on a non-local recipient or an unconfigured resolver.
maildir-child-deliver
(maildir-child-deliver store head state did)
The default delivery instruction: land STATE as a :delivered record into inbox/<did>/new/ over STORE and the fenced HEAD — the qmail maildirchild write. Re-stamps the record :delivered, names it by its content identity (entry-name body-score envelope-rcpt — the SAME shape the inbox node enumerates and the queue lands by), encodes it with encode-transport-state, splices it into the DID's new/ subtree off a fresh head read, and commits with a single advance-head at the read generation — the link() analog, the COMMIT POINT.
Returns the new head generation, reported ONLY after advance-head returns success (ack-after-fsync = qmail's _exit(0)-after-link). A delivered message is never partial: a lost CAS (a coincident writer advanced the head) FAILS CLOSED as transport-queue-corrupt and nothing landed — there is no half-applied splice. Lands ONLY into new/ (delivered-unseen); the new->cur read-state move belongs to the inbox node, never this writer. Introduces no new durable store or tier.
Idempotent across the read-state boundary: a re-land of the same content to the same recipient collapses onto its prior entry rather than duplicating. Within new/ the splice overwrites in place. When the owner has already READ the prior delivery — the inbox node moved it new->cur — a queue retry must NOT resurface it as unseen: a content-addressed redelivery is the same message the owner already read, so landing a fresh new/ entry would make the SAME (body, recipient) appear in both new/ (unseen) and cur/ (seen). This delivery therefore no-ops when the content's bare name is already present in cur/, returning the current head generation unchanged — the established read-state is preserved and no duplicate is created.
parse-recipient-extension
(parse-recipient-extension rcpt)
Parse RCPT (an RFC5321 recipient 'local@domain') into (values base extension): the local part is split at the FIRST '-' into a base mailbox and an optional extension — the qmail .qmail-ext convention (dot-qmail.5). A local part with no '-' yields a NIL extension. BASE is always a string; EXTENSION is a string or NIL. INBOUND-ONLY: the submission outbox passes its derived recipients through unchanged and never calls this; the extension is parsed here for the resolver key but resolves to the Maildir default this phase (no extension handler ships).
register-delivery-instruction
(register-delivery-instruction key instruction)
Register INSTRUCTION (a function of (store head state did)) under KEY in the handler table. IDEMPOTENT: re-registering the same key replaces the instruction. This is the forward seam a deferred handler phase installs onto; no caller ships now. Returns INSTRUCTION.
resolve-delivery-instruction
(resolve-delivery-instruction rcpt)
Return the delivery instruction for envelope recipient RCPT. Parses the extension off the local part and looks the recipient up in delivery-instruction-handlers longest-match first (the local-part-with-extension, then the bare base mailbox) — the .qmail-ext resolution analog. With NO registered handler (the empty / unmatched case) it returns maildir-default-instruction (the maildirchild write). The resolved instruction is then passed through the pre-delivery insertion point (delivery-insertion-point, a no-op = deliver this phase) before being returned. Pure resolution: no store access, no I/O.
unregister-delivery-instruction
(unregister-delivery-instruction key)
Remove the instruction registered under KEY. Returns T if one was present.
Variables
*delivery-insertion-point*
The pre-delivery insertion-point hook: a function of (rcpt instruction) the resolver calls before returning the final delivery instruction. Defaults to default-insertion-point (pass-through = deliver). This is the forward-compatible surface for deferred message-processing/classification + forward handlers; it ships as a NO-OP this phase and MUST, when a concrete handler is added, operate only within the owner's authorized view and never weaken the upstream open-relay guard or the inbox sovereignty guarantees.
*delivery-instruction-handlers*
Recipient-key -> delivery instruction. The .qmail-ext handler table, EMPTY this phase — every recipient resolves to maildir-default-instruction. A key is the extension-aware recipient material a future handler matches on (the local-part-with-extension or the bare base mailbox); the value is an instruction function of (store head state did). A deferred phase populates this with concrete extension/classification/forward handlers; valis ships none.
*local-delivery-did-resolver*
SEAM: resolves a local recipient address to the owner DID whose inbox subtree its mail is keyed under. v1.4 single-identity: every local recipient resolves to the one owner DID (keyed under a DID even with one identity). The fabric binds this at startup from custody (custody-owner-did); the default returns NIL — no identity configured, so local-deliver fails closed. This mirrors the router's config-special idiom (local-domains) and keeps the writer out of the identity import graph. INBOUND-ONLY: never consulted on the outbox path.
*maildir-default-instruction*
The default delivery instruction the resolver returns on empty/unmatched resolution: the maildirchild write into inbox/<did>/new/. An instruction is a function of (store head state did) that performs the delivery and returns its result. Rebind to swap the default delivery behavior wholesale; a concrete per-recipient handler is registered onto delivery-instruction-handlers instead.
Package valis/src/mail/rcpt-disposition
Classes
callout-cache
A bounded-TTL cache of RCPT-callout verdicts keyed by (primary . recipient), so a burst of RCPTs to the same recipient costs one probe and a cached "primary down" short-circuits to accept-and-queue without re-probing every message. TABLE maps the key to (verdict . expiry-universal-time); TTL-SECONDS is the entry lifetime. Both positive and negative verdicts are cached — the point is to cap the dictionary-probe amplification a naive per-RCPT callout would create.
Functions
callout-cache-p
(callout-cache-p object)
Undocumented: this exported symbol needs a docstring.
make-callout-cache
(make-callout-cache &key (ttl-seconds 300))
Construct a fresh RCPT-callout verdict cache with entries living TTL-SECONDS. Pass one into rcpt-disposition to memoize primary-MX callout verdicts across a message burst; omit it (NIL) to probe on every callout.
rcpt-disposition
(rcpt-disposition rcpt &key authenticated-submit-p callout-probe callout-cache)
The RCPT-stage decision a wire SMTP engine calls at RCPT TO time. Return four values:
VERDICT one of :accept / :reject / :defer; CODE the SMTP reply code the engine sends (250 / 550 / 451); REASON a short human reason string for the reply text / log; ROLE the domain role decided — :local-deliver / :secondary-relay / :relay / :refuse-open-relay — for the engine's telemetry and the land-time path.
The engine translates VERDICT to the wire: :accept -> 250 (proceed to DATA), :reject -> the 5xx CODE (no DATA — anti-backscatter: nothing accepted to bounce), :defer -> the 4xx CODE (try later — the fail-closed-to-defer posture on any uncertainty).
AUTHENTICATED-SUBMIT-P is the engine's own upstream verdict that this session is an authenticated owner-rooted submission (SASL/capability on :587/:465). When true a FOREIGN recipient accepts (the owner may relay anywhere); a local or secondary recipient still follows its own role policy.
CALLOUT-PROBE is the injected outbound RCPT probe used for the secondary role: a function of (primary-mx recipient) returning {:accept :reject :unreachable :defer} by speaking connect / EHLO / MAIL FROM / RCPT TO to the primary and stopping before DATA. CALLOUT-CACHE, when supplied (make-callout-cache), memoizes those verdicts across a burst. Neither is consulted for the local or foreign roles.
Pure over its inputs plus the injected probe: it reads only the config classifiers and the owner-DID locality resolver, and names no key/custody/store-write symbol.
Variables
*local-accept-localparts*
The reject-unknown ACCEPT-LIST for primary-local (local-domains) recipients: the base localparts that accept. An unknown localpart 5xx-rejects at RCPT (no pure catch-all — a catch-all accepts-then-bounces and a bounce to a forged sender is backscatter, design §5.1). Matched CASE-INSENSITIVELY against the BASE mailbox (the localpart with any qmail-style '-extension' stripped), so fade-newsletter@ accepts under fade. Every named localpart at every local-domains domain collapses to the single owner Maildir (the alias question is resolved by the single-identity DID resolver), so this is a membership policy, not an alias table. The fabric threads the operator's configured list here; the default is the six names the operator requires across the go-live domains.
Package valis/src/mail/relay
Functions
relay-disposition
(relay-disposition resolver rcpt deliver-thunk entry-name)
Resolve RCPT's recipient MX through RESOLVER and map runciter's outcome onto a release-slot disposition (:delivered / :deferred / :bounced).
DOMAIN is the RFC5321 envelope domain only — the substring of RCPT after the last #\@; no RFC5322 header/body input enters resolution. Only :delivered-targets reaches the wire: DELIVER-THUNK is called with ENTRY-NAME and the MX-preference-ordered TARGETS, and its return (itself one of the three dispositions) is returned — so a relay attempt that transiently fails defers. The three failure/terminal outcomes are decided here without a DELIVER-THUNK call: :deferred-retryable requeues (never lost), :rejected-permanent and :no-mail bounce. ECASE (not CASE) on the outcome so an unknown keyword signals rather than silently defers — the resolver vocabulary is a closed contract, and drift must surface, not be masked.
Package valis/src/mail/resolver-seam
Functions
build-mail-resolver
(build-mail-resolver &key (host *mail-resolve-dot-host*) (port 853) (adn *mail-resolve-dot-adn*) (ca-file *mail-resolve-ca-file*) (connect-timeout *mail-resolve-connect-timeout-seconds*) (read-timeout *mail-resolve-read-timeout-seconds*) budget cache)
Construct the outbound mail MX resolver from operator config and return it, or NIL when no resolver can (or should) be built — in which case the drain defers every relay entry (never a smarthost, never a delivery over an unauthenticated channel).
Returns NIL fail-closed when either:
- HOST is NIL (no DoT upstream configured), or
- runciter's curated make-mail-resolver is absent (its real-wire system is not loaded), so valis has nothing to build the resolver from.
Otherwise late-resolves runciter's make-mail-resolver and calls it with the configured HOST/PORT/ADN, the trust anchor CA-FILE (a NIL ca-file is an EMPTY trust store — fail-closed, not a bypass), and the two per-operation deadlines so a hung upstream defers rather than wedging the drain. BUDGET and CACHE are passed through when supplied; the resolver refreshes its per-call budget internally, so one instance is correct across many deliveries.
Variables
*mail-resolve-ca-file*
The sole TLS trust anchor (a CA bundle path) handed to mercer for the DoT peer. NIL means an EMPTY trust store — mercer then rejects every peer closed, never a bypass — so mail defers until an operator names a trust anchor. The empty-trust-store default is the fail-closed posture, not an oversight.
*mail-resolve-connect-timeout-seconds*
Seconds the outbound DoT connect may wait before the mail resolver defers. Bounds the SYN wait so a black-holed upstream defers rather than wedging the drain thread.
*mail-resolve-dot-adn*
The authentication-domain-name of the DoT upstream — the SNI + RFC6125 verify name mercer authenticates the peer certificate against. NIL when unconfigured.
*mail-resolve-dot-host*
The DoT upstream host valis resolves recipient MX through. NIL until an operator configures it; a NIL host yields no resolver, so the drain defers every relay entry (fail-closed default — there is no smarthost fallback).
*mail-resolve-read-timeout-seconds*
Seconds any single socket read (the TLS handshake and the DNS answer read) may wait before the mail resolver defers. Set as SORCVTIMEO on the borrowed fd, so a peer that connects then goes silent surfaces an I/O-timeout the resolver maps to a DEFER.
*mail-resolver-package-name*
The runciter package that curates the live-DoT mail resolver surface (make-mail-resolver + resolve-mail-exchangers). A defparameter so a test can redirect it to a stand-in package.
Package valis/src/mail/router
Conditions
mail-open-relay-refused
Signalled fail-closed when a non-local recipient is presented for relay without an authenticated owner-rooted submission verdict in its provenance. The open-relay guard refuses such a recipient — it never silently forwards. A hard fault at the fan-out boundary; the pure classifier route-recipient reports the same condition as a :refuse-open-relay decision.
Generic functions
mail-open-relay-refused-reason
(mail-open-relay-refused-reason condition)
Undocumented: this exported symbol needs a docstring.
Functions
recipient-local-p
(recipient-local-p rcpt)
Return T when RCPT (an RFC5321 envelope recipient string) is locally delivered — i.e. its domain (the part after the last #\@) is a member of the configured local-domains set (case-insensitive). This is the v1.4 locality seam: a config lookup now, the :names service later. The owner Ed25519 DID (valis/src/identity/custody:custody-owner-did) is the anchor of authority this config speaks for — locality ultimately means "delivered to the owner" — but v1.4 decides locality from local-domains because no address->DID map exists yet, and the router never grows names logic. Pure: an in-memory config read only, no disk/DNS/identity I/O.
recipient-secondary-p
(recipient-secondary-p rcpt)
Return T when RCPT (an RFC5321 envelope recipient string) is addressed to a domain valis is an AUTHORIZED backup MX (secondary) for — i.e. its domain (the part after the last #\@) is a key in the secondary-for alist (case-insensitive). This is the secondary-role classification the router's :secondary-relay disposition turns on; it is authorized relay, never open relay — the authority is presence in secondary-for. Pure: an in-memory config read only, no disk/DNS/identity I/O.
route-message
(route-message envelope-from from-null-p rcpt-list body-score provenance)
Fan a message addressed to RCPT-LIST into a list of independent per-recipient transport-state records, one per recipient, ALL sharing the single content-addressed BODY-SCORE. Each recipient is classified by route-recipient: a :local-deliver, :secondary-relay, or :relay recipient yields a record carrying the shared BODY-SCORE, the envelope (ENVELOPE-FROM / FROM-NULL-P / the recipient as ENVELOPE-RCPT), the recorded PROVENANCE, and a seeded max-hops (the loop-detection ceiling, defaulted by the constructor). The records share one body but differ in ENVELOPE-RCPT, so when the queue lands them it derives a distinct content-addressed identity per recipient from (BODY-SCORE, recipient) — the router produces the per-recipient records; it does not compute that identity.
Fail-closed at this boundary: a recipient the open-relay guard refuses (:refuse-open-relay) SIGNALS mail-open-relay-refused — it is never emitted as a forwardable record (BLOCKING).
This produces records only; it does NOT land them (no place-incoming call) — the caller lands each, keeping the router I/O-free.
route-recipient
(route-recipient rcpt provenance)
PURE classifier: return the routing decision for envelope recipient RCPT given its recorded PROVENANCE verdict — one of:
:local-deliver when RCPT is local (recipient-local-p); :secondary-relay when RCPT is addressed to a domain valis is an authorized backup MX (secondary) for (recipient-secondary-p) — AUTHORIZED relay, not open relay: the authority is the domain's presence in secondary-for. A secondary entry is forwarded UNMOLESTED to the domain's primary MX downstream; :relay when RCPT is non-local AND PROVENANCE is an authenticated owner-rooted submission — a bare keyword: the resolver owns MX selection downstream, so the router names no next-hop target; :refuse-open-relay otherwise — a non-local recipient whose PROVENANCE is absent, ill-typed, or not an authenticated submission (the BLOCKING open-relay guard).
The decision derives only from RCPT (an RFC5321 envelope recipient) and the already-recorded PROVENANCE — never from any RFC5322 header/body input, so a forged sender cannot influence it. Default-deny is structural AND fail-closed on contract: the relay branch is reachable only when PROVENANCE is an actual mail-provenance whose authenticated-submit-p is T, so peer-landed mail (authenticated-submit-p NIL) — and a nil or otherwise ill-typed PROVENANCE — both fall through to :refuse-open-relay rather than raising a type-error. There is no allow-relay flag. No store handle, no I/O.
secondary-primary-mx
(secondary-primary-mx rcpt)
Return the configured PRIMARY MX host for RCPT's secondary domain (the value paired with the domain in secondary-for), or NIL when RCPT is not addressed to a configured secondary domain. This is the forward target a :secondary-relay entry is delivered to and the host the RCPT-callout probes — the domain's own primary, never valis itself. Pure: an in-memory config read only.
Variables
*local-domains*
The configured set of locally-delivered domains/addresses (a list of strings), the v1.4 answer behind the recipient-local-p seam — locality is a config lookup now, the :names service later. Empty by default: with no configured local domain every recipient is non-local, so default-deny holds.
*secondary-for*
The authorized backup-MX (secondary) authority: an ALIST mapping each domain valis is a listed secondary for to that domain's PRIMARY MX host — e.g. '(("example.com" . "203.0.113.10")). Empty by default: with no entry a recipient domain is never classified :secondary-relay, so default-deny holds.
This is a config/capability list, NOT an allow-relay flag: the router permits the :secondary-relay disposition ONLY because the recipient's domain is a key here, exactly as it permits :relay only when an authenticated-submit capability is present. A domain valis is neither primary (in local-domains) nor secondary (here) for still hits :refuse-open-relay. The value is the domain's PRIMARY MX — the higher-priority target that is not us — which the outbound forward targets and the RCPT-callout probes; a store-and-forward secondary validates and forwards to that primary, never to itself (see the design-of-record §5.1).
Package valis/src/mail/seam
Classes
mail-receipt
The acknowledgement land-message returns once a received message is durably ingested. BODY-SCORE is the content-address (a store score) of the opaque body octets, shared by every per-recipient record. PLACEMENTS is a list describing where each per-recipient transport-state landed on the queue, one element per recipient route-message fanned the message into — a plist (:rcpt :entry-name :generation) naming the recipient, the queue entry name, and the head generation the place-incoming advance acked at.
Functions
authenticated-submit-provenance
(authenticated-submit-provenance submitter-did)
The relay-authorizing provenance for an AUTHENTICATED owner-rooted submission — the :587/:465 submission engine or the owner /mail/outbox path — naming the verified SUBMITTER-DID (the audience DID of the owner-rooted chain; supports the delegated-submit path). authenticated-submit-p is T, so the router permits :relay to ANY foreign domain. A wire engine passes this to LAND-MESSAGE ONLY after it has verified the SASL/capability-authenticated owner-rooted submit UPSTREAM of the landing door — the seam trusts the recorded verdict, it does not re-verify a token.
land-message
(land-message store head body-octets envelope-from from-null-p rcpt-list provenance)
Ingest a received message into the substrate and return a MAIL-RECEIPT.
STORE/HEAD are the content-addressed block store and its fenced head. BODY-OCTETS is the opaque RFC5322 body. ENVELOPE-FROM / FROM-NULL-P / RCPT-LIST are the RFC5321 envelope; PROVENANCE is the recorded submission provenance.
Three steps over the existing spine: (1) content-address the opaque body into the store, yielding the single shared body score; (2) ROUTE-MESSAGE classifies and fans the message into one independent per-recipient transport-state per recipient, all sharing that one body score — it owns the open-relay guard and may signal MAIL-OPEN-RELAY-REFUSED, which propagates here unmolested (never caught-and- allowed, never a silent forward); (3) PLACE-INCOMING lands each per-recipient record on the incoming tier, ack-after-fsync, collecting one placement per recipient into the receipt. land-message re-decides no policy and mutates no state beyond these spine calls.
mail-receipt-body-score
(mail-receipt-body-score instance)
Undocumented: this exported symbol needs a docstring.
mail-receipt-p
(mail-receipt-p object)
Undocumented: this exported symbol needs a docstring.
mail-receipt-placements
(mail-receipt-placements instance)
Undocumented: this exported symbol needs a docstring.
peer-landed-provenance
(peer-landed-provenance)
The DEFAULT-DENY provenance a wire adapter stamps on PEER-LANDED inbound mail — mail arriving over an unauthenticated hostile channel (the :25 SMTP receive leg). authenticated-submit-p is NIL, so the router's open-relay guard refuses any recipient that is neither primary-local (local-domains) nor a domain valis is an authorized secondary for (secondary-for): a foreign recipient lands NOTHING. This is the provenance c3po's :25 engine passes to LAND-MESSAGE for every peer message — the engine names it here rather than reaching into the transport-state codec, keeping the store-cluster import firewall intact on the wire side.
Package valis/src/mail/transport-queue
Conditions
transport-queue-at-capacity
Signalled fail-closed when a transition into the bounded active window would exceed its bound. BACKPRESSURE, not corruption: an EXPECTED refusal that keeps the active disposition at or below its cap regardless of which entry point drives the transition. A SEPARATE condition from transport-queue-corrupt so a caller can distinguish 'the queue is full, wait and retry' from 'the queue is structurally broken'. admit-active catches this and returns its NIL at-capacity sentinel.
transport-queue-corrupt
Signalled fail-closed when a transport-queue operation cannot complete: an entry required to be present is absent, a per-row CAS lost the update (a coincident legitimate writer advanced the row), or a tier/disposition mapping is unknown. A hard fault, never auto-repair, never a partial transition.
Generic functions
transport-queue-at-capacity-bound
(transport-queue-at-capacity-bound condition)
Undocumented: this exported symbol needs a docstring.
transport-queue-at-capacity-tier
(transport-queue-at-capacity-tier condition)
Undocumented: this exported symbol needs a docstring.
transport-queue-corrupt-reason
(transport-queue-corrupt-reason condition)
Undocumented: this exported symbol needs a docstring.
Functions
admit-active
(admit-active store head entry-name)
Admit the entry named ENTRY-NAME into the bounded active window over STORE: a fence-asserted per-row CAS moving its disposition (incoming first, then deferred) to "active", but ONLY if the active count is below active-window's bound. The count-check-then-admit runs under queue-lock so two contending in-image admits cannot both push past the bound (the table is the durable count; the lock is the single-image serialization).
At the bound, admission is REFUSED fail-closed: returns NIL (the at-capacity sentinel), the entry stays where it is. A terminal-resting row (delivered/bounced) is STRUCTURALLY ineligible — a freed slot pulls the next PENDING entry, never completed work — checked before mail-admission-predicate (the retry-timing seam, default always-eligible). Returns the instance generation on a successful admit; NIL when refused at cap, already active, terminal, ineligible, or absent.
advance-tier
(advance-tier store head from-tier to-tier entry-name expected-generation)
Move the entry named ENTRY-NAME to TO-TIER via a fence-asserted per-row CAS on the disposition column over STORE. The record is copied forward over the SAME bodyptr — the body never moves; only the disposition column advances.
Two preconditions are enforced fail-closed. A TERMINAL entry (a "delivered" or "bounced" record resting in the deferred-tier set) is REFUSED: completed work is never advanced back out of its terminal disposition, so a stale caller cannot resurrect it into the active or deferred set and risk a duplicate delivery. When TO-TIER is :active the active-window bound is enforced HERE, over the PG active count, failing closed with transport-queue-at-capacity (BACKPRESSURE) — so EVERY path into the active disposition respects the cap, not only admit-active. The :active cap-check and the CAS run together under queue-lock, matching admit-active, so two in-image admits cannot both observe room below the bound and both admit (advance-tier is never itself called from inside a held queue-lock).
A lost per-row CAS fails closed as transport-queue-stale-commit; an absent entry fails closed as transport-queue-corrupt. HEAD and EXPECTED-GENERATION are accepted for the unchanged seam signature; the fence generation is read live from the store head and the per-row CAS reads the row's own generation.
entry-name
(entry-name body-score rcpt)
Return the deterministic queue-entry NAME string for a message BODY-SCORE (a 32-byte content score) addressed to RCPT (the envelope recipient string). The name is the body score hex-encoded, a "-" separator, then the SHA-256 of the recipient's UTF-8 octets hex-encoded — filesystem-safe, bounded-length, and the SAME body to the SAME recipient always produces the SAME name (the at-least-once + idempotent-landing identity). PURE: same inputs yield string= results, no side effects, no store access. Fails closed as transport-queue-corrupt when BODY-SCORE is not a 32-byte vector.
migrate-fs-queue-to-pg
(migrate-fs-queue-to-pg store head)
The one-time FS->PG drain: quiesce the legacy FS queue under queue-lock, read every FS entry's state via the legacy decoder, and insert ALL rows into PG in ONE transaction — a single pass, no dual-read window. Each entry lands idempotently via place-mail-state (incoming), then advances to its actual disposition for an active/deferred/terminal entry; body blocks are untouched (opaque on the content store). Idempotent: a re-run collapses onto the existing rows. HEAD is accepted for signature symmetry; the generation is read live. Returns the number of FS entries imported.
name->tier
(name->tier name)
Map a tier name string back to its keyword, failing closed on an unknown name.
place-incoming
(place-incoming store head state)
Land transport-state STATE into the "incoming" disposition of the PG mail-state table over STORE (the head HEAD is accepted for the unchanged seam signature; the generation is read live from the store head). Names the entry by its body score + recipient, asserts the instance fence, and inserts the row idempotently (:on-conflict-do-nothing — a re-land of the same body to the same recipient collapses onto the existing row). The body is NOT touched here: it was already content-addressed to the store by land-message and is named only by bodyptr. Returns the instance generation ONLY after the PG row commits — durable-before- acknowledge at the PG commit. Fails closed (fenced-out) if this instance's write-epoch has been superseded.
queue-entries
(queue-entries store head tier)
List the entry-name strings whose PG disposition belongs to TIER, over STORE (HEAD accepted for the unchanged signature). An empty tier yields the empty list.
read-entry-state
(read-entry-state store head tier entry-name)
Recover the in-memory transport-state of the entry named ENTRY-NAME whose PG disposition belongs to TIER, or NIL when no such row is present in that tier. Built from the PG row (make-transport-state + accessors); bodyptr unchanged, no body read. This is the public reader the drain and the projections use.
release-slot
(release-slot store head entry-name &optional (outcome :delivered))
Record a drained entry's OUTCOME by advancing its disposition out of "active" — freeing exactly one active slot — then PULL the next eligible waiting entry via admit-active (the freed-slot re-admit).
OUTCOME drives the disposition: :deferred a TRANSIENT outcome: disposition advances to "deferred" for a later retry. :delivered / :bounced a TERMINAL outcome: disposition advances to the terminal keyword, resting in the deferred-tier set for audit.
The record is copied forward over the SAME bodyptr — the body never moves and nothing is mutated in place. Returns the instance generation of the move-out. A lost per-row CAS fails closed as transport-queue-corrupt; an absent entry fails closed.
tier->name
(tier->name tier)
Map a tier keyword to its name string, failing closed on an unknown tier.
Variables
*mail-with-connection*
The connection seam the mail seam runs its PG work through. In production it wraps with-operator-state-connection (acquire-late / release-early on the worker thread); a PG acceptance test binds it to a pass-through that runs the thunk on the ambient disposable-schema / rolled-back connection, so the same seam path the operator drives is exercised end to end.
+tier-names+
The pinned tier keyword<->name map. In the PG model a tier is the `disposition` column value; this map remains the canonical tier vocabulary the operator projection iterates and the drain walks the legacy FS tree by.
Package valis/src/mail/transport-state
Classes
mail-provenance
The opaque provenance / Authentication-Results verdict recorded onto a transport-state — the data the open-relay guard reads. AUTHENTICATED-SUBMIT-P is the load-bearing bit: T only when an upstream write boundary verified an owner-rooted, outbox-covering submission; NIL (the default) is structural default-deny, so peer-landed inbound mail carries no authority and cannot relay. SUBMITTER-DID is the audience DID of the verified owner-rooted chain ("" when absent), supporting the delegated-relay path. AUTH-RESULTS is a RESERVED list — the RFC 8601 Authentication-Results slot the post-v1.4 mail-auth subsystem (SPF/DKIM/DMARC) will fill; carried, not populated, today.
Stored as plain Lisp data ONLY (a flag, a string, a list) — NEVER a live capability or identity object. This keeps the store-cluster import firewall intact: the codec records a verdict, the router reads a verdict; neither holds a verifiable token.
transport-state
A decoded transport-state record: the mutable queue state around a content-addressed message body. DISPOSITION is the lifecycle keyword; ATTEMPTS the delivery-attempt count; NEXT-ATTEMPT and RECEIVED-AT are u64 timestamps; STATUS-CLASS / STATUS the SMTP result; ENVELOPE-FROM / ENVELOPE-FROM-NULL-P / ENVELOPE-RCPT the SMTP envelope; BODY-SCORE the 32-byte content score of the message body (opaque — this codec records it, the queue computes it). PROVENANCE is the opaque mail-provenance verdict the open-relay guard reads, defaulting to absent (authenticated-submit-p NIL — structural default-deny). MAX-HOPS is the loop-detection hop ceiling carried with the record.
Conditions
transport-state-corrupt
Signalled fail-closed when a transport-state record is structurally invalid: a bad envelope width, an unknown version, an unknown disposition/status-class byte, a malformed detail stream, or a referenced body/detail stream block that is missing (block-not-found) or corrupt (block-corrupt) at read time. A hard fault, never auto-repair, never a partial record. Byte integrity is provided by the store's content addressing (read-block re-hashes on every read); this condition covers STRUCTURAL failure only — there is no record-level integrity digest.
Generic functions
transport-state-corrupt-reason
(transport-state-corrupt-reason condition)
Undocumented: this exported symbol needs a docstring.
Functions
byte->disposition
(byte->disposition byte)
Map a disposition envelope byte to its keyword, failing closed on an unknown byte (a corrupt/forward-incompatible record).
byte->status-class
(byte->status-class byte)
Map a status-class envelope byte to its keyword, failing closed on an unknown byte.
decode-transport-state
(decode-transport-state store envelope-entry)
Invert encode-transport-state: read the envelope named by ENVELOPE-ENTRY, recover every field, and reconstruct the in-memory transport-state. Any block read failure (a missing or corrupt body/detail stream) or structural mismatch fails closed as transport-state-corrupt — never a partial record. The body score is recovered from the embedded body entry; the body bytes themselves are not read here (they are content-addressed and named, not embedded).
disposition->byte
(disposition->byte disposition)
Map a disposition keyword to its pinned envelope byte, failing closed on an unknown disposition.
encode-transport-state
(encode-transport-state store state)
Encode STATE into STORE and return the envelope's content-addressed ENTRY. The variable strings are packed into a framed detail stream and encode-filed, yielding the detail entry; the body is named by a synthetic depth-0 entry carrying STATE's already-stored body score. The envelope then names both entries. An absent or wrong-width body score is a structurally invalid record and fails closed as transport-state-corrupt before anything is written. Durable-before-returned (encode-file flushes before the score is returned).
mail-provenance-auth-results
(mail-provenance-auth-results instance)
Undocumented: this exported symbol needs a docstring.
mail-provenance-authenticated-submit-p
(mail-provenance-authenticated-submit-p instance)
Undocumented: this exported symbol needs a docstring.
mail-provenance-p
(mail-provenance-p object)
Undocumented: this exported symbol needs a docstring.
mail-provenance-submitter-did
(mail-provenance-submitter-did instance)
Undocumented: this exported symbol needs a docstring.
make-mail-provenance
(make-mail-provenance &key (authenticated-submit-p nil) (submitter-did "") (auth-results (quote nil)))
Undocumented: this exported symbol needs a docstring.
make-transport-state
(make-transport-state &key (disposition :incoming) (attempts 0) (next-attempt 0) (received-at 0) (status-class :none) (status "") (envelope-from "") (envelope-from-null-p nil) (envelope-rcpt "") (body-score nil) (provenance (make-mail-provenance)) (max-hops +default-max-hops+))
Construct a transport-state value. See the struct docstring for slot meaning. BODY-SCORE is a 32-byte content score, stored opaque. PROVENANCE defaults to an absent mail-provenance (authenticated-submit-p NIL — structural default-deny); MAX-HOPS defaults to the loop-detection hop ceiling.
pack-transport-envelope
(pack-transport-envelope version disposition-byte attempts next-attempt received-at status-class-byte body-entry detail-entry)
Return a fresh transport-state-envelope-size octet vector: the canonical
transport-state envelope naming the body and detail streams. Version-first, then
the scalar disposition/attempt/timestamp/status-class fields, then the body entry
and the detail entry. The exact-width assert proves no slack. NO integrity
digest — the envelope block's content address authenticates the bytes.
status-class->byte
(status-class->byte status-class)
Map a status-class keyword to its pinned envelope byte, failing closed on an unknown class.
transport-state-attempts
(transport-state-attempts instance)
Undocumented: this exported symbol needs a docstring.
transport-state-body-score
(transport-state-body-score instance)
Undocumented: this exported symbol needs a docstring.
transport-state-disposition
(transport-state-disposition instance)
Undocumented: this exported symbol needs a docstring.
transport-state-envelope-from
(transport-state-envelope-from instance)
Undocumented: this exported symbol needs a docstring.
transport-state-envelope-from-null-p
(transport-state-envelope-from-null-p instance)
Undocumented: this exported symbol needs a docstring.
transport-state-envelope-rcpt
(transport-state-envelope-rcpt instance)
Undocumented: this exported symbol needs a docstring.
transport-state-max-hops
(transport-state-max-hops instance)
Undocumented: this exported symbol needs a docstring.
transport-state-next-attempt
(transport-state-next-attempt instance)
Undocumented: this exported symbol needs a docstring.
transport-state-provenance
(transport-state-provenance instance)
Undocumented: this exported symbol needs a docstring.
transport-state-received-at
(transport-state-received-at instance)
Undocumented: this exported symbol needs a docstring.
transport-state-status
(transport-state-status instance)
Undocumented: this exported symbol needs a docstring.
transport-state-status-class
(transport-state-status-class instance)
Undocumented: this exported symbol needs a docstring.
transport-state-to-octets
(transport-state-to-octets store state)
Serialize STATE into STORE and return the concatenation of the envelope block and the FULL reconstruction of the detail stream it names — the complete record surface, EXCLUDING the body stream (the body is named, never embedded). Walking the envelope block plus the entire decode-file reconstruction of the detail stream is what lets a byte-absence assertion prove the body's distinctive bytes appear nowhere in the record surface (mirrors module-manifest-to-octets). The envelope is one fixed-width block, so its top block IS its bytes; the detail stream is descended in full via decode-file so a depth>0 pointer tree's leaves are covered too. The body entry is deliberately NOT walked — the body stays out of the record.
unpack-transport-envelope
(unpack-transport-envelope vec)
Decode a transport-state envelope. Returns (values version disposition-byte attempts next-attempt received-at status-class-byte body-entry detail-entry). Validates width FIRST, then version, BEFORE indexing any field (verify-on-read). No integrity digest check. Fails closed with transport-state-corrupt.
Constants
+default-max-hops+
Default hop ceiling carried on a fresh transport-state: the designed-in loop-detection seam. A non-zero starting budget that a relay path decrements; exhausting it (reaching zero) is how a self-alias / forwarding loop is refused. The v1.4 logic is minimal — the field is carried and round-tripped so the seam exists; the decrement-and-refuse policy fills in later.
+transport-state-envelope-size+
Total fixed envelope width: version[2] + disposition[1] + attempts[4] +
next-attempt[8] + received-at[8] + status-class[1] + body-entry[41] +
detail-entry[41]. The body entry names the content-addressed message body; the
detail entry names a framed stream holding the variable-length envelope-from /
rcpt / status strings. DERIVED from entry-size, never a literal. NO integrity
digest field — the envelope block's content address authenticates the bytes.
+transport-state-version+
Own transport-state record version (version-first, mirroring the tree/head/ manifest codec). Bumped to 2 when the detail stream gained the provenance verdict (authenticated-submit flag + submitter DID + reserved auth-results) and the max-hops field; the version gate refuses an unknown version fail-closed.