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

  1. promote-incoming-once moves :incoming entries into the active window;
  2. deliver-local-once delivers :local-deliver recipients into the owner Maildir and frees their slots;
  3. 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.