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

Classes

mail-resolver-source

The outbound mail resolver the running node uses, held so it can be replaced. The drain and the delivery loop take CURRENT afresh for each pass, so a replaced resolver is used from the next pass on, and a pass already running finishes on the resolver it took.

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.

current-mail-resolver

(current-mail-resolver resolver)

The resolver RESOLVER stands for: the current one when RESOLVER is a mail-resolver-source, else RESOLVER itself.

mail-resolver-source-current

(mail-resolver-source-current instance)

Undocumented: this exported symbol needs a docstring.

mail-resolver-source-p

(mail-resolver-source-p object)

Undocumented: this exported symbol needs a docstring.

make-mail-resolver-source

(make-mail-resolver-source current)

Undocumented: this exported symbol needs a docstring.

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.