85.1.9. pepsi-stage-bounce

rewrite a message into a delivery-status bounce

Manual section:

1

85.1.9.1.1. Name

pepsi-stage-bounce - the bounce-generation stage of the Pepsi pipeline.

85.1.9.1.2. Synopsis

pepsi-stage-bounce [GLOBAL-OPTIONS] worker

85.1.9.1.3. Description

pepsi-stage-bounce is a stage program: it is run by pepsi-dispatch(1) as a persistent worker, reading message ids on standard input, each identifying a row of the pepsi.workqueue table. It loads that row, refusing to act unless its status is running, and reads its own configuration from the message’s [stage-<stage>] section (see pepsi.conf(5)).

It rewrites the message in place into an RFC 3464 delivery-status notification (DSN): the envelope sender becomes the null sender (<>), the envelope recipient becomes the original message’s sender, and the body becomes a multipart/report quoting the original headers.

“The original message’s sender” is state.srs.original when pepsi-stage-srs(1) recorded one, and the mail_from column otherwise. The distinction matters on any relaying path, because SRS necessarily runs before the delivery stage: by the time a DSN is composed here the envelope sender is one of our own SRS aliases, and addressing the report to it would send it out to the next hop and back in through our own MX to be decoded — surviving the whole inbound pipeline as a null-sender message — only to reach an address that was on the row all along. SRS is a return path for a bounce the next hop generates; a report we write ourselves needs no such detour. See pepsi.state(7). By default this is a failure bounce (Action: failed); when the routing stage tags state.bounce with kind = success it is instead a positive (Action: delivered) report. The diagnostic and recipient are taken from the state the routing stage left on the row. The DSN’s From: is Mail Delivery Subsystem <POSTMASTER>, and POSTMASTER defaults to postmaster@SERVER_NAME. The rewritten DSN is left unsigned; the row is advanced to the stage’s NEXT_STAGE, normally pepsi-stage-dkim-sign(1), which DKIM-signs it (as the postmaster’s domain) before a delivery stage such as pepsi-stage-relay-to-internet(1) relays it.

A message that is already a bounce (it has the null envelope sender) is not rewritten: it is deleted and the deletion is logged. A bounce is never itself bounced (RFC 5321 §6.1).

A report is also not sent to an envelope sender the inbound message did not authenticate: an SPF pass for its domain, or an aligned DKIM pass for a From: at that domain. Spam forges its sender, so a bounce for it lands on an innocent third party (backscatter), and that gets this host blocklisted. Such a message is dropped and logged. Mail our own users submitted, and messages a stage created itself, are always reported. BOUNCE_UNAUTHENTICATED = send turns the check off.

DSN (RFC 3461): a failure bounce is generated only when the failed recipient’s NOTIFY requested FAILURE. The NOTIFY/ORCPT/ENVID the stage acts on are the ones the routing delivery stage copied onto the state.bounce object (from the recipient’s state.dsn entry). The default — no NOTIFY carried — is FAILURE, so ordinary mail still bounces; but NOTIFY=NEVER (or a NOTIFY list without FAILURE) means the sender does not want a failure report, and the message is then dropped without a bounce. When the original ENVID and ORCPT were supplied they are echoed into the DSN as Original-Envelope-Id and Original-Recipient.

A success report (kind = success) is generated only when the recipient explicitly requested NOTIFY=SUCCESS (success, unlike failure, has no implicit default); the routing stage emits one solely when the global [pepsi] ORIGINATE_SUCCESS_DSN is enabled. A delay report (kind = delay, Action: delayed) is generated only when the recipient requested NOTIFY=DELAY; a delivery stage enqueues a copy of a still-undelivered message here (see DELAY_DSN_AFTER in pepsi-stage-relay-to-internet(1)) while the original keeps being retried. Both success and delay have no implicit default.

The human-readable (text/plain) part of the bounce is, by default, a built-in English notice. When the stage’s BOUNCE_MESSAGE option names a template, that part is instead rendered from bounce-<NAME>.<lang>.body under the [pepsi] TEMPLATE_DIR (a Mustache template; see pepsi.conf(5)). <lang> is chosen from state.language in its q order, falling back to en: the bounce goes to the original message’s sender, and the language pepsi-stage-detect-language(1) found in the message they wrote is the best available evidence of what they read. It is a proxy, and where no detection ran (commonly the submission path) the notice is English. The whole message state is the rendering context — augmented with the top-level variables server_name, postmaster, bounce_to, failed_recipient, diagnostic, action (failed/delayed/delivered) and the three booleans failed / delayed / delivered — so a delivery stage’s captured next-hop detail (state.bounce.remote_mta / smtp_code / enhanced_status / phase / reply_text) lets the bounce state precisely why the next MTA refused the message. Rendering is best-effort: any failure falls back to the built-in text, so a bounce is always produced.

pepsi-stage-bounce performs no network delivery and issues no notification; the rewritten message is delivered by whichever stage NEXT_STAGE names.

85.1.9.1.4. Configuration

The bounce stage’s options (SERVER_NAME, POSTMASTER, the required NEXT_STAGE, the optional BOUNCE_MESSAGE template, RET_FULL_MAX_SIZE and BOUNCE_UNAUTHENTICATED) are documented in pepsi.conf(5).

85.1.9.1.5. Returning the message (RET)

The DSN’s third part quotes the message that failed. By default that is its header block, as text/rfc822-headers. When the original sender set RET=FULL on MAIL FROM — recorded by pepsi-ingress(1) and carried to this stage on state.bounce.ret — the whole message is returned instead, as message/rfc822, which is what RFC 3461 §6.2 asks for.

The exception is size. §6.2 permits returning the headers alone when the message “exceeds some implementation-specified size”; RET_FULL_MAX_SIZE is that size (default 256 KiB, measured over the whole stored message), above which the DSN quietly falls back to the header form and logs that it did. The value 0 disables the limit, so a RET=FULL message is always returned whole. The body is read from the database only for a message that asked for it and fits, so an ordinary bounce still costs one header-only load.

85.1.9.1.6. State

Inputs: state.bounce — the report context the routing stage left on the row. Its kind (permanent ⇒ a failure Action: failed report, success ⇒ Action: delivered, delay ⇒ Action: delayed) selects the report type; diagnostic and failed_recipient are quoted in it; the optional notify/orcpt/envid decide whether the report is emitted at all and what is echoed; ret selects whether the whole message or only its headers is returned (see above); and the optional structured next-hop fields remote_mta/smtp_code/enhanced_status/phase/reply_text (written by the relay stages) are surfaced to the BOUNCE_MESSAGE template and, for enhanced_status, into the DSN Status: field. A hand-staged message with no state.bounce yields a generic failure notice.

Outputs: the stage rewrites the message in place and clears ``state`` to null — the bounce is a new null-sender message that inherits none of the original’s provenance (this is the one stage that does not preserve state). The state layout is described in pepsi.state(7).

Transitions:

  • an incoming message that is itself a bounce (null sender) → finish (dropped, never re-bounced);

  • the recipient’s NOTIFY does not request the report this kind would emit (failure has an implicit default; success/delay do not) → finish (dropped without a DSN);

  • otherwise the message is rewritten in place into a DSN and advanced to NEXT_STAGE (the signing/delivery tail).

The stage never pauses, fails or reroutes.

85.1.9.1.7. Commands

worker

Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.

85.1.9.1.8. Global Options

These global options precede the subcommand.

-c FILE, –config FILE

Read the configuration from FILE instead of searching the default locations.

-L LOGLEVEL, –log LOGLEVEL

Set the logging verbosity (error, warn, info, debug or trace; default info).

-v, –verbose

Show log messages from all sources, including third-party libraries.

-h, –help

Print a usage summary and exit.

-V, –version

Print the version and exit.

85.1.9.1.9. Exit Status

The outcome of each message is reported to pepsi-dispatch(1) on the worker’s status line, not as the exit status.

0

The worker ran until its standard input was closed.

1

A fatal error occurred (unreadable configuration, the database could not be opened, or standard input/output failed). The reason is written to the log.

78

The stage’s section does not parse, or names no NEXT_STAGE: the worker refuses to start, and pepsi-dispatch(1) holds the stage’s messages until the configuration is fixed.

85.1.9.1.10. Examples

Run the bounce stage on message 42 by hand (it must be running):

echo 42 | pepsi-stage-bounce -c /etc/pepsi/pepsi.conf worker

85.1.9.1.11. See Also

pepsi-config(1), pepsi.conf(5), pepsi.state(7), pepsi-stage-dkim-sign(1), pepsi-dispatch(1), pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1), pepsi-setup(1)

85.1.9.1.12. Bugs

Report bugs to the Pepsi issue tracker.