70.1.10. pepsi-stage-relay-to-internet¶
deliver a message directly to the recipient’s MX hosts
- Manual section:
1
70.1.10.1.1. Name¶
pepsi-stage-relay-to-internet - the direct-to-MX delivery stage of the Pepsi pipeline.
70.1.10.1.2. Synopsis¶
pepsi-stage-relay-to-internet [GLOBAL-OPTIONS] worker
pepsi-stage-relay-to-internet [GLOBAL-OPTIONS] mta-sts DOMAIN
70.1.10.1.3. Description¶
pepsi-stage-relay-to-internet is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It loads that pepsi.ingress row (refusing to
act unless its status is running), reads its [stage-<stage>] section,
and delivers the message directly to the recipient domain’s mail exchangers,
using the message’s own envelope sender (the null sender <> for bounces).
It performs its own DNS MX lookup: candidate hosts are tried in ascending MX
preference (equal-preference hosts in random order), a domain with no MX
records falls back to its address records (the implicit MX of RFC 5321 §5.1),
and a non-existent domain or RFC 7505 null MX is a permanent failure. Each MX
host’s A/AAAA addresses are selected with Happy-Eyeballs and cached, per address,
in the dns_address table; the ADDRESS_FAMILY setting (intersected with
the families the host can route) chooses which to use. Connections are made on
port 25. Every query is made for the fully-qualified name, so a search list
in /etc/resolv.conf is never applied: without that, a domain with no MX
records would be looked up a second time under each search domain, and the
resulting NXDOMAIN — not the domain’s own NODATA — would be reported,
turning an ordinary implicit-MX delivery into a permanent failure.
Unless disabled with MTA_STS = no, the recipient domain’s MTA-STS policy
(RFC 8461) is honoured: an enforce policy restricts delivery to the listed MX
hosts over STARTTLS with a certificate that validates for the host, failing
the message (transient) when no MX matches. A testing policy is validated
the same way — the attempt and its outcome are recorded for TLS Reporting — but
never blocks delivery: a transport-security failure (or a policy that matches no
MX) falls back to opportunistic STARTTLS over all MX hosts, so the domain
owner learns from the reports whether enforce would break mail before
switching to it. Domains with no policy use opportunistic STARTTLS. Policy
lookups fail open only when nothing is cached.
Policies are cached for their max_age (capped at one week). The _mta-sts
TXT id is re-checked on each use of a cached policy (RFC 8461 §3.1): a changed
id triggers an immediate refetch, while a transiently unreachable record (or a
failed refetch of a changed policy) keeps the still-valid cached policy. A policy
host that returns a transient 5xx/429 is treated as “fetch failed” (fail
open, not cached as “no policy”), whereas a 404 or a non-text/plain body
is a definitive absence.
DANE (RFC 7672): when DANE is warn (the default) or strict and the
destination MX host publishes DNSSEC-validated TLSA records, the MX
certificate is authenticated against them instead of by PKIX — this takes
precedence over MTA-STS for that connection. DANE rests on DNSSEC: the configured
resolver (DNS_SERVERS, or resolv.conf) must validate and be reached
over a trusted path, because Pepsi trusts the response AD (Authentic Data) bit
rather than validating itself. TLSA (and MX) answers without the AD bit
are treated as “no DANE”. In strict mode a usable-but-mismatching record (or
a TLSA lookup that fails for a DNSSEC-bogus record) defers the message as a
transient failure; in warn mode the mismatch is logged and delivery proceeds.
A domain that simply publishes no TLSA records is delivered normally even in
strict mode (DANE is opportunistic — it only bites when records exist).
SMTP TLS Reporting (RFC 8460): when [pepsi-tlsrpt] SEND_REPORTS is set, the
outcome of each TLS session — a success, or a classified failure
(starttls-not-supported, certificate/handshake or DANE/MTA-STS validation) —
is recorded as an aggregate counter in pepsi.tls_session, keyed by the
applied policy (tlsa/sts/no-policy-found) and MX host.
pepsi-tlsrpt(1) later compiles these into daily reports. Recording is
best-effort (a database hiccup never fails a delivery) and is skipped for a
report message itself (state.tlsrpt).
On success the row is removed (or, if the stage sets NEXT_STAGE, advanced).
A transient failure pauses the message with an exponential backoff (re-queued by
pepsi-dispatch(1) when its timeout elapses); past MAX_LIFETIME the
failure is treated as permanent. A permanent failure of an ordinary message is
routed to the stage’s BOUNCE_STAGE (or, with none configured, the row is
marked failed). A permanent failure of a bounce (null sender) is never
bounced again: a copy is delivered to POSTMASTER if configured, otherwise it
is discarded, and the row is then removed.
This is the direct-to-MX delivery that the former monolithic pepsi-stage-bounce
daemon performed, now without the message transformation (that is the
pepsi-stage-bounce(1) stage’s job).
DSN (RFC 3461): when the recipient’s MX advertises the DSN extension, the
parameters the origin requested (RET/ENVID and the recipient’s
NOTIFY/ORCPT, read from the row’s state.dsn) are propagated to it;
otherwise they are omitted. On a permanent failure the recipient’s
NOTIFY/ORCPT and the ENVID are handed to the bounce stage so a failure
DSN is generated only when requested. This stage preserves state.dsn.
When the global [pepsi] ORIGINATE_SUCCESS_DSN is enabled and a successful
delivery’s recipient requested NOTIFY=SUCCESS, the message is routed (after
delivery) to BOUNCE_STAGE, which emits a positive (Action: delivered)
report to the sender. With the flag off, or without a SUCCESS request, a
successful delivery is simply finished.
When DELAY_DSN_AFTER is set and a still-undelivered message has been queued
at least that long, a one-shot “delayed” (Action: delayed) DSN is sent to the
sender — but only if the sender requested NOTIFY=DELAY (delay has no implicit
default). The original message keeps being retried; the warning is a separate
message routed through BOUNCE_STAGE, and is sent at most once.
Proof of origin: when the shared [pepsi-origin] section is configured (on by
default — pepsi-setup(1) generates a secret if none is set), each outbound
message is stamped with a Pepsi-Origin header before delivery — an HMAC over
the originating sender account, a fresh 128-bit nonce, a timestamp and our
HOSTNAME — and the nonce is recorded in pepsi.origin_nonce (valid for two
weeks). The same stamping is shared with pepsi-stage-relay-to-smarthost(1)
so a message takes a proof whichever outbound path it uses. A returning bounce
embeds this header, letting pepsi-stage-anti-spam(1) confirm the bounce is a
reply to mail we actually sent. The header is prepended like the Received:
trace header (above any existing signatures, so it disturbs none) and is left
unsigned. Recording the nonce is best-effort: a database failure is logged but
does not block delivery. Without the [pepsi-origin] section no header is
added.
Content downgrading (RFC 6152 / RFC 6531): the message is matched to the
extensions the MX actually advertises rather than sent verbatim. When the body
contains 8-bit octets and the MX supports 8BITMIME the envelope carries
BODY=8BITMIME; when it does not, the MIME tree is walked and each 8-bit
leaf part is re-encoded with a 7-bit content-transfer-encoding (quoted-printable
for text/*, base64 otherwise). When the message needs SMTPUTF8 (UTF-8 in
the headers or envelope) and the MX supports it the envelope carries
SMTPUTF8; when it does not, UTF-8 header fields are rewritten as RFC 2047
encoded-words. A non-ASCII envelope address, and a non-ASCII octet inside a
header address, cannot be downgraded — such a message is a permanent failure
(bounced) towards a non-SMTPUTF8 hop. Re-encoding a body necessarily breaks
any body-covering signature already on the message (the originator’s DKIM body
hash and the ARC-Message-Signature from pepsi-stage-arc(1)); this is
unavoidable, since the MX’s capabilities are only known after MX selection, and
rare in practice.
70.1.10.1.4. Configuration¶
Pipeline wiring and every operational option live in the stage’s own
[stage-<name>] section (PROGRAM = pepsi-stage-relay-to-internet): the
SERVER_NAME/POSTMASTER identities, the connection timeouts, the
retry-backoff schedule and MAX_LIFETIME, DELAY_DSN_AFTER,
MAX_HOP_COUNT, the DNS settings, MTA_STS/MTA_STS_TIMEOUT,
ADDRESS_FAMILY and DANE. They are documented in pepsi.conf(5).
70.1.10.1.5. State¶
Inputs (read from the row’s state; all optional):
state.dsn— the RFC 3461 parameters (message-levelret/envidand the first recipient’snotify/orcpt); propagated to the MX when it advertisesDSNand consulted to gate failure/success/delay reports.state.origin— the 8BITMIME/SMTPUTF8 hints (body_8bit,smtputf8) used by the content-downgrading decision.
Outputs (merged into the row’s state):
On a transient failure (
pause):attempts,last_errorand, once a delay DSN has been emitted,delay_sent.On a permanent failure with a
BOUNCE_STAGE(reroute), or on a delay warning (an enqueued clone): astate.bounceobject (kind=permanentordelay) for pepsi-stage-bounce(1). For an SMTP-level failure it also records the structured next-hop detail —remote_mta(the recipient’s domain),smtp_code,enhanced_status,phaseandreply_text— so the bounce can state precisely why delivery was refused.On a successful delivery with
ORIGINATE_SUCCESS_DSNandNOTIFY=SUCCESS: astate.bounceobject withkind = success.On a permanent failure with no
BOUNCE_STAGE(fail):last_error.
state.dsn and state.origin are always preserved. The state layout is
described in full in pepsi.state(7).
Transitions (the retry schedule is RETRY_INITIAL / RETRY_FACTOR / RETRY_MAX_INTERVAL, bounded by MAX_LIFETIME):
successful delivery → finish (the row is deleted), or advance to NEXT_STAGE if one is set; with [pepsi] ORIGINATE_SUCCESS_DSN and a recipient that asked for
NOTIFY=SUCCESSit instead reroutes to BOUNCE_STAGE to emit a positive (Action: delivered) DSN;transient failure → pause for the next retry interval;
still queued past DELAY_DSN_AFTER with a
NOTIFY=DELAYrecipient → enqueue a one-shot delay-DSN clone at BOUNCE_STAGE (the original stays queued and keeps retrying);permanent failure, or MAX_LIFETIME exhausted → reroute to BOUNCE_STAGE, or fail (terminal
failed) if no BOUNCE_STAGE is configured;an undeliverable bounce (null sender) is never re-bounced: a copy is delivered to POSTMASTER (if set) and the row is finished.
70.1.10.1.6. Commands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.
- mta-sts DOMAIN
Diagnostic: look up and print the MTA-STS policy that would apply when delivering to DOMAIN. Uses the system resolver and needs no message or stage configuration.
70.1.10.1.7. Global Options¶
- -c FILE, –config FILE
Read the configuration from FILE instead of searching the default locations.
- -L LOGLEVEL, –log LOGLEVEL
Set the logging verbosity (default
info).- -v, –verbose
Show log messages from all sources.
- -h, –help; -V, –version
Print a usage summary / the version and exit.
70.1.10.1.8. Exit Status¶
- 0
The message was processed (delivered, paused, rerouted, failed or discarded).
- 1
An error occurred (message not found or not
running, misconfigured stage, or a database error). The reason is written to the log.
70.1.10.1.9. Examples¶
Run the forward stage on message 42 (it must be running):
pepsi-stage-relay-to-internet -c /etc/pepsi/pepsi.conf 42
Check which MTA-STS policy applies to a recipient domain:
pepsi-stage-relay-to-internet -c /etc/pepsi/pepsi.conf mta-sts example.com
70.1.10.1.10. See Also¶
pepsi-config(1), pepsi-stage-bounce(1), pepsi-stage-dkim-sign(1), pepsi-stage-relay-to-smarthost(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)
70.1.10.1.11. Bugs¶
Report bugs to the Pepsi issue tracker.