85.2.1. pepsi.conf¶
configuration file for the Pepsi pipeline
- Manual section:
5
85.2.1.1.1. Name¶
pepsi.conf - configuration file shared by all Pepsi components.
85.2.1.1.2. Description¶
Every Pepsi component — pepsi-ingress(1), pepsi-dispatch(1), every
pepsi-stage-* program, pepsi-httpd(1), pepsi-queue(1),
pepsi-tlsrpt(1), pepsi-setup(1) and the rest — reads the same
INI-style configuration file, conventionally pepsi.conf. The format is
shared with the GNU Taler components Pepsi builds upon.
Because there is one file, each setting is documented once, under the section
that owns it. A component reads only the sections relevant to it: the shared
[pepsi] and [pepsi-postgres] sections are read by everything; the
[stage-<name>] pipeline sections are read by the dispatcher and the stage
programs; component-specific sections ([pepsi-ingress], [pepsi-httpd],
[pepsi-dispatch], [pepsi-payments], [pepsi-tlsrpt] …) by the
component named.
An option nothing reads is ignored, so a misspelt one silently takes its default; pepsi-setup(1) warns about every such option in a section it knows, naming the closest real option.
Renamed options. When a release renames an option, the old name keeps working for at least one more release: every program reads it under the new name and logs a warning saying so, and pepsi-setup(1) warns too. When both names are set, the new one wins. NEWS lists every rename.
85.2.1.1.3. File Format¶
A configuration file is a sequence of [SECTION] headers, each followed by
OPTION = VALUE assignments:
[pepsi-ingress]
HOSTNAME = mail.example.org
ACCEPTED_DOMAINS = example.org example.com
Section and option names are case-insensitive and are conventionally written in
upper case. Blank lines are ignored. A # or % at the start of a line
introduces a comment. A value may be wrapped in double quotes to preserve
leading or trailing whitespace.
85.2.1.1.3.1. Directives¶
- @inline@ FILE
Include another configuration file, resolved relative to the current file.
- @inline-matching@ GLOB
Include every file matching the shell glob GLOB.
- @inline-secret@ SECTION FILE
Merge section SECTION from FILE (typically a mode-restricted file holding credentials) into the configuration. A file that cannot be read is ignored with a warning. This keeps secrets out of the world-readable main file.
A directive is a top-level line: like
@inline@, it ends the section it appears in. Write it after the last option of the section it feeds — an option placed below it belongs to no section, and the whole file then fails to load withExpected section header or directive. pepsi-setup(1) always emits its directives last within a section for this reason:[pepsi-srs] SRS_DOMAIN = srs.example.org MAX_AGE_DAYS = 21 @inline-secret@ pepsi-srs secrets.d/pepsi-ingress.secret
Because an unreadable fragment is only a warning, an option it should have supplied simply looks unset — a permission problem is reported by whatever needs the value, typically as “… is not configured”. Pepsi detects that case and says so explicitly, naming the fragment, its owner and mode; run
pepsi-setup runas root to give each fragment to its reader account.
85.2.1.1.3.2. Value Substitution¶
Options read as paths undergo $-expansion: $VAR and ${VAR} are
replaced with the value of VAR from the [PATHS] section or, failing that,
the environment. The form ${VAR:-default} supplies a fallback. The
[PATHS] section is pre-populated with the usual installation directories
(PREFIX, BINDIR, DATADIR and so on).
85.2.1.1.3.3. Value Types¶
- boolean
YESorNO(case-insensitive).- number
A decimal integer. A few options documented as such take a fractional value instead, and each says so where it is described (the ingress connection-rate options and the relay stages’
RETRY_FACTOR).- duration
A span such as
5 sor2 h. See the caveat below.- path
A filesystem path subject to
$-expansion as described above.- unix mode
A file permission written in octal, e.g.
660.
Warning
duration values are parsed by jiff::SignedDuration, which accepts
only hours/minutes/seconds and below. A calendar unit such as 5 d or
2 weeks does not parse. For day-or-longer windows write the
equivalent in hours (120 h) or use an integer option where one is
provided (e.g. [pepsi-srs] MAX_AGE_DAYS).
85.2.1.1.4. The Database Overlay¶
This file is the base layer. On top of it, every component reads a set of
administrator-managed overrides from the pepsi.config_override table of the
shared schema, so a deployment can be reconfigured without editing files and, for
the stage sections, without a restart. With no rows in that table a deployment
behaves exactly as this file says; deleting every override returns it to that
state.
The scope chain, lowest precedence first:
this file;
database scope
global;database scope
domain:DOMAIN;database scope
address:ADDRESS;the per-address
pepsi.settingstable (pepsi-settings(1)).
Higher layers override lower ones option by option, never section by
section: an option that no layer mentions keeps the value written here. The
address a domain:/address: scope is matched against is the envelope
sender for a locally-originated message, otherwise each envelope recipient.
The last layer is a different table with a different write authority.
pepsi.settings is written by account owners themselves, by e-mail, within
the EDITABLE_STAGES allowlist; config_override defines the pipeline and
is writable only through the pepsi-config PostgreSQL role.
pepsi-setup(1) grants that role INSERT/UPDATE/DELETE on the
table, revokes them from every service account, and verifies both against the
live database after each install: a stage worker parses hostile mail for a
living and must not be able to rewrite the configuration that defines the stage
it runs in.
85.2.1.1.4.1. Sections That Are Never Read From the Database¶
An override naming one of these is ignored at run time and refused at write time:
[pepsi],[pepsi-postgres],[PATHS]Read before a database connection can exist – and
[pepsi]additionally holds the administrator-only cryptographic policy.[pepsi-httpd],[pepsi-httpd-listener-*],[pepsi-httpd-cert-*]The server that serves the configuration interface must always be able to start.
[pepsi-ingress-listener-*]Listening sockets are bound at start-up, often under socket activation and before privileges are dropped.
[pepsi-secure-link]Its
PEPPERis the server-side secret that makes a stolen database useless, so a database able to replace it could arrange for the next message stored to be one an attacker can open. The rest of the section rides along.[pepsi-admin]Decides who may administer the server and how the administrative surface reaches the database; a database opinion about it would be a way to grant yourself the console.
[pepsi-crypto],[pepsi-srs],[pepsi-origin]Each holds a server-side key that exists so that the database alone is not enough: the key-encryption keys (and which of them wraps new private keys), the SRS key that authorises relaying to any address, and the proof-of-origin key a returning bounce or payment demand is checked against. A database able to replace one could arrange for the next wrapped key to be one an attacker can open, or for a forged bounce to be relayed or paid. The rest of each section rides along.
[pepsi-wizard]Not configuration at all, but pepsi-setup(1)’s record of the answers it was given, which the browser console stages as draft rows in this very table. Nothing reads it at run time, so the database never supplies it either.
Two further rules apply in every other section:
No credential is ever stored in the database. An option whose name marks it as one – containing
PASS(PASSWORD,PASSPHRASE,API_PASS),SECRET,TOKEN,CREDENTIAL,CLIENT_IDorPEPPER, the same rule that masks a value wherever a configuration is shown – is refused at write time and ignored at run time, in every section. That includes the file a credential is read from and the endpoint it is sent to (SECRET_FILE,TOKEN_FILE,TOKEN_ENDPOINT): a database able to set those can redirect the credential. Credentials live in this file or itssecrets.dfragments, each readable only by the account that needs it.The
domain:andaddress:scopes take[stage-*]sections only. Those layers are consulted by a stage, per message, for the section it is running; nothing reads any other section per domain or per address, so such an override is refused rather than stored and never read.
Consequently adding a submission port, changing a listener’s TLS material, the
database connection or the crypto policy is a text-editor-and-restart
operation, and no administrative interface can do it. [pepsi-ingress]
DB_POOL_SIZE and [pepsi-dispatch] DB_POOL_SIZE are file-only for the same
bootstrap reason: the pool must exist before the overlay can be read.
85.2.1.1.4.2. Hot Reload¶
Every write to the table fires a config_changed notification.
Every [stage-*] change is applied without a restart, and that is
both halves of the pipeline. On the notification pepsi-dispatch(1) rebuilds
its own stage table from the overlay – so a stage may be added, edited or
removed while the pipeline runs, and an edited PROGRAM/PARALLELISM/
MAX_MESSAGES/QUEUE_LIMIT takes effect – and it retires its stage workers,
so the replacements re-read the overlay. No message is interrupted: a worker is
given no further work and its slot is dropped once its in-flight messages report
back.
The one thing a reload will not do is route by a pipeline the workers no longer
share. A reloaded stage graph that does not parse ends the dispatcher
(exit EX_CONFIG, through the ordinary shutdown, so nothing is stranded)
rather than leaving the previous table in place.
Every other section is read once at start-up by the component that owns it and
therefore needs that component restarted – including [pepsi-dispatch]’s
own options, which are deliberately not reloaded. pepsi-config set prints
which case applies.
A component that misses a notification re-reads the overlay when its database listener reconnects, so a lost notification costs latency, not correctness.
Secrets are never stored in the database: the overlay refuses a credential
(see above), and the @inline-secret@ reference lives in this file. Each
fragment stays owned by the single reader that needs it.
A broken value in the database cannot stop a program from starting: an overlay that cannot be read or does not produce a usable configuration is logged loudly and this file is used unchanged.
See pepsi-config(1) for set/unset/list, for
dump --origin (which annotates every effective value with the layer it came
from), and for the encrypted export/import of this file and its
secrets.d fragments.
85.2.1.1.5. The [pepsi] Section¶
Cross-cutting identity and policy settings shared across the Pepsi tools. This section is never read from the database overlay.
- KEY_DIR = PATH
Directory below which per-domain signing keys are stored. Each domain gets a sub-directory (mode
0700) containingdkim.rsa.keyanddkim.ed25519.key(mode0600). Optional; defaults to/var/pepsi/keys.- DKIM_SELECTOR = NAME
Base DKIM selector. The RSA key is published as a TXT record at
<NAME>._domainkey.<domain>and the Ed25519 key at<NAME>-ed25519._domainkey.<domain>. Optional; defaults topepsi.- DKIM_ALGORITHMS = LIST
The DKIM signatures pepsi-stage-dkim-sign(1) adds: a whitespace- or comma-separated list of
rsaanded25519. Optional; defaults to both. Gmail, Microsoft 365 and Yahoo do not verify Ed25519 (RFC 8463) signatures; they ignore them, and Google’s DMARC aggregate reports list the Ed25519 selector asfail. That is harmless — DMARC needs one aligned pass and the RSA signature supplies it — butrsaalone removes the noise. Leaving outrsais permitted and warned about by pepsi-setup(1): those receivers would then see no verifiable signature at all. pepsi-setup publishes and checks only the selectors in use (plus the one the ARC seal needs when this is theARC_DOMAIN); both key files are generated regardless.- ARC_DOMAIN = DOMAIN
Domain whose identity pepsi-stage-arc(1) uses to ARC-seal inbound mail (RFC 8617). The seal reuses this domain’s DKIM key, so pepsi-setup(1) adds it to the set of domains it generates keys and DKIM DNS records for; ARC needs no separate record. Optional; if unset, authentication results are recorded but not sealed.
- ARC_ALGORITHM =
rsa|ed25519 Algorithm that signs the single ARC seal (ARC permits only one per hop, unlike DKIM, which by default is signed with both — see
DKIM_ALGORITHMS). Optional; defaults torsafor the widest interoperability.- ORIGINATE_SUCCESS_DSN =
yes|no Whether Pepsi may originate a positive (
Action: delivered) delivery-status notification when a delivery succeeds and the sender requestedNOTIFY=SUCCESS. Optional; defaults tono— Pepsi normally emits only failure bounces. Whenyes, the delivery stages (pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-maildir(1), pepsi-stage-relay-to-lmtp(1)) and pepsi-stage-discard(1) route a successful delivery to theirBOUNCE_STAGE(for LMTP, itsNOTIFY_STAGE) to emit the report.It governs
SUCCESSreports only.DELAYreports have their own, per-stage switch (a delivery stage’sDELAY_DSN_AFTER, unset by default) andFAILUREbounces need no switch at all – they are the default when a recipient’sNOTIFYsays nothing.SUCCESSandDELAYalike are emitted only when the sender’sNOTIFYasked for them by name; neither has an implicit default.- MAILBOX_QUOTA = SIZE
Default limit on how much mail a local account may hold, applied by pepsi-stage-relay-to-maildir(1). A plain byte count or a
K/M/G/Tsuffix in powers of 1024 (2G,500M,1048576), ornone. Optional; defaults tonone— no quota. A single account’s limit is set with pepsi-quota(1)set, which overrides this; the kernel’s own limit tightens whichever applies.The quota covers the whole Maildir++ tree — the INBOX plus every folder (
.Sent,.Trash, …) — because that is what the user calls their mailbox, and counting only the INBOX would let anyone park unlimited mail one drag-and-drop away from it.- MAILBOX_QUOTA_COUNT = N
Default limit on the number of messages an account may hold;
0or absent for none. A very large number of very small messages is its own denial of service against a mailbox, which a byte limit alone does not prevent.- MAILBOX_OVER_QUOTA =
defer|bounce What happens to a recipient whose mailbox is full. Optional; defaults to
defer.deferkeeps the message queued and retries it until the delivery stage’sMAX_LIFETIME, then bounces — the same treatment a kernelEDQUOTgets, and it gives the account time to make room.bouncerefuses at once with an RFC 34635.2.2, routing the recipient to the delivery stage’sQUOTA_LIMIT_STAGE, so the sender is not left waiting for days.The same choice picks the reply pepsi-ingress(1) gives when it refuses an over-quota recipient at
RCPT:452 4.2.2fordefer,552 5.2.2forbounce.- MAILBOX_QUOTA_MAX_AGE = DURATION
How long a mailbox measurement may be trusted for a
RCPT-time refusal. Optional; defaults to15 m. Units areh/m/s.pepsi-ingress(1) refuses an over-quota recipient only on a measurement younger than this, and never on the running usage estimate. That is a correctness bound, not a performance one: the estimate only ever grows (nothing tells Pepsi when a user deletes mail over IMAP), so refusing on it would shut a mailbox whose owner has since emptied it — permanently, because with every message refused no delivery would run to re-measure. This bound, and pepsi-quota(1)
reconcile, are what keep that from happening.- MAILBOX_QUOTA_RCPT_CHECK =
yes|no Whether pepsi-ingress(1) consults the quota table at
RCPTtime. Optional; defaults toyes. Refusing there costs one indexed lookup and saves a bounce: the sending MTA is still on the line, so it tells its user, and Pepsi generates no backscatter to an envelope sender that may well be forged. Turning it off moves all enforcement to delivery time.- MAILBOX_HELPER = PATH
The privileged helper pepsi-quota(1) runs to measure a mailbox — for its
measuresubcommand and itsreconcilesweep. Optional; defaults topepsi-helper-maildir-writer, resolved on the invoking process’sPATH.It is the same binary the local-delivery stage uses, invoked in its
measuremode: aMaildiris0700, so this is the one process that can become the user and read it. Set this only when the helper is not onpepsi-quota’sPATHunder its name — chiefly a--prefix-ed source install. Keep it in step with the delivery stage’s ownHELPERoption in its[stage-<name>]section (see pepsi-stage-relay-to-maildir(1)): the two name the same program for two callers, and only the stage’s is per-stage.- MAILBOX_FS_QUOTA =
yes|no Whether the filesystem holding home directories enforces kernel disk quotas, in which case Pepsi consults
quotactl(2) as well and lets the kernel’s limit tighten the effective one. Optional; defaults tono.Normally set for you. This is a fact about the host’s mounts, not a preference, so pepsi-setup(1) probes for it and records the answer without asking. An explicit value is never overwritten. Have it on where it applies: the kernel’s accounting is the only layer of the three that the account owner cannot tamper with (the Maildir++
maildirsizefile lives in their own directory), and it counts everything they own on that filesystem rather than only their mail.- RECIPIENT_DELIMITER = CHARACTER |
none The site-wide sub-address separator (
alice+listsis accountalice). Optional; defaults to+. A[stage-<name>]section may override it for its own delivery decisions, but prefer setting it here: pepsi-ingress(1) uses this value to work out whose mailbox quota a recipient belongs to and has no stage section to read, so a stage that disagrees makes sub-addressed mail escape theRCPT-time refusal. pepsi-setup(1) warns when one does.Mailing lists use this value, and only this one, for every sub-address they write and read: the VERP envelope sender of each copy (
announce-bounces+alice=example.net@…), a bounce probe’s sender, the-confirm+token address, and pepsi-stage-list(1)’s router that recognises them when they come back.noneleaves lists on+, since a VERP sender cannot be written without a delimiter.- MAX_QUEUE_ROWS = N |
none Queue admission control: the most messages (
pepsi.workqueuerows) the queue may hold before pepsi-ingress(1) stops accepting mail. Optional; defaults to1000000;none(or0) for no limit. A row is one recipient group, so this is the figure a mailing-list fan-out or a relay stage’s one-row-per-recipient split multiplies.At the limit, pepsi-ingress(1) answers
452 4.3.1atRCPT, atDATA/the firstBDATchunk, and after the end of data. That is a temporary refusal: the sending MTA keeps the message and retries, while the queue that is already there drains. pepsi-stage-list-post(1) holds a fan-out back forQUEUE_THROTTLE_DELAYinstead of emitting its next batch. Nothing already queued is affected: bounces, DSNs and auto-replies the pipeline itself originates are never refused.- MAX_QUEUE_BYTES = SIZE |
none The most octets the queue may hold: every queued header block plus every queued body, each body counted once however many recipient rows share it (it is stored once — see STORED DATA below). A plain byte count or a
K/M/G/Tsuffix as forMAILBOX_QUOTA. Optional; defaults tonone. After the end of data the check includes the size of the message being offered, so one oversized message is refused on its own merits. Refusals are as forMAX_QUEUE_ROWS.- MIN_FREE_SPACE = SIZE |
none The free space that must remain on the filesystem holding
FREE_SPACE_PATHfor mail to be accepted; a message is refused when accepting it would leave less. Optional; defaults to1G;nonedisables the probe. This is the limit that sees what is not the queue – write-ahead log, other databases, logs – and PostgreSQL does not degrade when its filesystem fills: it stops, and every stage, the console and the submission socket with it. Refusals are as forMAX_QUEUE_ROWS.- FREE_SPACE_PATH = PATH
Where MIN_FREE_SPACE is measured (
statvfs(3), the space available to an unprivileged writer). Optional; defaults to/var/lib/postgresql, the parent of the Debian clusters’ data directories – a data directory itself is0700 postgres, butstatvfsneeds only to reach a path, and its parent lies on the same filesystem in a default layout. Set it to the data directory (or its mount point) when PostgreSQL keeps its data elsewhere. When the database is on another host the default path usually does not exist and the probe is silently off; a path set here that cannot be probed is logged once, and the limit is then not enforced.- QUEUE_CHECK_INTERVAL = DURATION
How long a measurement of the queue is reused. Optional; defaults to
10 s. Measuring is two sequential scans (thepepsi.queue_usage()function), so each process takes at most one per interval and everyRCPTin between is answered from the cached figure; the limits are therefore approximate by up to one interval’s worth of admissions. A measurement that fails admits mail (and is logged) – a database hiccup must not become an outage of its own, and an insert that genuinely cannot be stored still answers451.- QUEUE_THROTTLE_DELAY = DURATION
How long pepsi-stage-list-post(1) holds back a fan-out it found the queue full for, before it looks again. Optional; defaults to
60 s.- MAIL_LOG =
off|summary|full Whether to keep a per-message record. Optional; defaults to
off.Pepsi deletes a message’s row when the pipeline is finished with it, so there is normally no per-message success log: an ordinary deployment does not accumulate a record of its users’ correspondence, and cannot be compelled to produce one it does not have.
Setting this option changes that.
summarywrites onepepsi.mail_logrow as a message leaves the pipeline, carrying the envelope sender and recipients, the direction (outboundfor locally-submitted mail,inboundotherwise), the stage it ended at, the outcome (completed/failed) and what the pipeline decided about it (a closed list ofstatekeys: the authentication verdict, the spam and paid decisions, any next-hop failure detail, and the cryptography and language findings).fulladditionally records theSubject:line. Message content is never recorded at any setting.Warning
Turning this on means the deployment keeps a record of who corresponds with whom. Some deployments genuinely need that evidence; make sure yours is one of them, and that keeping it is lawful where you operate.
Rows are readable through
GET /api/v1/mail-logby a principal holdinglogs:read, are append-only for every component that processes mail, and are pruned after[pepsi-admin] MAIL_LOG_RETENTION_DAYSdays. The write rides on the same statement as the terminal that removes or fails the message, so enabling the log costs no extra database round-trip. See the manual’s “The administrative API” chapter.- ALLOW_FUSION =
yes|no Whether stage fusion is permitted. When a stage advances to a successor that is marked
FUSION = yes(see the per-stage option below), whosePROGRAMis folded into the same unifiedpepsibinary, and that needs no message column the predecessor did not already load, the predecessor runs the successor’s body in its own worker process — skipping both the advancingUPDATEand the successor’s loadingSELECT, and reporting a single completion to the dispatcher for the whole fused chain. Optional; defaults toyes. Set it tonoto force every stage transition back through the database and the dispatcher (the per-stage cost benchmark does this so each stage is measured as its own dispatched worker). Fusion is in any case inert in a per-program (multibin) build, where a successor is always a separate process. See pepsi-dispatch(1) for the full model.- MTA_STS_MODE =
enforce|testing|none Mode of the MTA-STS policy published for our domains.
enforceandtestingcause pepsi-setup(1) to emit a_mta-sts.<domain>TXT record (and pepsi-httpd(1) to serve the corresponding policy file);nonedisables MTA-STS publishing. Optional; defaults toenforce. Which hosts the policy permits is MTA_STS_MX.- MTA_STS_MX = ENTRY[, ENTRY…]
The hosts published as
mx:lines in the MTA-STS policy — every MX a sender may deliver to, RFC 8461 §3.2 (mx“must appear at least once and may appear more than once”). Each ENTRY is eitherHOSTa host pattern applying to every served domain, or
DOMAIN:HOSTa host pattern applying to that one served domain.
A domain’s
mxset is the unqualified entries plus the entries qualified with its own name; qualified entries add to the shared set rather than replacing it, so a site whose domains share a primary MX writes it once and cannot lose it by qualifying one domain’s backup. A host pattern may use a single leading-label wildcard (*.example.net), and:is unambiguous as the separator because a hostname cannot contain one.The policy is therefore per domain, which is what a host serving several domains needs: each domain’s MX records name its own hosts, and there is no reason those agree. Each domain’s policy gets its own
idin its own_mta-sts.<domain>TXT record, so senders cache them independently:MTA_STS_MX = example.org:mx.example.org, example.net:mail.example.net
Optional. When the option is absent entirely, every domain’s policy names the
[pepsi-ingress] HOSTNAME. That fallback is a last resort, not a good default:HOSTNAMEis theEHLOgreeting name, which need not be a name anyMXrecord points at — and when it is not, anenforcepolicy tells every compliant sender not to deliver to the domain at all. So set this option to the hosts yourMXrecords actually name. The pepsi-setup(1) wizard reads them out of DNS and writes the option for you, and everypepsi-setup runre-checks the resolved set against liveMXrecords and prints the option to set when they disagree (see pepsi-setup(1), MTA-STS cross-checks).Under the default
enforcemode a policy that omits a host tells every compliant sender it must not deliver there, so a backup MX left out of this list receives no mail while the primary is down.Each host named must also present a TLS certificate valid for its own name (RFC 8461 §4.1), and pepsi-setup(1) provisions that: the hosts listed here become Subject Alternative Names on the ingress listeners’ certificate, which is expanded through certbot on the next
pepsi-setup runif it does not cover them yet. One certificate rather than one per host, because pepsi-ingress(1) serves a single certificate per listener and does not select on SNI. A wildcard entry is the exception: HTTP-01 cannot obtain one, so that certificate has to be supplied withTLS_CERT/TLS_KEY.- MTA_STS_MAX_AGE = SECONDS
The
max_agepublished in the MTA-STS policy (how long senders may cache it). Optional; defaults to604800(one week). RFC 8461 §3.2 bounds it to1..``31557600``, and a value outside that is refused – in particular0, which would publish an immediately-expiring policy and so disable MTA-STS while looking like it was configured.- TEMPLATE_DIR = PATH
Directory holding message templates (
<name>.<lang>.body) expanded at runtime — for example the pepsi-stage-anti-spam(1) payment-request reply and the pepsi-stage-bounce(1)BOUNCE_MESSAGEbody. Optional; defaults to${DATADIR}/templates, wheremake installplaces the bundled templates.${DATADIR}is resolved from the install prefix of the running binary, so the default is/usr/share/pepsi/templatesfor a--prefix=/usrinstallation and follows the prefix for any other; set this option only if the templates live somewhere the prefix does not imply.- LOG_JSON =
yes|no Emit logs as structured JSON lines (one JSON object per record) on standard error instead of the default human-readable text format, so a log aggregator such as Loki can ingest them without extra parsing. Applies to every Pepsi binary. Optional; defaults to
no(text). The-L/--loglog-level option and-v/--verboseapply in both formats.- LOG =
error|warn|info|debug|trace Default maximum log level for every Pepsi binary. Because all components — including the stage workers the dispatcher spawns (which receive no command-line log flag of their own) — load this same configuration, this is the single knob that lowers or raises logging across the whole pipeline at once; it is, for instance, what the benchmark scripts set to
warnso per-message logging does not skew their measurements. Optional; defaults toinfo. The-L/--logflag, when given, overrides it for that one process.- PUBLIC_RESOLVERS = IP … |
none The public recursive resolvers through which pepsi-setup(1) re-checks, as the Internet sees them, the DNS records this host depends on: the reverse DNS of every
PUBLIC_IP, each served domain’sMXand TXT records, and the ingressHOSTNAME’s addresses (see DNS as the Internet sees it in pepsi-setup(1)). A list of IP addresses separated by spaces or commas. Optional; defaults to Google (8.8.8.8,2001:4860:4860::8888), Cloudflare (1.1.1.1,2606:4700:4700::1111) and Quad9 (9.9.9.9,2620:fe::fe).nonedisables the check.A lookup from this host cannot see a zone that only this host’s network can reach — a nameserver behind NAT whose port 53 is not forwarded looks healthy from inside — which is why the check exists. The names it sends to these resolvers are ones this host publishes for anybody to query; set
noneanyway if you prefer that no third-party resolver be asked, and verify from a machine outside your network instead. Read only by pepsi-setup.- SHARE_TELEMETRY =
yes|no Whether this deployment shares anonymous feature-usage telemetry with the Pepsi project. Opt-in: optional, and defaults to
no, so an installation that never sets it shares nothing. When set toyes, pepsi-setup(1) generates a randomSYSTEM_ID(below) and the pepsi-telemetry-client(1) daemon aggregates feature-usage events from the stage workers and pepsi-ingress(1) and submits per-feature counts toTELEMETRY_SERVERunder that anonymous id (no addresses, message data, host names or IP addresses). Left atno, a running daemon stays dormant (no socket, nothing submitted), noSYSTEM_IDis generated, and the in-process telemetry calls are no-ops that open no socket. An unparseable value is read asno. Whoever changes this option in the file notifies the daemon (telemetry_changed); it also re-reads the file every minute. The browser setup console can switch it on only while the daemon is running with aSYSTEM_ID.- SYSTEM_ID = 64-HEX
Anonymous 256-bit identifier (64 hexadecimal characters) submitted with every telemetry report. Generated and written here by pepsi-setup(1) only once
SHARE_TELEMETRYhas been turned on — a deployment that has not opted in never has one minted for it; required by pepsi-telemetry-client(1). An operator may add one by hand while telemetry is off, which is what lets the browser console offer to switch telemetry on; a console save keeps it. Do not reuse it across deployments.- TELEMETRY_SERVER = HOST | URL
The central telemetry collector. Optional; defaults to
telemetry.pepsi.taler.net. pepsi-telemetry-client(1) submits tohttps://<server>/telemetry/{usage,features}; a value containing an explicitscheme://is honoured verbatim (e.g.http://localhost:18080for local testing).
The following CRYPTO_* options govern end-to-end message cryptography
(OpenPGP and S/MIME) — not the DKIM/ARC keys above. They are deliberately
system-administrator settings and live in [pepsi] rather than in a
[stage-*] section, which puts them structurally out of reach of the
per-address override layer (pepsi.settings overrides only stage sections)
and therefore of pepsi-stage-edit-settings(1): algorithm selection,
downgrade permission, weak-digest acceptance and minimum key sizes are security
decisions with a secure default, not per-user preferences. Every one is
optional; a deployment that does no end-to-end cryptography can ignore them all.
The key store those keys live in is configured in [pepsi-crypto] (below) and
managed with pepsi-keys(1).
- CRYPTO_ALLOW_DOWNGRADE =
no|negotiate|yes Whether Pepsi may emit a downgraded container (S/MIME AES-256-CBC
EnvelopedData, OpenPGP SEIPDv1-with-MDC) and accept 3DES on decryption. Optional; the value compiled in isnegotiate, which downgrades only when the recipient’s own key proves it can do no better — which matters, because much of the installed base cannot read an RFC 9580 AEAD message at all. Reading SEIPDv1 and AES-CBC is always permitted and is not gated by this option.An installed Pepsi ships
yes, from${DATADIR}/config.d/thunderbird.conf, so the effective default on a packaged system is S/MIME AES-256-CBCEnvelopedData. Thunderbird 140.12.0esr cannot read our AES-256-GCMAuthEnvelopedDataand fails silently — no error, an empty plaintext — so a deployment left on the compiled-in default would send blank mail to the largest S/MIME client population there is. Deleting that file restores AEAD in one step, andCRYPTO_ALLOW_DOWNGRADE = negotiateinpepsi.confdoes the same without deleting it (pepsi.confis parsed afterconfig.d). The measurement ispepsi-crypto/tests/thunderbird.rs; the client matrix is in the manual’s interoperability chapter.yesdoes not coarsen OpenPGP: the encrypt stage requests the containerauto, which bothnegotiateandyesresolve per recipient — AEAD for a certificate that advertises SEIPDv2, SEIPDv1+MDC only for one that does not. The difference between the two values is the S/MIME container and the acceptance of an inbound 3DES S/MIME message, which is never emitted under any setting. Inbound OpenPGP 3DES is accepted undernegotiateas well asyes, and refused only underno: an OpenPGP certificate states its own cipher capabilities, sonegotiatehas something to negotiate with, whereas X.509 advertises none and therefore gets the stricter rule.pepsi-setupdoes not write this option. The wizard deliberately leaves it out of the generatedpepsi.conf— anything it wrote there would override the packaged file — so a deliberate override is made by editingpepsi.confby hand or withpepsi-setup --wizard --expert=CRYPTO_ALLOW_DOWNGRADE.- CRYPTO_ALLOW_WEAK_DIGESTS =
yes|no Accept SHA-1 and MD5 signatures on verification. Optional; defaults to
no. A signature under a weak digest is reported as such, never silently counted as valid.- CRYPTO_MIN_RSA_BITS = BITS
Smallest RSA modulus accepted on inbound verification and outbound encryption. Optional; defaults to
2048. Values below1024are refused outright.- CRYPTO_GENERATE_RSA_BITS = BITS
Modulus size used when this deployment generates an RSA key. Optional; defaults to
2048. It may not be belowCRYPTO_MIN_RSA_BITS— a deployment must not generate keys it then refuses to accept — and OpenPGP key generation accepts only2048,3072or4096.- CRYPTO_INLINE_PGP =
accept|reject How to treat inline (non-MIME) PGP. Optional; defaults to
accept, because real senders still emit it. Pepsi never generates it.- CRYPTO_OPENPGP_ALGORITHM =
ed25519|rsa Algorithm used when generating a new OpenPGP identity. Optional; defaults to
ed25519— generation is instantaneous, which matters because an identity may have to be created while a message waits.rsausesCRYPTO_GENERATE_RSA_BITS.- CRYPTO_SMIME_ALGORITHM =
rsa|p256|p384 Algorithm used when generating a new S/MIME identity. Optional; defaults to
rsa(atCRYPTO_GENERATE_RSA_BITS);p256andp384are ECDSA/ECDH over the corresponding NIST curve.- CRYPTO_SMIME_SHARED_KEY =
yes|no Issue one S/MIME certificate carrying both
digitalSignatureandkeyEnciphermentinstead of a separate signing and encryption certificate. Optional; defaults tono.The default splits them because a separate signing certificate can be destroyed at expiry or revocation — nothing legitimate ever re-signs old mail, so a retired signing key is pure forgery liability — while the decryption half is retained so archived and in-flight ciphertext stays readable. Turning this on halves the certificates an operator buys per identity and gives that up: a shared key follows the decryption rule, so revoking a compromised signing key also ends the ability to receive new encrypted mail at that certificate.
The option decides only what newly created identities look like. Both shapes are supported permanently and may coexist for one address, so every lookup resolves by capability rather than by shape. Combined with an elliptic-curve
CRYPTO_SMIME_ALGORITHMit is refused by pepsi-setup(1): one EC key doing both ECDSA and ECDH is cross-algorithm key reuse that several S/MIME clients reject.
85.2.1.1.6. The [pepsi-postgres] Section¶
Shared database connection settings. Every Pepsi component connects to the
same database and the shared pepsi schema, so these settings live in one
section rather than being repeated per component.
Note
Connection budget. Each Pepsi process opens its own connection pool. The
stage workers and the operator CLIs do their database work strictly serially,
so they use a single connection each — and because the dispatcher runs up
to one worker process per stage slot (the sum of the stages’ PARALLELISM),
this single-connection-per-worker rule is what keeps the bulk of the
connection count predictable and minimal. The concurrent components keep a
small bounded pool sized by their own DB_POOL_SIZE option: the
pepsi-dispatch(1) coordinator (default 8, for the many workers’
concurrent result-processing) and the pepsi-ingress(1) and
pepsi-httpd(1) servers (default 1 each). When tuning, ensure
Σ PARALLELISM (one connection per live worker) + the dispatcher pool
+ the two servers’ pools stays below the server’s max_connections.
A stage’s QUEUE_LIMIT does not enter this budget: pipelining more
messages onto a worker raises its in-flight count, not the number of worker
processes (or connections), so it is the safe knob for throughput when
max_connections is the binding constraint.
- CONFIG
(string, required) PostgreSQL connection string or URI, e.g.
postgres:///pepsi. The server sets the schema search path topepsiand usesREAD COMMITTEDtransactions (at most one writer ever touches a given queue row, so serializable isolation is unnecessary).This file is world-readable, so the string must not carry a password; every component refuses to start with one.
Local database (the canonical setup). Each component connects over the UNIX socket as the role named after its own system account (
pepsi,pepsi-ingress,pepsi-httpd,pepsi-crypto, …) and PostgreSQL’s peer authentication checks the uid. No password exists anywhere, and the database grants of each role are the privilege boundary between the components.Remote database (recommended setup). Keep the same model: one role per account, each with credentials of its own that no other account can read. The placeholder
{role}in the connection string is replaced, when a component connects, by the role it connects as, so one string names a different file for every role. With client certificates (the database’spg_hba.confusingcert):CONFIG = postgres://db.example.org/pepsi?sslmode=verify-full&sslcert=/etc/pepsi/postgres/{role}.crt&sslkey=/etc/pepsi/postgres/{role}.keyor with passwords, one libpq password file (
.pgpassformat, e.g. the single line*:*:*:*:<password>) per role:CONFIG = postgres://db.example.org/pepsi?sslmode=verify-full&passfile=/etc/pepsi/postgres/{role}.pgpassEach key or password file belongs to its role’s account and has mode
0600or0400; a password file anybody else can read is refused, as libpq refuses it, and pepsi-setup run reports every role whose account exists on the host but whose file is missing, owned by another account or open to others. Leave the user name out of the string (or write{role}there): each component supplies its own. Because the file is read when the connection is opened, by the account that opens it, a setuid stage such as pepsi-stage-encrypt reads thepepsi-cryptofile and the dispatcher that started it cannot. The string itself then holds no secret and stays in this file.Environment override. The environment variable
PEPSI_POSTGRES_CONFIG, when set and not empty, replaces this option and may carry a password. Every shipped systemd unit reads it fromEnvironmentFile=-/etc/pepsi/postgres.env(the leading-makes the file optional; keep it root-owned0600, since systemd reads it as root before starting the service). The setuid and setgid programs keep the variable only when root or thepepsidispatcher account started them, so the stages reach the dispatcher’s database while an ordinary user cannot point a privileged program at another one. A password in it is one password for every role, which gives up the separation between the components; prefer the per-role files above and use the variable for what differs between hosts. When it is set,CONFIGmay be omitted; a password inCONFIGis refused even then. A tool run from a shell (pepsi-setup run, pepsi-queue, …) needs the variable exported the same way, e.g.set -a; . /etc/pepsi/postgres.env; set +a.- SQL_DIR
(path, optional) Directory containing the SQL migration files. Only pepsi-setup(1) reads this, when installing the schema; the serving components ignore it. Defaults to
${DATADIR}/sql(i.e.<install-prefix>/share/pepsi/sql, wheremake installplaces the files). Override it when running from a source checkout.- WORKER_SYNCHRONOUS_COMMIT
(boolean, optional) Whether a stage worker’s database connection keeps PostgreSQL’s
synchronous_commiton. Defaults tono.With it off, a worker’s
COMMITreturns before the write-ahead log record has reached stable storage, which lets the pipeline’s writes group-commit instead of each paying its own flush. Nothing else changes: isolation, atomicity and visibility are unaffected, and a crash can never expose a partial transaction. What can be lost is the last few hundred milliseconds of durability — and only for stage transitions, never for message acceptance, because pepsi-ingress(1) always commits synchronously. A250still means the message is on disk.Losing a stage advance to a machine crash means the message is found still at the previous stage and runs through it a second time — exactly what already happens when a worker is killed after doing its work but before committing. Set this to
yesif a stage in your pipeline has side effects that must not be repeated even across a power loss, and accept the throughput cost.
85.2.1.1.7. The [pepsi-srs] Section¶
The Sender Rewriting Scheme parameters, shared by pepsi-stage-srs(1) (the
forward rewrite) and pepsi-ingress(1) (the reverse decode at RCPT
time), so the secret and domain match in both directions. Omitting the section
disables SRS: the stage refuses to run and ingress treats SRS-looking recipients
as ordinary addresses.
- SRS_DOMAIN
(string, required to enable SRS) Domain that rewritten envelope senders live under. It must be a domain you control: its MX must point at the Pepsi ingress (so bounces return) and its SPF must authorise Pepsi’s sending IPs. pepsi-setup(1) provisions keys and DNS for it. The MX is the half it cannot provision — where mail for a domain is routed is your decision — so it checks instead: with a stage running pepsi-stage-srs(1), a
SRS_DOMAINwith no MX and no address record is reported on every run, because an SRS address is a return path and a domain that cannot receive discards every bounce for the mail this host forwards. It does not have to be a dedicated subdomain; a domain you already accept mail for works and needs no new records at all (pepsi-ingress(1) decodes an SRS local-part before it looks up a mailbox, so ordinary addresses there are unaffected).- SECRET_FILE / SECRET
(required when SRS_DOMAIN is set) The secret keying the HMAC that signs SRS addresses, given as a file path (preferred; store it mode 0600) or inline. It must stay stable over time — a bounce returned days later must still verify — and be identical across all instances.
- MAX_AGE_DAYS
(integer, optional) How many days a rewritten address stays valid for returning bounces (SRS timestamps are day-granular). Default 21. Clamped to
1..``1021``: the day counter wraps after 1024 days, so a window at or above it would make every timestamp verify and disable expiry altogether.
The signature in a rewritten address is 8 base32 characters — 40 bits — and no
shorter one is ever accepted. A valid signature is not a small thing to forge:
pepsi-ingress(1) treats a verified SRS recipient as authorisation to relay
to the decoded address even when its domain is not one this deployment serves,
so a forged token is an open relay and arbitrary backscatter attributed to this
host. There is no option to accept a narrower signature: it would leave the
deployment only as strong as the narrower MAC for as long as it was set. The
per-connection cap on failed SRS recipients (pepsi-ingress(1) drops a
connection with 421 after three) bounds the online search separately;
neither bound substitutes for the other.
85.2.1.1.8. The [pepsi-origin] Section¶
Proof-of-origin keying, shared by both relay stages
(pepsi-stage-relay-to-internet(1) and pepsi-stage-relay-to-smarthost(1),
which stamp the Pepsi-Origin header on outbound mail and record its nonce) and
pepsi-stage-anti-spam(1) (which verifies the header on a returning bounce).
The feature is on by default: when no secret is configured, pepsi-setup(1)
generates a random one on first run (provided [pepsi-ingress] HOSTNAME is set).
The secret is not stored inline in this world-readable file; it is written to a
secrets.d/pepsi-origin.secret fragment beside the configuration (owned by the
pepsi dispatcher account, mode 0640) and pulled into this section with an
@inline-secret@ directive. Re-running setup reuses the existing fragment rather
than rotating the key. ENABLED = no disables the feature — outbound mail
is unstamped and every bounce is then unverifiable (routed per the stage’s
BOUNCE_TARGET_STAGE, else forwarded). Removing the section does not: the
next pepsi-setup(1) run would provision a new secret.
- ENABLED
(boolean, optional) Default
YES.NOswitches proof-of-origin off for every component that reads this section, whatever secret is configured, and stops pepsi-setup(1) from generating one. An existing secret fragment is left in place, so switching back on resumes with the same key.
The header carries the version, our HOSTNAME (the [pepsi-ingress]
option), the originating sender account, a 128-bit nonce and a timestamp, plus an
HMAC-SHA256 over all of them. Each stamped nonce is recorded in
pepsi.origin_nonce for two weeks; a bounce is trusted only if its embedded
header’s MAC verifies and its nonce is still tracked. See
pepsi-stage-anti-spam(1).
- SECRET_FILE / SECRET
The secret keying the HMAC, given as a file path (preferred; store it mode 0600) or inline. Like the SRS secret it must stay stable over time — a bounce returned days later must still verify — and be identical across all instances. When neither is set, pepsi-setup(1) provisions a random
SECRETin asecrets.d/pepsi-origin.secretfragment referenced by@inline-secret@(see above), keeping it out of the world-readable main configuration. The feature requires[pepsi-ingress] HOSTNAME(bound into the MAC).
85.2.1.1.9. The [pepsi-crypto] Section¶
Custody and lifecycle of the end-to-end key store — the
pepsi.crypto_identity, pepsi.peer_key and pepsi.ca_trust tables
managed by pepsi-keys(1). Like the [pepsi] CRYPTO_* options above
this is administrator-only, but it is about storage rather than about
cryptographic strength. Omit the whole section if the deployment does no
end-to-end cryptography; as soon as it says anything, pepsi-setup(1)
insists on a usable KEY_WRAP_SECRET, because a key store that cannot store
keys is a configuration mistake rather than a choice.
Private key material is kept in the database, sealed with AES-256-GCM under a
key-encryption key derived from KEY_WRAP_SECRET. The database alone therefore
never yields a private key: a dump, a replica or a backup tape produces
ciphertext, and the key that opens it is a separate artefact guarded by file
permissions. The other half of the boundary is a database grant —
crypto_identity.private_wrapped is granted to the pepsi-crypto role
alone; see pepsi-setup(1).
- KEY_WRAP_SECRET
(string, required once the section exists) The key-encryption key that wraps private key material at rest. Keep it out of this world-readable file: put it in a
secrets.d/pepsi-crypto.secretfragment and reference it with an@inline-secret@directive, exactly as the SRS and proof-of-origin secrets are handled. Every pepsi-setup(1)runre-asserts mode0640on that fragment and hands it to thepepsi-cryptoaccount. Fewer than 16 characters is refused.Back it up separately from the database. If it is lost, every stored private key is gone: mailboxes hold plaintext, so nobody’s mail becomes unreadable, but in-flight and archived ciphertext is lost, every published identity is invalidated and the organisation must re-key.
- KEY_WRAP_KEY_ID
(string, optional) Identifier of the key-encryption key new material is wrapped under. Default
k1. Must be a non-empty run of ASCII letters, digits and underscores, because it is spliced into theKEY_WRAP_SECRET_<ID>option name a retired key is read from. Each row records the id that sealed it, which is what makes rotation incremental rather than a flag day.- KEY_WRAP_SECRET_<ID>
(string, optional) A retired key-encryption key, kept until every row wrapped under it has been rotated.
<ID>is thewrap_key_idit names, upper-cased (soKEY_WRAP_SECRET_K0for keyk0). Between introducing a new key and runningpepsi-keys wrap rotatethe deployment keeps working: new material is sealed under the new key while old material still opens under this one. Remove it once the rotation reports everything re-wrapped.- AUTO_CREATE_IDENTITY
(boolean, optional) Whether an identity may be generated automatically for a served address. Default
YES. pepsi-stage-encrypt(1) is what acts on it, and only for an address in a[pepsi-ingress] ACCEPTED_DOMAINSdomain that has nocrypto_identityrow of any kind yet (a revoked key or the user’s own registered key counts), underCRYPTO_OPENPGP_ALGORITHM/CRYPTO_SMIME_ALGORITHMandIDENTITY_VALIDITY_DAYS. When it fires depends on that stage’sENABLE_PEP:ENABLE_PEP = yes(the default): eagerly, on the sender’s first locally submitted message, whether or not it asked for protection. Every sender at a served domain therefore ends up with a wrapped private key on this server, and the deployment’s WKD serves its public half.ENABLE_PEP = no: lazily, the first time a local sender explicitly asks for protection (to sign or only to encrypt), or on every message under that stage’sSIGN = always; never merely because an address appeared in an envelope, so a user who never uses the feature never has key material.
This option is the operator’s veto over both: it lives in an administrator-only section, so no per-address
pepsi.settingsoverride can turn automatic creation back on. With it off, every identity is created on request – pepsi-keys(1), the web console, or the e-mailgeneratecommand.Because the trigger is on the outbound path, an address that only ever receives stays keyless and is never published — outsiders therefore have nothing to encrypt to it with. Pre-provision such addresses with
pepsi-keys identity generate.- IDENTITY_VALIDITY_DAYS
(number, optional) Lifetime of a generated identity, in days. Default
730(two years); must be positive. An integer count of days rather than adurationbecause the parser rejects calendar units (see the warning at the top of this page).pepsi-keys identity generate --daysoverrides it for one identity.- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(string, optional) Which domains and accounts an address is attributed to when deciding who owns an identity, whether pepsi-keys(1), the setup applier or pepsi-stage-encrypt(1) creates it — the same option names, meanings and defaults as the local-delivery stages’ options (pepsi-stage-relay-to-maildir(1)).
LOCAL_DOMAINSdefaults to[pepsi-ingress] ACCEPTED_DOMAINS,TARGETSto the/etc/login.defsUID_MIN..``UID_MAX`` range andRECIPIENT_DELIMITERto+. An address that resolves to a local account has that login recorded with its identity and may be managed by that user; one that does not (a role address, a hosted domain) is operator-only.
85.2.1.1.10. The [pepsi-keydiscovery] Section¶
How Pepsi finds a correspondent’s public key or certificate — the input to
pepsi.peer_key, as distinct from [pepsi-crypto] above, which is about
custody of our own key material. Read by the pepsi-keydisc(1) discovery
services, by pepsi-keys(1) (whose peer refresh --address runs the same
fan-out in one process), and by any stage that must park a message on a key it
does not yet have.
The whole section is optional: with none of it, discovery runs with the default source set.
Discovery is asynchronous. A stage that needs a key it does not have in
cache performs no network I/O of its own — it commits its work, pauses the
message and enqueues a request in pepsi.key_request, and a discovery service
answers the request and releases every message parked on that address. See
pepsi-keydisc(1) for the service side and pepsi.state(7) for the
keydisc key a parked message carries.
- SOURCES
(string, optional) Which discovery methods this deployment runs, as a comma- or whitespace-separated list of
dane,wkd-advanced,wkd-direct,ldapandvks(wkdis accepted forwkd-advanced,hkpforvks; duplicates are ignored). Defaultdane wkd-advanced wkd-direct vks—ldapis deliberately absent, being feature-gated and useless until configured.This list is also the roster the stop rule waits for, which has two consequences. A
pepsi-keydisc@<method>instance whose method is not listed refuses to start; and a listed method with no instance running merely costs every request the fullTIMEOUTbefore it settles. Keep the list and the running units in step:pepsi.targetstarts exactly the four default methods, so leave the option unset unless you also mask or start instances.inboundandgossip(and their aliasesharvested/autocryptandautocrypt-gossip) are rejected: learning a key out of a message is not a service – it runs inline in pepsi-stage-autocrypt-learn(1) from material already in the message – so nothing would ever answer for it. An empty list is refused too; remove the option to take the default.- MIN_TRUST
(string, optional) The trust floor — a key from a worse-ranked source is not used. Named by its source:
manual(alsoapioroperator),dane,wkd-advanced(alsowkd),wkd-direct,ldap,vks(alsohkp),harvested(alsoinboundorautocrypt), orgossip(alsoautocrypt-gossip,any,none). Defaultany— accept anything.One further spelling,
owner-confirmed(owner_confirmed), names a rung by what the evidence is rather than by the method that produced it: it resolves tovks, the weakest rung at which somebody who could prove control of the mailbox or the domain published the key on purpose. It exists so that inserting a rung betweenvksandharvestedcannot silently move that line. It is also the default of[stage-encrypt] GOSSIP_MIN_TRUST.The alternative to encrypting to a trust-on-first-use key is not a better key, it is cleartext. Raising it is how a deployment that would rather send nothing than send to an unauthenticated key says so. The ranking still does work at the default: it decides conflicts, and it is recorded per key.
any/nonename the bottom rung, whatever it currently is, while every other spelling names one rank exactly. The distinction matters for the two that look alike:harvestedis rank 7 — what a correspondent said about their own address, in anAutocrypt:header or an attached key — andgossipis rank 8, one correspondent’s introduction of another (Autocrypt Level 1 §5.3). SoMIN_TRUST = harvestedis the useful middle setting: keep opportunistic encryption, refuse third-party introductions. Gossiped keys are still stored either way; the floor decides only whether they may be used.The floor does not apply to an address this deployment holds an identity for. Our own key is not a discovered key: it pre-empts the lookup, so no
peer_keyrow for that address is consulted whatever its rank, and no floor can exclude it.own(alsolocal) is accepted as a spelling for completeness — every rank has a name andstate.cryptorenders this one — but as a floor it means “encrypt only to addresses we hold our own key for”, i.e. no external correspondent ever receives ciphertext. See pepsi-stage-encrypt(1) and pepsi-stage-decrypt(1).- TIMEOUT
(duration, optional) Whole-request deadline: how long a request may stay outstanding before it settles with whatever has been found. Default
60 s. Usesh/m/sunits only (see the duration note above).- INBOUND_TIMEOUT
(duration, optional) The same deadline for a message parked on the inbound path — waiting for the key that verifies a signature — defaulting to
TIMEOUT. It is separate precisely so it can be lowered on its own: an inbound park delays exactly the mail somebody is waiting for, so an operator who feels it can drop this to seconds without shortening the outbound deadline. pepsi-setup(1) warns above five minutes.- RANK_GRACE
(duration, optional) The speed-versus-thoroughness dial. When a lookup succeeds, every method still running that ranks better is granted
RANK_GRACE× (how many ranks better it is) of extra time; when that expires, the best answer in hand wins. Default5 s, so one rank up gets 5 s and three ranks up gets 15 s.One knob spanning the whole range:
0means the first usable answer wins outright; the default sits where a slow WKD server cannot hold a message behind a key-server hit that has already arrived, but a nearly-as-fast better source still gets to win; a value at or aboveTIMEOUTmeans every method is waited for and the ranking applies in full (pepsi-setup(1) notes that case rather than treating it as an error — it is a legitimate choice).- METHOD_TIMEOUT
(duration, optional) One method’s own network deadline, covering connect, handshake and every read. Default
15 s. Setting it aboveTIMEOUTmakes a slow method unable to finish inside the whole-request deadline, which pepsi-setup(1) warns about.- CACHE_TTL
(duration, optional) How long a stored key is used before
pepsi-keys peer refreshre-checks it (it becomes the row’srefresh_after). Default24 h.- NEGATIVE_TTL
(duration, optional) How long an authoritative “this address has no key” is remembered. Default
24 h. While the entry is fresh a message for that address takes the no-key path immediately and never parks: pausing 60 s to re-learn a known answer only delays every message to that correspondent. The accepted cost is that a key published five minutes ago stays invisible until the entry ages out — lower this if that matters more than the delay.- ERROR_TTL
(duration, optional) The same, after a method failed rather than answering “nothing here”. Default
1 h: “the server was down” is worth re-trying sooner than “there is no key”.- MAX_RESPONSE_BYTES
(number, optional) Cap on a fetched WKD or VKS response body, enforced while streaming rather than afterwards, so a 10 MB body costs the cap and not 10 MB. Default
262144(256 KiB). Must be greater than zero.- VKS_SERVERS
(string, optional) Verifying key servers to query, in order, as a comma- or whitespace-separated list. Default
https://keys.openpgp.org. Every entry must be anhttps://URL — a key fetched over an unauthenticated channel is not a key there is any reason to believe — and a trailing/is stripped.- ALLOW_DOMAINS, DENY_DOMAINS
(string, optional) Recipient-domain lookup policy, comma- or whitespace-separated and matched case-insensitively on the domain part. A non-empty
ALLOW_DOMAINSis exclusive (only those domains are ever looked up);DENY_DOMAINSalways wins over it. Both empty by default, so every domain may be looked up. An address with no@is never looked up.This is the recipient-side privacy control, and it is the operator’s. The per-user switch below is the sender side; the two are not interchangeable.
- LDAP_URL, LDAP_BASE, LDAP_FILTER, LDAP_ATTRIBUTE, LDAP_BIND_DN, LDAP_BIND_PASSWORD
(string, optional) The directory
pepsi-keydisc@ldapqueries.LDAP_URLmust be anldap://orldaps://URL and, withLDAP_BASE, is required onceldapis inSOURCES.LDAP_FILTERdefaults to(mail={address})and must contain the{address}placeholder (the address is RFC 4515-escaped before substitution, so a local part containing*or(cannot become filter syntax);LDAP_ATTRIBUTEdefaults touserCertificate;binary. An absentLDAP_BIND_DNmeans an anonymous bind; a present one requires anldaps://URL, because a simple bind sends the password verbatim and the lookup is refused rather than performed in the clear (the same rule the LMTP and SMTP clients apply toAUTH).LDAP support is a compile-time cargo feature. On a build without it,
ldapinSOURCESis rejected by pepsi-setup(1) and thepepsi-keydisc@ldapinstance refuses to start.
85.2.1.1.10.1. The per-sender switch: [stage-encrypt] DISCOVERY¶
A stage that consumes the key store reads one option of its own, in its
[stage-<name>] section, so it can be overridden per address through the
pepsi.settings layer (pepsi-settings(1)):
- DISCOVERY
(boolean, optional) Whether a cache miss may trigger a network lookup. Default
YES. WithNOthe cached keys are still used, but a miss goes straight to the stage’s no-key path instead of parking the message — there would be nothing to wait for.
Important
What the per-address override actually means. The settings layer resolves
an outbound message on its envelope sender, so an override of
DISCOVERY for an address means “this user’s mail never triggers a
lookup” — not “never look this correspondent up”. Recipient-side
suppression is the operator’s ALLOW_DOMAINS/DENY_DOMAINS above; there
is deliberately no per-address recipient denylist, which would need its own
column and a second list to stay consistent with this one.
DISCOVERY is read by pepsi-stage-encrypt(1) and
pepsi-stage-decrypt(1) from that stage’s effective section, so a
pepsi.settings override applies. Their other options are documented under
pepsi-stage-encrypt Options and pepsi-stage-decrypt Options.
85.2.1.1.11. The [pepsi-keys] Section¶
The mirror image of the section above: how other people find our users’ keys. Read by pepsi-keys(1) and by pepsi-stage-vks-confirm(1). The whole section is optional, and a configuration that omits it publishes nothing to any key server.
Web Key Directory publication needs no options at all. It follows each
identity’s published flag (on by default, cleared with pepsi-keys identity
unpublish) and is served by pepsi-httpd(1) from our own domain, so it
takes effect on the next request and is undone by clearing the flag. What this
section configures is the other channel, which behaves nothing like it: a key
server accepts a key permanently — it can later be told the key is revoked,
never told to forget it.
- VKS_PUBLISH
(boolean, optional) Whether an identity created or imported by hand (
pepsi-keys identity generate/import, the web console’s or the API’sgenerate, the e-mailgeneratecommand) starts with its key-server upload requested (crypto_identity.vks_wanted). Default NO.Each identity records whether its upload was asked for, and the retry job (
pepsi-keys identity publish --retry) uploads exactly the published, active OpenPGP identities that havevks_wantedset, retrying until verified. It is not gated by this option. Two consequences:automatically generated keys (
[stage-encrypt] ENABLE_PEP) and MUA keys registered from a user’s own mail are never uploaded unasked, whatever this says – Autocrypt and the deployment’s own WKD are how they are found;a user’s explicit request to upload (
pepsi-keys identity publish <ID> --vks, the web console’s Upload to the key server,PATCH /api/v1/identities/{id}withvks_wanted, or the e-mailpublishcommand) is honoured whatever this says.
“Not uploaded” is therefore recorded state rather than an absence, and
pepsi-keys identity showprints it. An upload cannot be withdrawn, which is why the default for keys made by hand stays an operator’s decision.- VKS_SERVER
(string, optional) The key server uploads go to;
https://only, and a plaintext URL is refused rather than downgraded. Defaults to the first[pepsi-keydiscovery] VKS_SERVERSentry, so a deployment that already chose a key server to read from does not name it twice.One server, not a list: publishing the same key to several multiplies an irreversible act, and every one of them then has to be kept up to date with revocations.
Its host is also the default
VKS_HOSTof pepsi-stage-vks-confirm(1) — the one host that stage will accept a verification mail from and follow a link to.- VKS_MAX_ATTEMPTS
(integer, optional) How many attempts one identity gets before
pepsi-keys identity publish --retryleaves it alone. Default5. The row keeps its last error, so an abandoned identity says why.- VKS_RETRY_INTERVAL
(duration, optional) Minimum spacing between attempts for one identity. Default
6 h. Calendar units are rejected:6 hparses,1 ddoes not.- VKS_BATCH
(integer, optional) How many identities one
--retryrun works on, so a first run over a large deployment is not one enormous burst at a third party. Default50.
85.2.1.1.12. The [pepsi-ingress] Section¶
Global options governing message acceptance, read by pepsi-ingress(1).
- HOSTNAME
(string, required) Host name announced in the SMTP greeting and the
EHLOresponse, e.g.mail.example.org.- ACCEPTED_DOMAINS
(string, required) Whitespace- or comma-separated list of domains for which mail is accepted. Recipients in any other domain are rejected as relaying with a
550reply. Matching is case-insensitive. At least one domain must be given.- USERNAME_MAP
(path, optional) File mapping each SASL authentication id to the e-mail addresses it may use as the envelope sender and
From:identity (RFC 6409 §6/§8.1). Each non-blank, non-comment line isusername: addr…— a#begins a comment, the key is the SASL username, and the value is the whitespace/comma separated list of allowed addresses (which may be empty). A username listed with addresses is allowed exactly those (the first concrete one being the canonical identity used for theSender:fixup); an address may be a*wildcard such as*@example.org(any address at the domain) or a bare*(any address). A username listed with no addresses is denied any address (itsAUTHis refused even though the credentials are valid); a username not listed falls back to the defaultusername@HOSTNAME. The file is re-read whenever its modification time changes (checked on each authentication); a missing/unreadable file is treated as empty. When unset, every authenticated user is mapped to its defaultusername@HOSTNAME. See pepsi-ingress(1).- POSTMASTER
(string, optional) Routable address the domainless reserved
<Postmaster>mailbox is rewritten to. RFC 5321 §4.5.1 requires<Postmaster>(with no domain) to be accepted regardless ofACCEPTED_DOMAINS; on acceptance Pepsi rewrites it to this address so the normal pipeline (aliases, local delivery, relay) can route it. When unset it is rewritten topostmaster@HOSTNAME— add an alias for that address in apepsi-stage-aliasesALIASESmap to deliver postmaster mail to a real person, or setPOSTMASTERto that person directly. See pepsi-ingress(1).- MAX_MESSAGE_SIZE
(number, optional) Maximum accepted message body size in bytes — the size of the
DATApayload. The value is advertised through the ESMTPSIZEextension; messages exceeding it are rejected with552. Defaults to26214400(25 MiB).- MAX_CONNECTIONS
(number, optional) Maximum number of SMTP connections served concurrently across all listeners. The effective admission ceiling is the smaller of this and
MAX_OPEN_SOCKETS. Defaults to1024.- DB_POOL_SIZE
(number, optional) Maximum number of PostgreSQL connections in the ingress database pool. Ingress serves many SMTP sessions concurrently, each briefly calling
workqueue_add, so unlike the single-connection stage workers it can use more than one. Defaults to1; raise it only if inbound acceptance is bottlenecked on the single connection, and keep the sum of every component’s pool below PostgreSQL’smax_connections.- MAX_OPEN_SOCKETS
(number, optional) Hard ceiling on concurrently open client connections, guarding against file-descriptor exhaustion. When unset, it is derived from the process’s soft
RLIMIT_NOFILEminusRESERVED_SOCKETS. When the ceiling is reached and a new connection arrives, the server makes room by closing the longest-idle connection in the source range (one IPv4 address, or an IPv6/32) whose total idle time is largest (a421, then close).- RESERVED_SOCKETS
(number, optional) File descriptors held back from the
RLIMIT_NOFILE-derived budget (for the database pool, listeners, stdio and DNS sockets). Defaults to64. Ignored whenMAX_OPEN_SOCKETSis set.- CONN_RATE_PER_SECOND
(number, optional) Token-bucket refill rate limiting accepted new connections per source range, in connections per second. A connection over the rate is refused with
421and closed without taking a slot. Defaults to1. A fractional value is accepted (0.2is one connection every five seconds). Local (UNIX-socket) connections are never rate-limited.- CONN_RATE_BURST
(number, optional) Token-bucket burst allowance per source range, likewise fractional. Defaults to
1.- MAX_CONNECTIONS_PER_IP
(number, optional) Concurrent connections one source range (one IPv4 address, or an IPv6
/32) may hold. A connection over it is refused like a rate-limited one. Defaults to16;0disables the cap.- MAX_LOCAL_CONNECTIONS
(number, optional) Concurrent connections all UNIX-socket peers together may hold. Local connections are exempt from the rate limit and never evicted, so this is what keeps local submitters from holding every slot. Defaults to
32;0leaves them bounded only byMAX_CONNECTIONS.- MAX_MESSAGES_PER_SESSION
(number, optional) Mail transactions one session may start; the next
MAILis answered421and the session closed. Defaults to100;0is unlimited.- VERIFY_RECIPIENTS
(``auto`` or ``off``, optional) Whether an address at a served domain that nothing on this host would accept is refused at
RCPTtime with550 5.1.1. Defaultauto. The sending MTA then tells its own user at once. Accepted, the message would be bounced later to its envelope sender — which spam forges, so the bounce hits an innocent third party (backscatter) and gets this host blocklisted.Nothing needs to be listed by hand. For each served domain, the sources that can vouch for an address are derived from the
[stage-*]sections:the local accounts a pepsi-stage-relay-to-maildir(1) or pepsi-stage-dot-forward(1) resolves for its
LOCAL_DOMAINS,TARGETSandRECIPIENT_DELIMITERincluded;the MDA behind a pepsi-stage-relay-to-lmtp(1), asked for its
LOCAL_DOMAINSwithRCPT TOand no message;the backend behind a pepsi-stage-route(1), as its RECIPIENT_CHECK says;
every pepsi-stage-aliases(1) map, with the stage’s own matching;
the mailing lists, when a pepsi-stage-list(1) router is configured;
postmaster@andabuse@, and the control addresses of the stages that answer one (pepsi@,pepsi-keys@,pepsi-wallet@,secretary@).
An address any of them vouches for is accepted. So is every address at a domain that cannot be checked:
an alias catch-all covers the domain;
a stage runs a program Pepsi does not know;
the domain is routed to a backend with
RECIPIENT_CHECK = none;the domain is relayed onward by a smarthost stage with nothing here holding its mailboxes.
Every uncertainty accepts too: an unreadable file, a database error, a probe that fails or times out. pepsi-setup run prints the result for every domain.
offaccepts every address at a served domain, as before. Read at start-up: a changed stage graph needs an ingress restart, while edits to an alias map, a recipient list or the mailing lists apply at once.- MAX_INVALID_RECIPIENTS
(number, optional) Unknown recipients one session may name. At the limit the session is closed with
421, which turns a dictionary attack (harvesting valid addresses, or making the MDA answer guess after guess) into a stream of connections the connection limits meter. Defaults to10;0is unlimited.- MAX_SELF_HOPS
(number, optional) How many times a message may already have passed through this host’s ingress before it is refused as a routing loop, with
554 5.4.6. Counted from theReceived:fields ingress itself writes (byHOSTNAME(pepsi-ingress)). Legitimate mail re-enters a few times at most: an alias to another served domain, a~/.forward, or a report to one of our own users each leave over SMTP and come back in. A loop, by contrast, would otherwise run until the generic hop limit of the relay stages, dozens of passes. Defaults to5;0disables the check.- AUTH_FAILURES_PER_HOUR
(number, optional) Failed
AUTHattempts one source range may make per hour across all of its sessions (a token bucket). Once spent,AUTHfrom the range is refused with421without consulting the SASL backend until the bucket refills. Defaults to20;0is unlimited.- LOCAL_MESSAGES_PER_MINUTE
(number, optional) Messages one local uid, identified by
SO_PEERCREDon a UNIX-socket session, may submit per minute across its sessions; aMAILover the rate gets451 4.7.1. Defaults to60;0is unlimited.- LOCAL_MESSAGE_BURST
(number, optional) The burst allowed on top of
LOCAL_MESSAGES_PER_MINUTE. Defaults to120.- COMMAND_TIMEOUT
(duration, optional) Longest a session waits for the next command (or
AUTHcontinuation, the start of aDATA/BDATbody, or completion of aSTARTTLS/implicit-TLS handshake) before the connection is closed with421(RFC 5321 §4.5.3.2). Defaults to300 s.- DATA_TIMEOUT
(duration, optional) Longest a session waits for the next block of payload octets while receiving a
DATA/BDATbody before closing with421. Defaults to180 s.- DMARC_ENFORCE
(boolean, optional) When
YES, reject at SMTP time (550) a message whose DMARC evaluation fails and whose published policy isquarantineorreject. Defaults toNO: pepsi-ingress(1) normally accepts such mail (and pepsi-stage-arc(1) seals the failing result) so the downstream receiver can decide. Other authentication failures never reject a message.- DNS_SERVERS
(string, optional) Whitespace- or comma-separated list of resolver IP addresses (IPv4 and/or IPv6) used for the SPF/DKIM/DMARC lookups, queried on port 53. If unset, the system resolver configuration (
/etc/resolv.conf) is used.- DNS_TIMEOUT
(duration, optional) Per-query timeout for those lookups. Defaults to
5 s. Because verification runs synchronously while receivingDATA, a slow resolver delays the250acceptance.It applies only when DNS_SERVERS names the resolvers. With
DNS_SERVERSunset the system resolver configuration is used whole, timeout included, so this option has no effect and theoptions timeout:/attempts:lines of/etc/resolv.confare what bound a query.- MAX_DKIM_SIGNATURES
(number, optional) How many
DKIM-Signaturefields of one inbound message are verified; the ones whosed=is theFrom:domain or a parent or child of it are taken first. Defaults to10.- VERIFY_TIMEOUT
(duration, optional) Deadline on inbound verification: SPF and the selected DKIM signatures are checked concurrently under it, then DMARC under a second one. A check that misses it is recorded as
temperrorfor that check only. Defaults to20 s.- DISPATCH_WAKE_INTERVAL
(duration, optional) Minimum spacing between two
NOTIFYs telling pepsi-dispatch(1) that new mail has arrived. Defaults to5 ms;0disables coalescing and notifies once per accepted message.Ingress does not notify from inside the transaction that stores the message. PostgreSQL holds a database-wide lock from the moment a transaction queues a notification until it commits, so notifying transactions cannot group-commit — under concurrent load they serialise, each paying its own disk flush. Admissions are therefore notified out of band, once per interval, after the rows have committed; because the dispatcher answers any wake-up with a full scan of the queue, one notification serves an arbitrary number of newly accepted messages.
The limit is trailing: the first wake-up after an idle period is sent immediately, so a lone message is not delayed at all, and only notifications behind it are merged. Raising this trades a little worst-case queue latency for fewer notifications under sustained load; there is rarely a reason to.
The ARC sealing identity and algorithm are configured in the shared [pepsi]
section (ARC_DOMAIN and ARC_ALGORITHM) above and used by
pepsi-stage-arc(1), not by ingress.
85.2.1.1.13. The [pepsi-ingress-listener-*] Sections¶
Each section whose name begins with pepsi-ingress-listener- defines one
listening socket. The suffix is a free-form name used in log messages, e.g.
[pepsi-ingress-listener-mx]. Declare as many listeners as required.
- SERVE
(required) The kind of socket to bind. One of:
tcpBind a TCP socket. Requires BIND_TO and PORT.
unixBind a UNIX-domain socket. Requires UNIXPATH; honours UNIXPATH_MODE and UNIXPATH_GROUP.
systemdAdopt a socket passed by a systemd parent through socket activation; see FD_INDEX.
- BIND_TO
(string) IP address to listen on when
SERVE = tcp, e.g.0.0.0.0or::. Required fortcp.- PORT
(number) TCP port to listen on when
SERVE = tcp. Required fortcp.- UNIXPATH
(path) Filesystem path of the socket when
SERVE = unix. Required forunix.- UNIXPATH_MODE
(unix mode, optional) Permission bits of the UNIX-domain socket, in octal. Defaults to
660.- UNIXPATH_GROUP
(string, optional) Group name the UNIX-domain socket is
chgrped to after binding (the owner is unchanged). Combined with the default0660mode this lets a specific peer reach the socket while it stays non-world-accessible — used when pepsi-httpd runs behind a front web server (e.g.www-data) that reverse-proxies to it. The group must exist; an unknown group is a startup error.- FD_INDEX
(number, optional) Zero-based index of the systemd-passed file descriptor to adopt when
SERVE = systemd. Index0is the first descriptor (SD_LISTEN_FDS_START). Defaults to0.It is a position, not a name: it counts
ListenStream=entries in the owning.socketunit, so inserting one renumbers every listener after it while appending one renumbers nothing. The unit and these sections are two halves of one statement and nothing checks that they agree at configuration time, so a mismatch is reported where the descriptors actually are: the server logs a warning at start-up naming every index no listener claimed. Take it seriously — a socket the server never accepts on is worse than an absent one, because systemd still queues the connection and the client blocks until its own read timeout expires rather than being refused at once.A descriptor may be a UNIX-domain socket as readily as a TCP one; which it is lives in the unit, not here. Options that depend on the socket’s family are therefore resolved once the descriptor is in hand rather than while parsing: AUTH_PEERCRED must be stated explicitly (it cannot be inferred as it is for
SERVE = unix), and a plaintext SUBMISSION listener that turns out to have inherited a network socket makes pepsi-ingress(1) refuse to start rather than carry credentials in the clear.- MODE
(optional) Transport security of the listener. One of:
plainCleartext only;
STARTTLSis not advertised. This is the default.tlsImplicit TLS: the connection is wrapped in TLS from the first byte (e.g. the
submissionsport 465). Requires TLS_CERT and TLS_KEY.starttlsCleartext that may be upgraded with the
STARTTLScommand. Requires TLS_CERT and TLS_KEY.
- TLS_CERT
(path) PEM file containing the server certificate chain. Required when MODE is
tlsorstarttls— but may be omitted, in which case pepsi-setup(1) auto-fills the certbot path/etc/letsencrypt/live/<HOSTNAME>/fullchain.pemand can obtain the certificate (see pepsi-setup(1) and its –no-certbot option).- TLS_KEY
(path) PEM file containing the private key for TLS_CERT. Required when MODE is
tlsorstarttls; like TLS_CERT it may be omitted for pepsi-setup(1) to auto-fill (…/privkey.pem).- MYNETWORKS
(optional) Whitespace/comma separated list of trusted IPv4/IPv6 CIDR networks (a bare address is a
/32or/128host). A connection from any of these is authenticated without further proof. Empty by default.- AUTH_PEERCRED
(optional, default ``yes`` on a ``SERVE = unix`` listener, ``no`` elsewhere) Authenticate the client by its kernel-reported UNIX-socket peer credentials (
SO_PEERCRED): the connecting process’s user id is resolved to a login through the passwd database, and that login is looked up in USERNAME_MAP exactly as a SASL username would be. The session is then authenticated and carries a submission identity, so the RFC 6409 §6/§8.1 rules apply — a local program can only use an envelope sender andFrom:its own account is mapped to, and an account mapped to no address is refused.This is the mechanism behind pepsi-sendmail(1) and the reason the socket may safely be world-writable: being able to open it decides nothing about who you can send as. Unlike MYNETWORKS, which trusts a network position and therefore lets any local process send as anyone, the identity here is supplied by the kernel and cannot be forged by the client.
Rejected on a
SERVE = tcplistener, where there are no peer credentials to read. Set it tonofor an unauthenticated local socket (for instance behind a proxy) — but such a listener cannot then also be a SUBMISSION listener, since nothing would authenticate.With
SERVE = systemdit is accepted but not inferred, and so must be written out: whether the inherited descriptor is a UNIX socket or a TCP one is a property of the.socketunit rather than of this file, and the shippedpepsi-ingress.socketpasses both kinds. If the descriptor turns out to be a TCP socket the option authenticates nobody —SO_PEERCREDhas no meaning there, so every session stays anonymous — which is the safe direction but a silent one, so pepsi-ingress(1) warns at start-up naming the listener.- SASL_TYPE
(optional) SASL backend for the
AUTHcommand:none(default) ordovecot.AUTH(mechanismsPLAINandLOGIN) is advertised and accepted only on a TLS-protected session; a cleartextAUTHis refused with504 5.5.4(RFC 4954 §4 – the538of RFC 2554 is deprecated), andAUTHon a listener with no backend with503 5.5.1. Any value other thannonerequires MODEtlsorstarttls.- SASL_PATH
(path) Path of the Dovecot auth-client socket. Required when SASL_TYPE is
dovecot— there is no default, and a listener that asks fordovecotwithout it fails to parse. The pathpepsi-setupproposes,/run/dovecot/auth-client-pepsi, is a Pepsi-private listener rather than Dovecot’s sharedauth-clientsocket: the latter is0600 dovecotand unreachable by the unprivilegedpepsi-ingressuser, so pointing this at it gives a submission listener whoseAUTHalways fails with a permission error.pepsi-setup runprobes this socket as the service user and, if it is missing or unreadable, offers to install a/etc/dovecot/conf.d/10-pepsi.confthat adds a dedicated listener owned bypepsi-ingress(or prints it for you to deploy).- TLS_AUTH_CLIENT
(optional) Whitespace/comma separated list of SHA-256 hashes (64 hex characters) of the DER
SubjectPublicKeyInfoof authorised client or CA public keys. When set, the listener requests a client certificate: a session is authenticated if the presented leaf’s key hash is listed, or if its chain signature-validates up to a listed CA key. An unrecognised certificate does not abort the handshake. Requires MODEtlsorstarttls. Compute a hash withopenssl x509 -in cert.pem -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256.- SUBMISSION
(optional, default ``no``) When
yes, the listener is an RFC 6409 message submission agent: authentication becomes mandatory (an unauthenticatedMAIL FROMis refused with530 5.7.0), and every accepted message gets the submission fixups — a missingDate:andMessage-ID:are added. Requires at least one authentication mechanism (SASL_TYPE, TLS_AUTH_CLIENT, MYNETWORKS or AUTH_PEERCRED); a submission listener with none is rejected. Leave it off for an MX listener (port 25).A TLS MODE is required too, except on a
SERVE = unixlistener: a filesystem socket has no transport an attacker could observe or interpose on, and its authentication comes from the kernel rather than from the wire, so requiring a certificate there would protect nothing.
A session accepted by any of MYNETWORKS, AUTH_PEERCRED, SASL AUTH or
TLS_AUTH_CLIENT is recorded with state.local_origin = true (otherwise
false) and may relay to any domain, not only the served domains.
85.2.1.1.14. The Stage Pipeline and [stage-*] Sections¶
After ingress, a message is advanced by a chain of stage programs.
pepsi-dispatch(1) claims a pending row, sets it running and hands
the message id to a persistent worker process of that stage — started as
PROGRAM [-c FILE] worker and fed ids on its standard input, one per line,
answering with one status line each — and the program does its work and advances
the row (or finishes it). There is no positional-id invocation: a PROGRAM
that is a wrapper script must therefore forward its arguments and its standard
input. New messages enter at the initial stage, [stage-init], which must
exist.
Each section whose name begins with stage- describes one stage. The suffix
is the stage name, e.g. [stage-init]. The stage name is an
operator-chosen label, independent of the binary it runs.
- PROGRAM
(string, required) The stage binary to run, e.g.
pepsi-stage-relay-to-smarthost. It is located on thePATHunless given as an absolute path. The same binary may serve several stages (it reads its options from whichever section the message is currently at).- NEXT_STAGE
(string, optional) The stage a message advances to on success. With none, a successful terminal stage removes the message.
- BOUNCE_STAGE
(string, optional) The stage a message is routed to so a delivery-status notification is generated. For delivery stages (pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-internet(1)) and pepsi-stage-discard(1) it receives a permanently-failed (non-bounce) message for a bounce, and — when
[pepsi]ORIGINATE_SUCCESS_DSNis enabled — a successfully-delivered message for a positive report. With none, a permanently-failed message is markedfailed(delivery stages) or simply dropped (pepsi-stage-discard(1)).- PARALLELISM
(number, optional) Upper bound on the number of concurrent worker processes pepsi-dispatch(1) runs for this stage. Workers are started on demand and stopped when idle, so this is a ceiling, not a fixed pool size. Each worker holds one database connection, so this also bounds the stage’s share of the connection budget (see the
[pepsi-postgres]note above). Default 4. The dispatcher temporarily lowers a stage below this value when the database reports its connection limit is exhausted (see pepsi-dispatch(1)).- MAX_MESSAGES
(number, optional) Number of messages a worker of this stage handles before the dispatcher retires it and starts a fresh process (bounding memory growth). Default 1000.
- QUEUE_LIMIT
(number, optional) Upper bound on the number of messages pepsi-dispatch(1) pipelines to a single worker process of this stage at once. The worker still processes them strictly one at a time, but having the next ids already queued on its standard input removes a coordinator round-trip per message, so the stage’s total in-flight capacity is
QUEUE_LIMIT × PARALLELISM. Because the worker count (and thus the connection budget) is unchanged, raising this trades a little head-of-line latency (a message can wait behind a slower pipelined sibling on the same worker) for throughput, without spending more database connections; lower it (e.g. to1) for a stage whose per-message work is long and uneven, such as a network relay. Default 4.- MAX_LIFETIME
(duration, optional) How long a message may keep failing at this stage — with any error the stage does not mark permanent, which is to say a fault of the host rather than of the message, such as an unreadable resolver configuration, a helper that cannot be started or a template that cannot be read — before the worker gives up, counted from when the message was queued (for a DSN, from when pepsi-stage-bounce(1) built it). Until then the message is paused and retried, after one minute and then twice as long each time, up to an hour, with the error in
state.last_error. Then it is rerouted to this stage’s BOUNCE_STAGE, or failed when there is none (see pepsi-dispatch(1)). The delivery stages and pepsi-stage-milter(1) read the same option for their own retries. Default120 h.- FUSION =
yes|no (optional) Whether a predecessor stage may run this stage in its own worker process instead of writing the row
pendingand waiting for the dispatcher to claim it (stage fusion; see[pepsi] ALLOW_FUSIONabove and pepsi-dispatch(1)). Fusion happens only when it is also globally enabled, this stage’s PROGRAM is folded into the unifiedpepsibinary, and this stage needs no message column the predecessor did not already load — so it is refused (and the advance falls back to a normal dispatched hop) whenever any of those does not hold, which is always safe. Defaults toyesfor the fast, body-free routing/classification stages — pepsi-stage-if(1), pepsi-stage-discard(1), pepsi-stage-srs(1), pepsi-stage-check-whitelist(1), pepsi-stage-auto-whitelist(1), pepsi-stage-block-language(1), pepsi-stage-list(1) and pepsi-stage-edit-settings(1) (which is body-free for every message that is not a control message, i.e. effectively all of them) — andnofor every other program. Two of those need the header block rather than the envelope alone — pepsi-stage-block-language(1), whoseENFORCEMENT = softpath rewrites theSubject:, and pepsi-stage-check-whitelist(1), which readsList-Id:— so each fuses only into a predecessor that had already loaded it. Listing a program here only sets the default of itsFUSIONoption; whether a particular pair may actually fuse is decided per pair, at run time.
The remaining options in a [stage-<name>] section depend on its PROGRAM.
The subsections below document the options each stage program reads from its own
section; a section reads only the keys listed for its PROGRAM plus the
general keys above.
Some programs are the exception and are documented only in their own man
pages, because their option sets are large and change with the delivery
mechanism they drive: pepsi-stage-relay-to-maildir(1) (whose
SERVER_NAME is required — the stage refuses to start without it — plus
HELPER, UNKNOWN_MAILBOX_STAGE, QUOTA_LIMIT_STAGE, the RETRY_*
family, MAX_LIFETIME and the locality options LOCAL_DOMAINS,
TARGETS and RECIPIENT_DELIMITER), pepsi-stage-relay-to-lmtp(1)
(SOCKET or HOST/PORT, TLS, TLS_VERIFY, TLS_CA,
TLS_CLIENT_CERT/TLS_CLIENT_KEY, AUTH, USERNAME, PASSWORD,
SERVER_NAME, QUOTA_LIMIT_STAGE, SIEVE_REJECT_STAGE, NOTIFY_STAGE
and the timeouts), pepsi-stage-dot-forward(1) (HELPER,
RESTART_STAGE, ALLOW_PIPE, ALLOW_FILE and the locality options) and
the mailing-list stages pepsi-stage-list(1) (POST_STAGE,
COMMAND_STAGE), pepsi-stage-list-post(1) (DELIVERY_STAGE,
RESPONSE_STAGE, REMOVE_DKIM_HEADERS, SITE_HEADER_MATCH_CHAIN,
SENDER_POSTS_PER_HOUR, MAX_HELD_MESSAGES),
pepsi-stage-list-deliver(1), pepsi-stage-list-command(1)
(RESPONSE_STAGE, MAX_COMMAND_LINES) and pepsi-stage-list-bounce(1)
(RESPONSE_STAGE), whose site-wide options are in [pepsi-list] below.
Everything on this page about
[stage-<name>] in general still applies to them. The shared sections these programs also draw on are
documented elsewhere on this page: [pepsi] (signing identity), [pepsi-srs]
(the SRS engine), [pepsi-payments] (the merchant backend) and the smarthost
[pepsi-stage-relay-to-smarthost-mta-*] sections.
85.2.1.1.14.1. pepsi-stage-arc Options¶
A stage with PROGRAM = pepsi-stage-arc reads, besides a required
NEXT_STAGE, its DNS settings and the signature parameters of the ARC set it
adds (these apply to the ARC-Message-Signature; the ARC-Seal is always
relaxed-canonicalized over a fixed header set, per RFC 8617):
- DNS_SERVERS
(string, optional) Whitespace/comma-separated explicit resolvers (port 53) for the SPF/DKIM/DMARC/ARC lookups. When unset, the system
resolv.confis used.- DNS_TIMEOUT
(duration, optional) Per-query DNS timeout. Default
5 s.- HEADER_CANONICALIZATION
(``relaxed`` | ``simple``, optional) Header canonicalization of the AMS (the left half of
c=). Defaultrelaxed.- BODY_CANONICALIZATION
(``relaxed`` | ``simple``, optional) Body canonicalization of the AMS (the right half of
c=). Defaultrelaxed. ARC supports any combination of the two.- SIGNATURE_EXPIRATION_DAYS
(integer, optional) When set to a positive number of days, the AMS carries an expiration (
x=) that many days after signing. Unset (the default) emits no expiration. The ARC-Seal has nox=tag in RFC 8617 and never carries one, whatever this says.- SIGNED_HEADERS
(string, optional) Whitespace/comma-separated list of headers the AMS covers (
h=). Must includeFrom. Defaults to the built-in originator/MIME header set.
The ARC signing identity, key directory, selector and algorithm come from the
shared [pepsi] section (ARC_DOMAIN, ARC_ALGORITHM, KEY_DIR,
DKIM_SELECTOR). The hash is always SHA-256; the signature algorithm (a=)
is the single [pepsi] ARC_ALGORITHM choice, since ARC permits one signature
per hop.
85.2.1.1.14.2. pepsi-stage-srs Options¶
A stage with PROGRAM = pepsi-stage-srs needs only a NEXT_STAGE in its own
[stage-<name>] section; all its parameters (SRS_DOMAIN,
SECRET/SECRET_FILE, MAX_AGE_DAYS) live in the shared [pepsi-srs]
section documented above, so they are identical to ingress’s reverse-decode.
85.2.1.1.14.3. pepsi-stage-encrypt Options¶
A stage with PROGRAM = pepsi-stage-encrypt signs locally submitted mail as
its From: author and encrypts it to each recipient. It reads, besides a
required NEXT_STAGE and an optional BOUNCE_STAGE:
Warning
Place this stage before the DKIM-signing stage, not after:
… → encrypt → srs → dkim-sign → relay. DKIM must sign the bytes that are
actually transmitted, and this stage rewrites the body. Signing first yields
mail that looks valid and whose signature does not verify — worse than no
signature, because a broken one is a stronger negative signal to a receiver
than an absent one. pepsi-setup(1) warns when it finds a DKIM-signing
stage that leads to this one.
The pay-to-send gate and language detection both read the message body and both want cleartext; on the outbound path they run before this stage, which is why encryption is last.
Every option below is behavioural and is per-address overridable through the
pepsi.settings table (pepsi-settings(1)), keyed for outbound mail on
the envelope sender. The cryptographic policy — algorithms, downgrade
permission, minimum key sizes — is in [pepsi]’s CRYPTO_* options and
[pepsi-crypto], is system-administrator-only, and is deliberately not
reachable from here.
- ENABLE_PEP
(boolean, optional) Behave like the pretty Easy privacy (pEp) project: every local sender at a served domain gets a key on their first message, mail is encrypted whenever a recipient key is known or discoverable, and the sender’s key goes out with every message. Default
YES.A preset of defaults, not a forcing mode: it changes the default of
SIGNtoencrypted-onlyand turns on eager key creation. Any option written out explicitly still wins. WithENABLE_PEP = no,SIGNdefaults toopportunisticand keys are created only on an explicit request (signing or encrypting) or underSIGN = always. Eager creation additionally needs[pepsi-crypto] AUTO_CREATE_IDENTITY(the operator’s veto, which no per-address override reaches) and aKEY_WRAP_SECRET, and happens only for an address at a[pepsi-ingress] ACCEPTED_DOMAINSdomain that has nocrypto_identityrow of any kind. A user may opt out withENABLE_PEP = noin their ownpepsi.settingsoverride whereEDITABLE_STAGESallows. No pEp wire format is produced: noX-pEp-Versionheader, no pEp 2.x wrapping, no trustwords and no key sync. See pepsi-stage-encrypt(1).- SIGN
(``no`` | ``encrypted-only`` | ``opportunistic`` | ``always``, optional) Whether to sign with the
From:author’s key. Defaultencrypted-onlyunderENABLE_PEP, elseopportunistic.encrypted-onlysigns only a message this stage encrypts, inside the ciphertext; cleartext mail carries the sender’s key but no signature, so a recipient who does not use cryptography never sees asignature.asc. An explicit request (X-Pepsi-Sign: yes, a[sign]subject keyword) still signs cleartext.opportunisticsigns when the author has usable material, otherwise sends unsigned.alwayssigns the same way and additionally has an identity created for an author who has none, under the same conditions as the eager creation ofENABLE_PEP(AUTO_CREATE_IDENTITY, aKEY_WRAP_SECRET, a served domain, no identity of any kind), unlessX-Pepsi-Sign: norefused the signature. None is a guarantee — an author this deployment does not serve is sent unsigned rather than bounced. A message the submitting client already signed gets no second signature from Pepsi, and one the client already encrypted is left alone entirely.- ENCRYPT
(``no`` | ``opportunistic`` | ``required``, optional) Default
opportunistic: encrypt to a recipient we have a usable key for, send ordinary mail to one we do not.requiredmeans a recipient with no usable key gets the secure link or a bounce, never cleartext.- PREFER
(``openpgp`` | ``smime``, optional) Which protocol to reach for when a correspondent has usable material for both. Default
openpgp;pgpis accepted as a synonym for it.- ON_NO_KEY
(``cleartext`` | ``secure-link`` | ``bounce``, optional) What happens to a recipient with no usable key. Defaults from ENCRYPT:
cleartextforno/opportunistic, and forrequiredeithersecure-link(whenSECURE_LINK_STAGEis set) orbounce. Setting it explicitly overrides that — except that a message whose own request raised the policy torequirednever falls back tocleartext.Caution
ON_NO_KEY = cleartexttogether with[pepsi] CRYPTO_ALLOW_DOWNGRADE = nosends a correspondent whose key cannot read AES-GCM plaintext — strictly worse than the SEIPDv1+MDC the option was set to avoid, and most of the OpenPGP installed base (GnuPG 2.4 and older) is in that position. pepsi-setup(1) warns about the pair. Leave the option alone: bothnegotiate(compiled in) andyes(whatconfig.dships) downgrade rather than refuse, and undernegotiateonly when the recipient’s own key proves they must.- ON_OVERSIZE
(``cleartext`` | ``secure-link`` | ``bounce``, optional) What happens to a message larger than
MAX_SIZE. Defaults exactly likeON_NO_KEY, sorequireddoes not become cleartext because an attachment was large.- MAX_SIZE
(number, optional) Largest message, in bytes, this stage will encrypt. Default
25000000(25 MB). Neither the CMS builder nor the OpenPGP paths stream, so the whole message is resident while it is encrypted, once per recipient; a visible cap is what keeps the per-worker memory ceiling predictable instead of lettingPARALLELISMworkers each hold a multi-hundred-megabyte message.- MIN_TRUST
(``own`` | ``manual`` | ``dane`` | ``wkd-advanced`` | ``wkd-direct`` | ``ldap`` | ``vks`` | ``owner-confirmed`` | ``harvested`` | ``gossip`` | ``any``, optional) The lowest-ranked key source this stage will encrypt to. Defaults to
[pepsi-keydiscovery] MIN_TRUST, so a deployment has one floor unless it deliberately wants two. See The [pepsi-keydiscovery] Section for the ranking, for why the default floor accepts anything, and for the difference betweenharvestedandgossip.- SUBJECT_KEYWORDS_SIGN, SUBJECT_KEYWORDS_ENCRYPT, SUBJECT_KEYWORDS_BOTH
(comma-separated list, optional) Keywords a sender may put anywhere in the
Subjectto ask for protection. Defaults[sign],[encrypt]and[secure]. Matched case-insensitively; the matched keyword is removed from the outgoingSubject. An encryption keyword raises the policy torequiredfor that message — reading a deliberate human request as “try, and quietly give up” would make the feature a lie. Commas, not spaces, separate the list, because a keyword may legitimately contain a space.- STRIP_REQUEST_HEADERS
(boolean, optional) Remove every
X-Pepsi-*field from the outgoing message. DefaultYES. The two headers this stage acts on (X-Pepsi-Sign,X-Pepsi-Encrypt) are removed unconditionally, whatever this says: a control channel that survives onto the wire is one the next hop can be told to obey. This option decides only the fate of the otherX-Pepsi-*fields an add-in may have added.Pepsi-Originis not in that namespace and is never touched.- PROTECT_HEADERS
(boolean, optional) Copy the RFC 5322 header fields into the protected part and replace the outer
Subjectwith...— what Thunderbird implements for PGP/MIME. DefaultNO: client support is uneven, and the failure mode is a message whose subject reads...in a client that cannot look inside.- AUTOCRYPT
(boolean, optional) Advertise the
From:author’s own OpenPGP key in anAutocrypt:header. DefaultYES, and on every locally submitted message — signed, encrypted or plain cleartext — because a correspondent has to learn the key from ordinary mail before there is anything to encrypt with.The key is reduced to the minimal five-packet form of Autocrypt Level 1 §2.1.1. Only OpenPGP is advertised (Autocrypt has no S/MIME form), only for an identity that still holds its private material, and never on a message that already carries an
Autocrypt:field — a real client’s own header names the key it can decrypt with, and a second field makes Level 1 parsers discard both.prefer-encrypt=mutualis claimed only when the effectiveENCRYPTisrequired; see pepsi-stage-encrypt(1).Nothing is advertised when the address’s public face is the user’s own MUA key (
custody = client), or on a message the client already encrypted: the mail client advertises its own key.Turn it off to keep key publication to the Web Key Directory and DNS, which is a defensible choice for a deployment that does not want its users’ keys travelling on every message they send.
- ATTACH_KEYS_AS_FILES
(boolean, optional) Also attach the sender’s public key as an
application/pgp-keysfile namedOpenPGP_0x<long key ID>.asc, the classic OpenPGP way, in addition to theAutocrypt:header. DefaultNO, independent ofENABLE_PEP; an operator who wants the file only setsAUTOCRYPT = noas well.On an encrypted message the file goes inside the ciphertext; on cleartext the message is wrapped in a
multipart/mixed(or the key is added to an existing one). It is attached until every recipient provably holds the key – they sent back a message encrypted to it and signed by them, recorded inpepsi.peer_has_own_key– and starts again for a new key. Never when the public face is the user’s own MUA key, never on mail the client encrypted, and not on cleartext the client signed.- KEYS_CONTROL_LOCAL_PART
(string, optional) Local part of the address at which a user operates their own keys by e-mail: a locally submitted message whose only recipient is
<KEYS_CONTROL_LOCAL_PART>@<served domain>is a command (status,generate,register,publish [FPR],retire FPRin theSubject:), consumed and answered atRESPONSE_STAGE. Defaultpepsi-keys;noneswitches the surface off. NeedsRESPONSE_STAGE.- RESPONSE_STAGE
(stage name, optional) Where the replies to the e-mail key commands and the “a new key was registered for your address” notices are injected, as new null-sender messages. Normally the outbound DKIM-signing stage, which is what pepsi-setup –wizard writes (
dkim-sign). Unset means no notices and no e-mail commands (control mail is then sent on like any other message); pepsi-setup(1) warns about that, refuses a name that is not a stage, and requires thekey-registered.en.bodyandkeys.en.bodyfallback templates when it is set.- AUTOCRYPT_GOSSIP
(boolean, optional) Advertise the other recipients’ OpenPGP keys in
Autocrypt-Gossip:fields (Autocrypt Level 1 §5.3), so that two correspondents who have never written to each other can nevertheless answer each other encrypted after one message. DefaultNO.Off by default because it is not the same kind of act as
AUTOCRYPT: that one publishes a key its owner asked this deployment to hold, while this one redistributes third parties’ keys — material merely cached here — to correspondents who did not ask for them.It applies only to a message this stage actually encrypts, and the fields go inside the ciphertext, never on the outer header block: their whole purpose is to introduce the recipients to each other without disclosing the recipient set to the network. There is therefore no gossip on the cleartext pass-through, on a signed-but-unencrypted message, or on an S/MIME container (Autocrypt is defined over OpenPGP and has no S/MIME form). One field is emitted per introduced recipient — unlike
Autocrypt:, where a second field makes a Level 1 parser discard the message — andprefer-encryptnever appears, since it states the sender’s own policy and this field speaks for somebody else.Warning
Blind carbon copies are never gossiped. The addresses advertised are read from the message’s parsed
To:andCc:fields and from nowhere else; the envelope recipient list is consulted only to remove an address from the set. ABccrecipient is on the envelope and in neither field, so no copy of the message can carry a header naming them — which is the property to rely on, because a gossip header for a blind recipient would tell every other recipient that a hidden one exists and who it is. This holds even if aBcc:field was left on the message by a broken submitting client, because the field is not read.The converse is deliberate: a blind recipient’s own copy does carry gossip about the
To:/Cc:recipients. They received those header fields like everyone else, so it discloses nothing they cannot already read, and it is what lets them reply encrypted.A recipient this deployment holds no cached key for is simply left out — the stage performs no key discovery on their behalf and never delays a message to fetch a third party’s key. A message with more than 50 visible recipients gets no gossip at all: at that size it is a broadcast rather than a conversation, each introduced key is repeated in every recipient’s own ciphertext, and introducing an arbitrary subset would be worse than introducing none.
- GOSSIP_MIN_TRUST
(``own`` | ``manual`` | ``dane`` | ``wkd-advanced`` | ``wkd-direct`` | ``ldap`` | ``vks`` | ``owner-confirmed`` | ``harvested`` | ``gossip`` | ``any``, optional) The lowest-ranked key source whose key may be republished in an
Autocrypt-Gossip:field. Defaultowner-confirmed— the weakest rung at which somebody entitled to the address took a deliberate step to publish the key.It is a real tightening over
MIN_TRUST, and deliberately so: encrypting to a trust-on-first-use key is a good trade, because the alternative is cleartext, but handing that key to third parties is a different act. A gossiped key is stored by everyone who receives it at a rung they judge by our say-so and not by how we came to it, so passing on a guess launders it into something that looks like knowledge.The floor actually applied is the stricter of this and
MIN_TRUST: introducing a correspondent to a key this stage would not itself encrypt to would be recommending something it declined. So raising either option tightens gossip and lowering either cannot loosen it past the other, and an operator who hardensMIN_TRUSTneed not remember that a second option exists.GOSSIP_MIN_TRUST = anygossips whatever may be encrypted to. Has no effect unlessAUTOCRYPT_GOSSIPis on.- SECURE_LINK_STAGE
(stage name, optional) Where a recipient taking the
secure-linkroute is sent. Required ifON_NO_KEY/ON_OVERSIZEresolves tosecure-link.- DISCOVERY
(boolean, optional) Whether a cache miss may trigger a network lookup; documented under The per-sender switch: [stage-encrypt] DISCOVERY.
- DISCOVERY_TIMEOUT
(duration, optional) Override
[pepsi-keydiscovery] TIMEOUTfor the parks this stage creates. Note thatSection::duration()rejects calendar units:90 sand2 hparse,1 ddoes not.
An author identity this stage creates or registers is attributed to a passwd
login through [pepsi-crypto]’s LOCAL_DOMAINS, TARGETS and
RECIPIENT_DELIMITER, as pepsi-keys(1) attributes one, so the
ownership rule of The [pepsi-crypto] Section does not depend on which
program made the key. An address that resolves to no local account is a
legitimate state (a role address or a hosted domain), not a failure. Whether an
identity may be created at all is [pepsi-crypto] AUTO_CREATE_IDENTITY, and
it is offered only for an author in a [pepsi-ingress] ACCEPTED_DOMAINS
domain.
The binary is installed setuid pepsi-crypto, mode 4750
pepsi-crypto:pepsi, because it must be the one database role granted
crypto_identity.private_wrapped — peer authentication keys off the effective
uid — and must read the 0640 key-encryption-key fragment owned by the same
account. See pepsi-stage-encrypt(1).
85.2.1.1.14.4. pepsi-stage-decrypt Options¶
A stage with PROGRAM = pepsi-stage-decrypt decrypts mail addressed to
recipients this host serves and verifies whatever signature it carries. It reads,
besides a required NEXT_STAGE and an optional BOUNCE_STAGE:
- ENABLED
(boolean, optional) Whether to do any cryptography at all. Default
YES.NOstill removes theX-Pepsi-*header namespace from inbound mail: a disabled stage must not become a channel for a forged indicator.- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(optional) Which recipients this host serves — the same options, with the same defaults, as pepsi-stage-relay-to-maildir(1) and pepsi-stage-dot-forward(1).
LOCAL_DOMAINSdefaults to[pepsi-ingress] ACCEPTED_DOMAINS.- REQUIRE_ACCOUNT
(boolean, optional) Whether a local recipient must additionally resolve to a passwd account permitted by
TARGETS. DefaultNO, which is the difference from the delivery stages: they must know whose mailbox to write, while this stage only has to know whether the message is ours. A hosted domain whose users hold crypto identities but no shell accounts is an ordinary deployment.- ON_DECRYPT_FAILURE
(``passthrough`` | ``quarantine`` | ``bounce``, optional) What to do with a message this host could not open. Default
passthrough: the user may hold the key in their own client, and bouncing mail we merely could not open helps nobody.quarantineneedsQUARANTINE_STAGE;bounceneedsBOUNCE_STAGE.- ON_BAD_SIGNATURE
(``record`` | ``quarantine`` | ``bounce``, optional) What to do with a message whose signature did not verify. Default
record: mailing lists that rewrite bodies, forwarders and clients with broken canonicalisation all produce bad signatures on entirely legitimate mail. Only theinvalidverdict counts as bad here —valid-untrustedandunverifiableare the normal state of mail from an unknown correspondent and are never routed.- QUARANTINE_STAGE
(stage name, optional) Where a
quarantineroute leads. Required if either policy above is set toquarantine.- SUBJECT_TAGS
(boolean, optional) Prepend the earned tags to the
Subjectin nesting order —encrypted(signed(body))gives[decrypted][verified] …andsigned(encrypted(body))would give[verified][decrypted] …. DefaultYES: for most users this is the only signal they will ever see. The cost is a mutated user-visible field, which affects search, threading and quoted subjects in replies. (Both shapes open. The second earns[verified]only from a signature inside the ciphertext: an outer signature covers ciphertext and never reaches thevalidverdict the tag is written for — see pepsi-stage-decrypt(1).)- TAG_DECRYPTED, TAG_VERIFIED
(string, optional) The tag wording. Defaults
[decrypted]and[verified].TAG_VERIFIEDis written for thevalidverdict only.- TAG_BAD_SIGNATURE
(string, optional) The tag for the
invalidverdict. Empty by default: a bad signature is a routine property of legitimate mail, so a default-on tag would decorate a large part of a user’s inbox with an accusation.- STRIP_SUBJECT_TAGS
(boolean, optional) Remove this stage’s own tag vocabulary from the front of an inbound
Subjectbefore prepending ours. DefaultYES, and effectively mandatory: without it an external sender simply writes[verified]themselves. The vocabulary is derived from the threeTAG_*options plus the built-in[decrypted]and[verified], so renaming a tag cannot reopen the hole.- STRIP_SUBJECT_KEYWORDS
(comma-separated list, optional) Extra inbound subject tags to remove. Commas rather than whitespace, because a keyword may contain a space.
- ADD_RESULT_HEADER
(boolean, optional) Add the
X-Pepsi-Cryptoresult header to a message that carried protection. DefaultYES. Its grammar is documented in pepsi-stage-decrypt(1). EveryX-Pepsi-*field is removed from inbound mail first, unconditionally and whatever this option says — that removal is what makes the header believable.- MAX_LAYERS
(integer, optional) How many nested encrypted layers are unwrapped. Default
4. A message with more is delivered with the remainder unopened.- TRUST_ANCHOR_TTL
(duration, optional) How long a worker process reuses the
ca_trustanchors it last read. Default5 m. An anchor added or disabled becomes effective within this window; restart the workers to apply it at once. Calendar units are rejected (5 mand2 hparse,1 ddoes not).- DISCOVERY
(boolean, optional) Whether a cache miss may trigger a network lookup; documented under The per-sender switch: [stage-encrypt] DISCOVERY. Read from this section by the same reader, so the two crypto stages cannot disagree about its name or its default.
- DISCOVERY_TIMEOUT
(duration, optional) Override
[pepsi-keydiscovery] INBOUND_TIMEOUTfor the parks this stage creates.
[pepsi] CRYPTO_ALLOW_DOWNGRADE does not gate this direction. OpenPGP
SEIPDv1+MDC and CMS AES-CBC EnvelopedData are always readable, whatever the
emission policy says: refusing them inbound would make this host unable to
receive mail from most of the world while protecting nobody. Only 3DES and the
weak signature digests are gated inbound, and each refusal produces its own
verdict.
The binary is installed setuid pepsi-crypto, mode 4750
pepsi-crypto:pepsi, for exactly the reasons given for
pepsi-stage-encrypt(1) above. See pepsi-stage-decrypt(1).
85.2.1.1.14.5. pepsi-stage-reencrypt Options¶
A stage with PROGRAM = pepsi-stage-reencrypt seals a message the decrypt
stage opened to the recipient’s own mail-client (MUA) key before local delivery.
It acts only on rows whose state.crypto.in says the message arrived
encrypted and was decrypted. NEXT_STAGE is mandatory.
- ON_NO_CLIENT_KEY
(string, optional) What happens to a local recipient with no usable MUA key:
plaintext(default) files the plaintext;bouncerefuses the message through BOUNCE_STAGE with a5.7.5DSN that carries only the addressing header fields and no body (RET=HDRSis forced).pepsi-setuprefusesbouncewithout a BOUNCE_STAGE. Per-recipient overrides throughpepsi.settingsapply.- PROTECT_HEADERS
(boolean, optional) Copy the RFC 5322 fields inside the ciphertext and replace the outer
Subjectwith.... DefaultNO.- ENABLED
(boolean, optional) Off passes every message through. Default
YES.- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER, REQUIRE_ACCOUNT
Which recipients are served here, as for pepsi-stage-decrypt Options. Other recipients are neither sealed nor refused.
The container follows [pepsi] CRYPTO_ALLOW_DOWNGRADE as for outbound
encryption. The binary is unprivileged and folded into the multi-call
pepsi binary: it seals to public keys and reads only the public columns of
pepsi.crypto_identity. See pepsi-stage-reencrypt(1).
85.2.1.1.14.6. The [pepsi-secure-link] Section¶
The secure-link fallback portal: where a message that could not be encrypted is
held, and how the recipient is let in. Read by three programs — the stage that
stores a message (pepsi-stage-secure-link), the server that serves it
(pepsi-httpd(1)) and the operator CLI (pepsi-secure-link(1)) — which
is why it is one global section rather than options copied into a [stage-*]
one.
That placement is also a decision about who may change these values. This is
not a [stage-<name>] section, so it is out of reach of pepsi.settings and
pepsi-stage-edit-settings(1): a correspondent cannot mail themselves a
longer expiry, a shorter PIN or a disabled lockout. The genuinely behavioural
knob — whether an unencryptable message goes to the portal at all — is
[stage-encrypt] ON_NO_KEY, which is per-address overridable.
- BASE_URL
(string) Public origin the portal is reachable at, e.g.
https://secure.example.org. Setting it (together withPEPPER) is what enables the feature; the link mailed to a recipient is<BASE_URL>/secure/<token>. Nothing here says where the portal is mounted — its own listener and hostname, or one shared with the API and the Web Key Directory, are both supported.- PEPPER
(string) The server-side secret mixed into every content-key derivation. Written by pepsi-setup(1) to
secrets.d/pepsi-secure-link.secretand referenced with@inline-secret@; it is the one managed fragment with two readers, so it is installed mode0640ownedpepsi-httpd:pepsi. Without it the portal is off — there is no mode in which a message is stored in the clear. Losing or changing it makes every stored message unreadable, which is the same property that makes a stolen database worthless.- EXPIRY_DAYS
(integer, optional) How long a stored message lives. Default
7. An integer count of days rather than a duration, becausedurationvalues reject calendar units.- PIN_LENGTH
(integer, optional) Characters in a generated PIN, from a 31-symbol unambiguous alphabet (no
0/O, no1/I/L). Default10, minimum8. Six digits is refused: the PIN is the only secret an attacker holding both the database and the pepper still has to find.- MAX_ATTEMPTS, LOCKOUT
(integer / duration, optional) Wrong PINs before the token locks (default
10) and the first lockout window (default15 m). The window doubles with each further lockout, capped at 64 times the base.- SESSION_LIFETIME
(duration, optional) How long the portal session lasts after a correct PIN. Default
30 m.- RATE_LIMIT
(integer, optional) Requests per minute per peer address across every portal route. Default
30;0disables the limiter.- NOTIFY_STAGE
(string) Required. Where the link mail, the PIN mail and read receipts are injected — a stage on the outbound path, since these are messages the deployment originates.
- REPLY_STAGE
(string, optional) Where a reply composed in the portal is injected. Unset disables replying. Point it at the inbound path: a portal reply is written by an external party, so it is injected without
state.local_originand must not be DKIM-signed as one of your domains (pepsi-setup(1) warns if it resolves to a signing stage).- SEND_RECEIPT
(boolean, optional) Tell the sender, once, that the message was read. Default
yes. The receipt reports who and when, never what — there is nothing for it to quote.- PIN_DELIVERY
(``sender`` | ``command`` | ``none``, optional) How the second factor reaches the recipient. Default
sender: the PIN is mailed to the sender, who relays it by telephone or text message.commandrunsPIN_COMMAND.noneputs the PIN in the link itself and is strictly weaker — the mailbox becomes the only factor.- PIN_COMMAND
(string, optional) The program to run; a filesystem path taken verbatim, so unlike a path-typed option it is not
$-expanded. Required byPIN_DELIVERY = command. Run with the recipient address as its single argument and the PIN on standard input (a command line is world-readable through/proc). A non-zero exit fails the message rather than leaving a link nobody can open, and so does taking longer than 30 seconds, after which the command is killed: an SMS gateway client that blocks on an unreachable address would otherwise hold a stage worker for ever, since the row staysrunningand nothing re-claims it.- MAX_REPLY_SIZE, MAX_REPLY_FILES, MAX_REPLIES
(integer, optional) Caps on a portal reply: total octets (default
25000000, matching[stage-encrypt] MAX_SIZE), files per reply (default10) and replies per stored message (default20). The size cap is enforced while the body streams, so a client that misstatesContent-Lengthgains nothing.- ACCESS_LOG_ROWS
(integer, optional) Access-log rows kept per message. Default
100. The log is deleted with the message.- NOTIFY_FROM
(string, optional) The
From:header of the three notifications the portal originates (the link, the PIN and the read receipt). Unset — the default — each isFrom:the original sender, so it reads as coming from the person who actually wrote; that is right for the link and PIN mails, and questionable for the read receipt, which is an automatic null-sender message then purporting to come from the sender’s own address. Set this to put one neutral, branded mailbox on all three. It is written into the header verbatim, so give a complete RFC 5322 value (Example Ltd <secure@example.com>). The envelope sender is unchanged either way.- ORGANIZATION, BRAND_COLOR, LOGO_FILE
(string / string / path, optional) Appearance.
BRAND_COLORmust be#rgbor#rrggbb— it is interpolated into the page’s stylesheet, so arbitrary text is refused rather than escaped.LOGO_FILEis a local PNG, JPEG, GIF or WebP inlined as adata:URI (SVG is refused: it can carry script). A logo URL is deliberately not an option — the portal’sContent-Security-Policyforbids remote content, which is also what stops a CDN learning who read what.
85.2.1.1.14.7. pepsi-stage-secure-link Options¶
A stage with PROGRAM = pepsi-stage-secure-link takes no options of its
own: everything is in [pepsi-secure-link] above. It has no NEXT_STAGE
(it is terminal — the message leaves the queue and its notification is a new
message injected at NOTIFY_STAGE).
85.2.1.1.14.8. pepsi-stage-dkim-sign Options¶
A stage with PROGRAM = pepsi-stage-dkim-sign reads, besides a required
NEXT_STAGE:
- HEADER_CANONICALIZATION, BODY_CANONICALIZATION
(``relaxed`` | ``simple``, optional) The two halves of the
c=canonicalization. Both default torelaxedand may be set independently; all four combinations are valid for both DKIM signing and ARC sealing.- SIGNATURE_EXPIRATION_DAYS
(integer, optional) When set to a positive number of days, the signatures carry an expiration (
x=) that many days after signing. Unset (the default) emits no expiration.- SIGNED_HEADERS
(string, optional) Whitespace/comma-separated list of headers the signatures cover (
h=). Must includeFrom. Defaults to the built-in originator/MIME header set.- SIGNING_DOMAIN
(string, optional) Force the signing domain (
d=). When unset, the domain is taken from each message’sFrom:header. A fixedSIGNING_DOMAINis a sending identity, so pepsi-setup(1) provisions its keys and DNS.
The DKIM key material and selectors come from the shared [pepsi] section
(KEY_DIR, DKIM_SELECTOR). The hash is always SHA-256; both an RSA
(rsa-sha256) and an Ed25519 (ed25519-sha256) signature are emitted.
85.2.1.1.14.9. pepsi-stage-relay-to-internet Options¶
A stage with PROGRAM = pepsi-stage-relay-to-internet reads (a NEXT_STAGE
and BOUNCE_STAGE are the general keys above):
- SERVER_NAME
(string, required) Our own host name, announced in
EHLOand written into theReceived:trace header, e.g.mail.example.org.- POSTMASTER
(string, optional) Address that receives a double bounce — a bounce (null sender) that is itself undeliverable. With none, such double bounces are discarded.
- PUBLIC_IP
(string, optional) The public addresses this relay sends from, read by pepsi-setup(1) to build the SPF record (and, for this stage, to check reverse DNS) — a relay-stage option documented under The Egress Identity Option: PUBLIC_IP below.
- CONNECT_TIMEOUT / COMMAND_TIMEOUT / DATA_TIMEOUT
(duration, optional) Per-connection timeouts for, respectively, establishing the TCP connection, an SMTP command/response, and the
DATAtransfer. Defaults300 s/300 s/600 s.- RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR
(duration / duration / number, optional) Exponential-backoff schedule between delivery attempts of a transiently-failing message: the first wait, the cap on the wait, and the multiplier applied each attempt. Defaults
300 s/2 h/2.- MAX_LIFETIME
(duration, optional) How long a message may stay in the pipeline before a transient failure becomes permanent (it is then bounced or failed). Default
5 dof wall-clock, but the value must be written withh/m/sunits — the duration parser rejects thedcalendar unit (so write120 h, not5 d).- DELAY_DSN_AFTER
(duration, optional) When set, a one-shot
Action: delayedDSN is sent to the sender once a still-undelivered message has been queued at least this long and the sender requestedNOTIFY=DELAY. Must useh/m/sunits. Unset (the default) disables delay warnings.- MAX_HOP_COUNT
(number, optional) Maximum number of
Received:headers tolerated before the message is treated as a mail loop and permanently failed. Default30.- DNS_SERVERS
(string, optional) Whitespace/comma-separated explicit resolvers (port 53) for the
MXand address lookups. When unset, the systemresolv.confis used.- DNS_TIMEOUT
(duration, optional) Per-query DNS timeout. Default
5 s.- MTA_STS
(boolean, optional) Whether to discover and enforce recipient-domain MTA-STS policies (RFC 8461). Default
yes.- MTA_STS_TIMEOUT
(duration, optional) Timeout for fetching an MTA-STS policy file over HTTPS. Default
10 s.- ADDRESS_FAMILY
(optional) Which IP address families to use when connecting to mail exchangers:
any(default),ipv4(also spelledv4/v4only) oripv6(v6/v6only). The configured value is intersected with what the local host can actually route, so pinning a family the host has no route for leaves nothing to connect to. pepsi-stage-relay-to-smarthost(1) has the same option, additionally settable per MTA entry.- DANE
(optional) DANE/TLSA (RFC 7672) enforcement for the destination MX:
off(noTLSAlookups),warn(the default; validate but, on a mismatch, log and deliver anyway) orstrict(a usable-but-mismatching record, or a failed TLSA lookup, defers delivery). Requires a DNSSEC-validating resolver (Pepsi trusts the response AD bit; see DNS_SERVERS); usableTLSArecords take precedence over MTA-STS.
85.2.1.1.14.10. pepsi-stage-relay-to-smarthost Options¶
A stage with PROGRAM = pepsi-stage-relay-to-smarthost reads the operational
options below; the upstream smarthosts themselves are defined once in the shared
[pepsi-stage-relay-to-smarthost-mta-*] sections (see The Smarthost (MTA)
Sections below). A NEXT_STAGE and BOUNCE_STAGE are the general keys
above.
- SERVER_NAME
(string, required) Our own host name, announced in
EHLO(unless an MTA overrides it withHELO_NAME) and written into theReceived:header.- CONNECT_TIMEOUT / COMMAND_TIMEOUT / DATA_TIMEOUT
(duration, optional) Per-connection timeouts for the TCP connect, an SMTP command/response and the
DATAtransfer. Defaults300 s/300 s/600 s.- RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR
(duration / duration / number, optional) Exponential-backoff schedule between relay attempts. Defaults
300 s/2 h/2.- MAX_LIFETIME
(duration, optional) How long a message may stay in the pipeline before a transient failure becomes permanent. Default
5 dof wall-clock, but write it withh/m/sunits — the parser rejects thedcalendar unit.- DELAY_DSN_AFTER
(duration, optional) When set, a one-shot
Action: delayedDSN is sent once a still-undelivered message has been queued at least this long and the sender requestedNOTIFY=DELAY. Must useh/m/sunits. Unset disables it.- MAX_HOP_COUNT
(number, optional) Maximum number of
Received:headers before the message is treated as a loop and permanently failed. Default30.- ADDRESS_FAMILY
(optional) The address family every MTA entry inherits unless its own
[pepsi-stage-relay-to-smarthost-mta-*]section overrides it:any(default),ipv4(alsov4/v4only) oripv6(v6/v6only). Same option, same spellings, as pepsi-stage-relay-to-internet(1)’s above.- PUBLIC_IP
(string, optional) The public addresses this relay sends from, read by pepsi-setup(1) to build the SPF record — a relay-stage option documented under The Egress Identity Option: PUBLIC_IP below.
- DNS_SERVERS
(string, optional) Whitespace/comma-separated explicit resolvers (port 53) for the DANE
TLSAlookups. When unset, the systemresolv.confis used.Only consulted when a smarthost enables
DANE; the resolver must validate DNSSEC.- DNS_TIMEOUT
(duration, optional) Per-query timeout for the DANE
TLSAlookups. Default5 s.
85.2.1.1.14.11. Bounce Stage Options¶
A [stage-<name>] section with PROGRAM = pepsi-stage-bounce additionally
reads, from its own section:
- SERVER_NAME
(string, required) Host name used in the generated bounce’s
From:andReporting-MTAfields and in itsMessage-IDdomain, e.g.mail.example.org.- POSTMASTER
(string, optional)
From:address of generated bounces (Mail Delivery Subsystem <POSTMASTER>) and the DKIM signing identity — the bounce is signed as this address’s domain, whose keys pepsi-setup(1) provisions. Defaults topostmaster@<SERVER_NAME>.- NEXT_STAGE
(string, required for this stage) The stage the rewritten bounce is advanced to. Because the bounce is left unsigned, this is normally a pepsi-stage-dkim-sign(1) stage (which signs it as the postmaster’s domain), which in turn points at a delivery stage such as pepsi-stage-relay-to-internet(1).
- BOUNCE_MESSAGE
(string, optional) Name of the template that renders the human-readable (
text/plain) part of the bounce. The template file isbounce-<NAME>.<lang>.bodyunder the[pepsi]TEMPLATE_DIR, and it is a Mustache template.<lang>follows the message’sstate.language(the language detected in the original message, whose sender the bounce goes to) in itsqorder, falling back tobounce-<NAME>.en.body, which must exist. When unset — or when rendering fails — a built-in English notice is used instead, so a bounce is always produced. Three template families are installed bymake install:bounce-default,bounce-language(the language policy of pepsi-stage-block-language(1)) andbounce-payment(the unpaid gate of pepsi-stage-anti-spam(1)).The whole message
stateis passed to the template as the rendering context, augmented with the convenience variablesserver_name,postmaster,bounce_to,failed_recipient,diagnostic,actionand the booleansfailed/delayed/delivered. In particular the delivery stage records structured next-hop detail understate.bounce—remote_mta,smtp_code,enhanced_status,phaseandreply_text— so the template can state precisely why the next MTA refused the message. When astate.bounce.enhanced_status(RFC 3463) is present it is also used for the DSN’s machine-readableStatus:field.- RET_FULL_MAX_SIZE
(integer, optional) Largest original message, in bytes, that a
RET=FULLbounce returns in full. Default262144(256 KiB);0removes the limit.RFC 3461 §6.2 says the full message “SHOULD be returned” when the sender set
RET=FULLonMAIL FROM, and explicitly permits returning the headers alone when the message “exceeds some implementation-specified size” — so the limit is part of conforming behaviour, not an escape from it. Without one, a failed 40 MB message would become a 40 MB bounce sent to a sender who is already having a bad day. Above the limit the DSN falls back totext/rfc822-headers, exactly as aRET=HDRS(or absent-RET) bounce does, and the fallback is logged.The body is fetched from the database only for a message whose sender asked for it and which is within the limit; an ordinary bounce still loads nothing but the header block.
- BOUNCE_UNAUTHENTICATED
(``drop`` or ``send``, optional) What to do with a report addressed to an envelope sender the inbound message did not authenticate. Default
drop.Spam forges its envelope sender, so a bounce for it goes to an innocent third party. That is backscatter: the recipients complain, and this host’s IP ends up on blocklists. Under
dropa report is sent only when:the message came from one of our own users (
state.local_origin);SPF passed for the envelope sender’s domain; or
an aligned DKIM signature passed for a
From:domain that is the envelope sender’s domain, or a parent or child of it.
Anything else is dropped and logged. A message a stage created, rather than one that arrived over SMTP, carries no authentication record and is always reported.
sendrestores the old behaviour of reporting to every sender.
The failed recipient and diagnostic quoted in the bounce are taken from the
state left on the row by the delivery stage that routed the message here
(see BOUNCE_STAGE above); when absent (e.g. a hand-staged message) a generic
notice is produced.
85.2.1.1.14.12. pepsi-stage-discard Options¶
A stage with PROGRAM = pepsi-stage-discard deletes the message (a staging /
test sink). It ignores NEXT_STAGE — a discard is always terminal — and reads:
- DISPOSITION
(optional) The simulated outcome:
success(default) treats the message as delivered;failuretreats it as a permanent delivery failure.- BOUNCE
(boolean, optional) Whether the discard may emit a DSN at all. Default
no.- BOUNCE_STAGE
(string, optional) The DSN-generation stage a reportable discard is routed to (normally a pepsi-stage-bounce(1) stage). Without one, nothing is emitted and the row is just deleted.
The success path also consults the shared [pepsi] ORIGINATE_SUCCESS_DSN flag;
a null-sender message is never bounced.
85.2.1.1.14.13. pepsi-stage-anti-spam Options¶
A stage with PROGRAM = pepsi-stage-anti-spam reads (besides a NEXT_STAGE):
- BOUNCE_STAGE
(string, optional) The stage an unpaid-by-deadline message is routed to — normally a pepsi-stage-bounce(1) stage (a rejection DSN, honouring the sender’s
NOTIFY) or a pepsi-stage-discard(1) stage (silent drop). Without one, an unpaid message is just deleted.- BOUNCE_TARGET_STAGE
(string, optional) Where a returning bounce that carries no valid ``Pepsi-Origin`` proof is routed — a message with the null sender claiming to be a report about mail of ours, which we cannot show we originated (see The [pepsi-origin] Section). Without one such a message takes the ordinary NEXT_STAGE path. Point it at a quarantine or review stage if you want to keep them for inspection: they are never payment-gated, since a bounce must not be bounced.
- ORDER_CHOICES
(JSON, required) The Taler v1 order
choicesarray, used verbatim: one or moreOrderChoiceobjects describing the accepted payments (for example several amounts in different currencies, or token-family inputs/outputs). See the GNU Taler merchant API.- PAYMENT_DEADLINE
(duration, optional) How long the message is held
pausedawaiting payment before it is rejected; also the order’spay_deadline. Usesh/m/sunits only (calendar units such asdare rejected). Default48 h.- DELAY_DSN_AFTER
(duration, optional) Send the sender a one-shot delayed delivery-status notification (RFC 3461) once a still-unpaid message has been held this long, without releasing the message — it stays paused until PAYMENT_DEADLINE. The DSN is sent only when the sender requested
NOTIFY=DELAYand a BOUNCE_STAGE is wired (it is routed there withkind = delay). Only fires when it lands strictly before PAYMENT_DEADLINE. Usesh/m/sunits only. Unset (the default) disables delay warnings.- SUMMARY
(string, optional) Human-readable order summary shown to the payer. Default
E-mail delivery.- FULFILLMENT_MESSAGE
(string, optional) Message shown to the payer after a successful payment. Default
Your e-mail has been queued for delivery.- BLOCK_RESPONSE_STAGE
(string, optional) The stage at which the payment-request auto-reply is injected — usually the head of a signing/relay chain (for example a pepsi-stage-dkim-sign(1) stage). Must reference an existing stage. When unset, no reply is sent (the message is still paused awaiting payment).
- BLOCK_RESPONSE_FROM
(string, optional) The
From:header of the auto-reply. When unset it defaults to the original recipient (the protected mailbox).- BLOCK_RESPONSE_SUBJECT
(string, optional) The
Subject:of the auto-reply. DefaultPayment required to deliver your e-mail.- BLOCK_RESPONSE_SUPPRESS
(duration, optional) The per-sender window for the auto-reply: a sender that was already sent a payment request for the same protected mailbox (the first envelope recipient) within this long gets none for the next message — which is still held for payment all the same. The request goes to an envelope sender nobody has verified, so without a window a flood of messages bearing one forged sender becomes the same flood of requests at that address. Claimed and recorded in one statement (
payment_request_should_sendoverpepsi.payment_request_reply, thevacation_should_replypattern), so two workers cannot both send one; rows are pruned as they age out.h/m/sunits only (capped at a year);0 sanswers every message. Default1 h.- PAYMENT_MESSAGE_DEFAULT_LANGUAGE
(string, optional) The fallback language for the auto-reply body, used when the message carries no detected
state.languageor none of the detected languages has apayment-request.<lang>.bodytemplate (under[pepsi]TEMPLATE_DIR). That template must exist (pepsi-setupchecks). Case-insensitive. Defaulten.
The stage also requires the shared [pepsi-payments] section (the merchant
backend and access token, documented below) and, for the payment webhook to
release paused messages, the [pepsi-httpd] RESUME_AUTHORIZATION_TOKEN
(see pepsi-httpd(1)).
85.2.1.1.14.14. pepsi-stage-auto-pay Options¶
A stage with PROGRAM = pepsi-stage-auto-pay reads:
- MAX_TOTAL
(amount, required) The most this stage will spend, in total, to deliver one original message — a GNU Taler amount
CURRENCY:VALUE(for exampleEUR:5). The cap is cumulative across every payment demand returning for that message (all sharing one Pepsi-Origin nonce, e.g. a mailing-list fan-out) and single-currency: a demand in any other currency is not paid. An account can override this for its own mail via pepsi-stage-edit-settings(1).- MAX_TOTAL_PER_DAY
(amount, optional) The most one wallet may spend on payment demands in any 24 hours (a rolling window), across however many original messages — the aggregate MAX_TOTAL does not provide, since a correspondent who received k of our messages can demand MAX_TOTAL for each. The wallet follows WALLET_MODE/WALLET_SCOPE: a local user’s own wallet, the shared account’s per-sender wallet, or its one unified wallet (then the limit is the deployment’s). Never per correspondent: mailing lists and aliases make the demanding party unknowable. Must be in MAX_TOTAL’s currency. Enforced atomically with MAX_TOTAL by
auto_pay_try_spendover thepepsi.auto_pay_spendledger; a payment that fails to settle is credited back. Unset (the default), there is no daily limit.- NEXT_STAGE
(string, required) Where a message is forwarded when it is not a payment demand, is not provably ours, or is not paid (over budget / settlement failure). A stage with no NEXT_STAGE errors at run time.
- WALLET_MODE
(``local-user`` | ``shared``, default ``shared``) Which account the wallet helper pays from.
local-userdrops to the bounce recipient’s own local login (the original sender) and uses that user’s default wallet — the recipient must resolve to a permitted local account (the LOCAL_DOMAINS/TARGETS/ RECIPIENT_DELIMITER test, as for pepsi-stage-relay-to-maildir(1)).shareddrops to a single dedicated account (WALLET_USER).- WALLET_USER
(string, default ``pepsi-wallets``) The shared account the helper drops to in
WALLET_MODE = shared; its home holds the wallet database(s).- WALLET_SCOPE
(``per-sender`` | ``unified``, default ``per-sender``) In
sharedmode, how the shared account’s wallet databases are segregated: one per original-sender address (per-sender), or a single wallet shared by everyone (unified— e.g. a business where individual wallets are not sensible).- WALLET_CLI
(string, default ``taler-wallet-cli``) The GNU Taler wallet command-line tool the helper runs (a name resolved on
$PATHor an absolute path).- WALLET_PAY_OPTIONS
(string, default none) Extra arguments appended to the wallet’s pay invocation, whitespace-separated. The literal
nonepasses no extra options. (--yesand--choice-index 0are always added by the helper to confirm the payment and to name the contract choice it priced.) Nothing is needed here by default — the wallet’shandle-urialready waits for the payment to reach a terminal state.- HELPER
(string, default ``pepsi-helper-auto-pay``) The privileged wallet helper binary (a name on
$PATHor an absolute path).
The following options enable and configure the wallet self-service role (a locally-submitted message to the control address operates the sender’s own wallet by e-mail; see pepsi-stage-auto-pay(1)):
- RESPONSE_STAGE
(string, optional) The stage at which the self-service reply is injected (so it is DKIM-signed and relayed). Setting it enables the self-service role; leaving it unset means the stage only pays inbound demands.
- CONTROL_LOCAL_PART
(string, default ``pepsi-wallet``) The local part of the wallet control address: a locally-originated message to
<part>@<served-domain>is a wallet-control message.- RESPONSE_FROM
(string, optional) The
From:of the self-service reply. Defaults to<{CONTROL_LOCAL_PART}@{domain}>for the served domain addressed.- RESPONSE_SUBJECT
(string, default ``Pepsi wallet``) The
Subject:of the self-service reply.
A manual withdraw (top-up) names the GNU Taler exchange to withdraw from in
the request (Subject: withdraw CURRENCY:VALUE EXCHANGE-URL); because a wallet
is multi-currency and can withdraw from many exchanges, there is no server-side
exchange option.
The stage also uses the shared [pepsi-origin] section (the proof-of-origin
secret) to verify that a demand is for mail this deployment actually sent; without
it no demand can be verified and none is paid (self-service is unaffected). All
wallet work is done by the setuid-root pepsi-helper-auto-pay(1); the stage
binary is installed SGID pepsi-wallets so its worker may exec it.
85.2.1.1.14.15. pepsi-stage-check-whitelist Options¶
A stage with PROGRAM = pepsi-stage-check-whitelist reads:
- WHITELIST_NAME
(list, required) The
whitelist_namegroups to consult in thepepsi.whitelisttable — a comma-separated list, queried as one= ANY(...), not a single name.An entry may be a template containing
{localpart}or{login}, which is expanded per envelope recipient before the query:{localpart}is the sub-address-stripped, lower-cased local part, and{login}the passwd login it resolves to. That is how a per-user namespace managed with pepsi-whitelist(1) — the<login>/segmentnames — is consulted without a per-address settings override. For example:WHITELIST_NAME = correspondents, {localpart}/correspondents
consults the shared group and each recipient’s own.
- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(optional) The same locality options, with the same defaults, as the delivery stages — read here only to resolve
{login}(and to strip a sub-address for{localpart}). Needed only when a template uses them.- NEXT_STAGE
(string, required) Where the message advances after the check (whether or not it matched). A stage with no NEXT_STAGE errors at run time.
85.2.1.1.14.16. pepsi-stage-auto-whitelist Options¶
A stage with PROGRAM = pepsi-stage-auto-whitelist reads:
- WHITELIST_NAME
(string, required) The
whitelist_namegroup to populate in thepepsi.whitelisttable. It should match the group the inbound pepsi-stage-check-whitelist(1) consults. One literal name in the whitelist name grammar: this stage expands no{login}/{localpart}placeholder, so a value containing one is refused. The operator may name any whitelist, one shared by all local users included; a value an account owner sets by mail through pepsi-stage-edit-settings(1) must name one in their own<login>/...namespace, or the one the operator’s configuration names for them.- DKIM_REQUIRED
(boolean, optional) Value stored in the
dkim_requiredcolumn of every row this stage inserts. Defaultyes.- NEXT_STAGE
(string, required) Where the message advances after the recipients are recorded. A stage with no NEXT_STAGE errors at run time.
85.2.1.1.14.17. pepsi-stage-autocrypt-learn Options¶
A stage with PROGRAM = pepsi-stage-autocrypt-learn reads:
- LEARN_KEYS
(boolean, optional) Store the sender’s own key material found in the message — S/MIME signer certificates,
application/pgp-keysparts and theAutocrypt:header — at theinboundtrust rank, where it can never silently replace a better-sourced key. DefaultYES. Only material claiming the sender’s own address is kept. Turning it off turns the whole stage into a pass-through,LEARN_GOSSIPincluded.- LEARN_GOSSIP
(boolean, optional) Also learn the other recipients’ keys from the
Autocrypt-Gossip:fields inside a message that arrived encrypted (Autocrypt Level 1 §5.3), at thegossiprank — the bottom of the ladder, belowinbound. DefaultYES; it does nothing unlessLEARN_KEYSis also on.This is what makes an encrypted reply-all possible to a correspondent one has never had direct mail from. A field is used only when it was genuinely inside the ciphertext, only when its
addrappears in the message’s ownTo:,Cc:orReply-To:, never for the sender’s own address, and never for an address inLOCAL_DOMAINS— nobody may introduce this host to its own users. A gossiped key can never displace a better-sourced one and never makes a signature count as verified.Note the asymmetry with the sending half (
[stage-encrypt] AUTOCRYPT_GOSSIP, defaultNO): emitting gossip redistributes third parties’ keys to correspondents who did not ask for them, while reading it only fills this host’s own cache. An operator who wants the introductions stored but never used sets[pepsi-keydiscovery] MIN_TRUST = harvestedinstead of turning this off.- LEARN_FROM_SPAM
(boolean, optional) Whether a message the pipeline has already scored as spam (
state.spam = true) may still teach a key. DefaultNO, which is Autocrypt Level 1 §5.3’s “messages SHOULD be ignored … when the MUA believes the message to be spam”.It is an option rather than a fixed rule because the belief is Pepsi’s own: an operator whose spam scoring is deliberately loose may prefer key continuity to the rule. Note that honouring the default requires the stage to be placed after whatever sets
state.spam— see pepsi-stage-autocrypt-learn(1).- ACCEPT_ROTATION
(string, optional) When the Autocrypt tier’s newest-wins rule may replace a correspondent’s stored
inbound/gossipkey with the key a newer message carries.newest(the default) is Autocrypt Level 1: a strictly newer effective date suffices.expiredadditionally requires every stored key for that address and protocol to be revoked or expired (by its recorded validity or itsexpires_at); otherwise the stored key is kept and the offer is held — audited askey.peer.rotate.heldand reported to the recipients like a rotation. A replacement by a strictly better-ranked source is not governed by this option. Any other value is a configuration error. Either way every rotation writes akey.peer.rotateaudit row; see pepsi-stage-autocrypt-learn(1).- RESPONSE_STAGE
(string, optional) The stage at which the key-change notices are injected: when a correspondent’s key is rotated or held, each local envelope recipient of the message that caused it is mailed the
key-rotation.<lang>.bodytemplate (null sender,From: postmaster@<domain>). Normally the DKIM-signing stage, like pepsi-stage-encrypt(1)’sRESPONSE_STAGE. Must name an existing stage; pepsi-setup(1) also requireskey-rotation.en.bodyin[pepsi] TEMPLATE_DIR. Absent, changes are audited and logged but nobody is mailed.- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(optional) The shared locality options. Only
LOCAL_DOMAINSis consulted: to refuse a gossip field naming an address this host serves (a key substitution rather than an introduction), and to pick which envelope recipients are ours to send a key-change notice to. Defaults to[pepsi-ingress]ACCEPTED_DOMAINS.- NEXT_STAGE
(string, required) Where the message advances once the stage has learnt whatever it could. The stage never consumes a message, so a missing NEXT_STAGE is refused by pepsi-setup(1) and errors at run time.
85.2.1.1.14.18. pepsi-stage-detect-language Options¶
A stage with PROGRAM = pepsi-stage-detect-language reads:
- NEXT_STAGE
(string, required) Where the message advances after classification (whether or not a language was recorded). A stage with no NEXT_STAGE errors at run time.
- LANGUAGES
(list, optional) The candidate languages, as whitespace/comma-separated ISO 639-1 codes (case-insensitive); at least two distinct codes are required. The token
*stands for every supported language and may be combined with explicit codes (which then come first). Detection only ever returns languages from this set, so it should cover the languages you expect to receive. Defaults to a broad common set (en de fr es it pt nl pl ru tr ar zh ja).Narrow it. Every enabled language adds to the memory each worker process uses, and — more importantly — every language that your traffic never contains still competes for probability. The stage removes non-prose (URLs, addresses, base64 blobs, DNS records) from the body before classifying, which is what stops a single such token from deciding the answer outright; what remains after that is a genuine contest between the candidates, and a language nobody writes to you can still win it on a short message. pepsi-detect-language(1) reclassifies a saved message against any candidate set, so a narrower list can be tried before it is configured.
85.2.1.1.14.19. pepsi-stage-block-language Options¶
A stage with PROGRAM = pepsi-stage-block-language reads:
- NEXT_STAGE
(string, required) Where a message scoring above THRESHOLD advances — and, under
ENFORCEMENT = soft, where a flagged message continues too. A stage with no NEXT_STAGE errors at run time.- BOUNCE_STAGE
(string, required when a message can be bounced) Where a message scoring at or below THRESHOLD is routed so a failure DSN can be generated (typically pepsi-stage-bounce(1)). Consulted only under
ENFORCEMENT = hard; a bounce with no BOUNCE_STAGE errors at run time. Worth setting even undersoft, so that switching tohardis a one-word edit.- ENFORCEMENT
(``hard`` | ``soft``, optional) What happens to a message that fails the policy.
hard(the default) routes it to BOUNCE_STAGE, so it is not delivered.softdelivers it anyway, marked: SUBJECT_FLAG_LABEL is appended to itsSubject:, aX-Pepsi-Detected-Languagesheader records what was detected, and the message advances to NEXT_STAGE.softmarks exactly the messageshardwould have bounced, which is what makes it a training mode: run it for a while and the flags in the mailbox are a faithful preview of what enforcement would refuse, at no cost to a correspondent whose mail the lists have not been tuned for yet. Being an ordinary stage option it is per-address overridable (pepsi-settings(1)), so one account can stay in training while the rest of the deployment enforces.Note the
Subject:rewrite breaks the sender’s DKIM signature and this server’s ARCAMS(both over-signSubject), exactly as pepsi-stage-vacation(1)’sVACATION_TAGdoes — free on a branch that ends in local delivery, not free on one that relays onward.- SUBJECT_FLAG_LABEL
(string, optional) The marker appended to a flagged message’s
Subject:underENFORCEMENT = soft. Defaults to[!LANG]. Appended only when the subject does not already contain it (case-insensitively), so a reply quoting it back through the stage does not collect a second one. An empty value re-applies the default; there is no way to switch the marker off, since flagging nothing is the same as leaving the stage out of the pipeline.- WHITELIST
(list, optional) Languages whose probability is added to the score, as whitespace/comma-separated ISO 639-1 codes (case-insensitive) plus the optional pseudo-code
none(matching undetected mail). Empty when unset. An entry may carry further subtags (de-CH); each entry is a language range matched by RFC 4647 §3.3.1 basic filtering, sodealso matches a detectedde-CHwhilede-CHdoes not match a barede.- BLACKLIST
(list, optional) Languages whose probability is subtracted from the score, same syntax as WHITELIST. Empty when unset. A language may appear on at most one of the two lists.
At least one of the two lists must be non-empty: with both empty every message scores
0.0, which the strictscore > THRESHOLDgate then fails, so the stage would bounce (or flag) all mail. The configuration is refused rather than accepted, and refusing it is what stops “I will fill the lists in later” from being a silent outage.- THRESHOLD
(float, optional) The score a message must exceed to advance. Defaults to
0.0.
85.2.1.1.14.20. pepsi-stage-vacation Options¶
A stage with PROGRAM = pepsi-stage-vacation answers mail that arrives while the
envelope recipient is away. Every option below is per-address overridable, and
that is how the feature is meant to be used: the INI section is the operator’s
default (no vacation), and each user’s leave dates — and, if they want, their own
message text — come from their pepsi.settings row or from a config_override
scope. See pepsi-stage-vacation(1).
- NEXT_STAGE
(string, required) Where the message continues. It always does: this stage adds a reply, it never consumes a message. pepsi-setup(1) rejects a stage without it.
- RESPONSE_STAGE
(string, required) Where the vacation notice is injected as a new null-sender message, so it is signed and relayed (typically the DKIM-signing stage). pepsi-setup(1) checks it names an existing stage.
- VACATION_RANGES
(list, optional) Comma-separated
YYYY-MM-DD:YYYY-MM-DDspans of days the recipient is away. Both endpoints are inclusive whole days in the server’s time zone; an empty end (2026-08-01:) is an open-ended leave and a bare date is that single day. Empty when unset, which switches the stage off for that address — the default, and the state of every recipient who has never configured a vacation. A range whose end precedes its start is rejected.- DEFAULT_LANGUAGE
(string, optional) The language whose message is used when none of the sender’s detected languages (
state.language) has one. Defaults toen. Written with either separator:pt_brandpt-BRare the same tag.- DEFAULT_MESSAGE_SECTION
(string, optional) The configuration section holding one inline message template per language, as
<LANG> = <template>. Defaults topepsi-vacation-default-message, which ships in${DATADIR}/config.dwith ten languages. A per-addressMESSAGE_<LANG>option of this section takes precedence over it, per language.- EMERGENCY_CONTACT
(address, optional) A single bare address offered to the message template as
{{EMERGENCY_CONTACT}}. Unset by default, in which case a template’s{{#EMERGENCY_CONTACT}}block is skipped. A display name or a list of addresses is rejected.- VACATION_TAG
(string, optional) Appended to the forwarded message’s
Subject:when a notice was sent, so the recipient can see which mail was answered for them. Defaults to[VACATION]; the sentinel valuenonedisables tagging. An empty value does not — the parser reads an empty option as absent, which re-applies the default. Appending is idempotent.- SUPPRESS_DAYS
(integer, optional) How long after answering one correspondent they may be answered again. Defaults to
7(the Sievevacationdefault, RFC 5230 §4.1);0answers every message. Must not exceed3650(ten years) — the value becomes a SQL interval subtracted fromnow(), and a larger one puts the result outside thetimestamptzrange, which would fail every message to the address rather than the configuration that set it.- REQUIRE_ADDRESSED_TO
(boolean, optional) Answer only when the recipient appears in
To:orCc:(RFC 3834 §3), so a blind carbon copy or a harvested-list blast draws no reply. Defaults toyes.- SUBJECT_PREFIX
(string, optional) Prepended to the original subject to form the notice’s subject. Defaults to
Auto:(RFC 5230 §4.5). A message with no subject yields the prefix alone, and an already-prefixed subject is not prefixed twice.
85.2.1.1.14.21. pepsi-stage-secretary Options¶
A stage with PROGRAM = pepsi-stage-secretary holds mail from senders no
whitelist knows and asks them to confirm by replying (confirm-to-send). Every
option below is per-address overridable, so a user may set their own challenge
text or PENALTY. See pepsi-stage-secretary(1).
- NEXT_STAGE
(string, required) Where whitelisted, confirmed and never-challenged mail (a bounce, locally submitted mail) continues.
- RESPONSE_STAGE
(string, required) Where the challenge and the confirmation notice are injected as new null-sender messages (typically the DKIM-signing stage).
- BOUNCE_STAGE
(string, optional) The timeout path: where held mail goes when nobody confirmed in time, the challenge bounced, or
state.spamistrue. Unset, such mail is deleted.- UNCHALLENGEABLE_STAGE
(string, optional) Where mail goes that must not be challenged: RFC 3834 says not to answer it, its
From:is not the envelope sender, the sender is not authenticated, or the sender’s daily cap is spent. Unset, it takes the timeout path.- WHITELIST_NAME
(string, required) The one whitelist a confirmed sender is written to; may be a
{login}/{localpart}template, expanded per recipient as by pepsi-stage-check-whitelist(1). A list of names is rejected. The operator may name any whitelist; a value an account owner sets by mail through pepsi-stage-edit-settings(1) must name one in their own<login>/...namespace, or the one the operator’s configuration names for them.- CONTROL_LOCAL_PART
(string, optional) Local part of the reply address
<CONTROL_LOCAL_PART>-<cookie>@<domain>. Defaults tosecretary; at most 30 ASCII letters, digits,.,_or-.- HOLD_TIME
(duration, optional) How long held mail waits. Defaults to
120 h;h,mandsunits only.- REQUIRE_AUTHENTICATED
(boolean, optional) Challenge only a sender whose SPF or DMARC passed. Defaults to
yes.- MAX_CHALLENGES_PER_SENDER
(integer, optional) Challenges one address may be sent per 24 hours, across all whitelists. Defaults to
5;0means no limit.- DKIM_REQUIRED
(auto|yes|no, optional) The
dkim_requiredof the whitelist row a confirmation writes.auto(the default) makes it required when any message held for the challenge passed DKIM (or ARC with DMARC).- CONFIRM_NOTICE
(boolean, optional) Tell a confirmed sender their mail was delivered. Defaults to
no.- PENALTY
(string, optional) What the sender agrees to pay if their message was unsolicited, offered to the template as
{{PENALTY}}. Unset by default.- SUBJECT_HINT_LENGTH
(integer, optional) Characters of the held message’s subject the challenge quotes. Defaults to
8;0omits the hint; at most64.- TEMPLATE
(string, optional) Base name of the
<TEMPLATE>.<lang>.bodychallenge templates under[pepsi] TEMPLATE_DIR. Defaults tosecretary-challenge.- SUBJECT
(string, optional) The challenge’s subject when no
SUBJECT_<LANG>matches the chosen language. Defaults toPlease confirm your message to {{RECIPIENT}}.- DEFAULT_LANGUAGE
(string, optional) Language used when none of the sender’s detected languages has a text. Defaults to
en. pepsi-setup(1) requires a template or aMESSAGE_<LANG>for it.- MESSAGE_<LANG>, SUBJECT_<LANG>, CONFIRM_MESSAGE_<LANG>
(string, optional) Per-language challenge text, challenge subject and confirmation text, overriding the templates for that language only.
- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(optional) The shared locality options (pepsi-stage-relay-to-maildir(1)): which domains a reply address may be at, and how the whitelist-name placeholders expand.
LOCAL_DOMAINSdefaults to[pepsi-ingress] ACCEPTED_DOMAINS.
85.2.1.1.14.22. pepsi-stage-if Options¶
A stage with PROGRAM = pepsi-stage-if branches on a member of the message
state JSON:
- STATE_PATH
(string, required) The dot-separated path into the message
statewhose value is tested (object keys and integer array indices, e.g.spam,auth.dkim,dsn.rcpt.0.notify).- VALUE
(string, required, non-empty) The value the member at STATE_PATH is compared against, by its natural scalar text form (string,
true/false, decimal number, ornull). An empty option reads as unset.- TRUE_STAGE
(string, required) Where the message advances when the value equals VALUE. pepsi-setup(1) checks it names an existing stage.
- FALSE_STAGE
(string, required) Where the message advances otherwise (the value differs, the path is absent, or it resolves to an array/object). pepsi-setup(1) checks it names an existing stage.
85.2.1.1.14.23. pepsi-stage-milter Options¶
A stage with PROGRAM = pepsi-stage-milter hands the message to an existing
milter daemon (the sendmail/Postfix mail-filter protocol) and routes it by the
filter’s verdict. See pepsi-stage-milter(1), and note in particular that
the filter runs post-queue, so a REJECT costs a bounce where an MTA would
have answered 5xx inside the SMTP session.
- SOCKET
(string, required) Where the milter daemon listens, in sendmail’s
S=grammar (which Postfix also accepts):unix:/path(or its synonymlocal:/path),inet:host:port,inet:port@host,inet6:[addr]:portorinet6:port@addr. A bare path with no scheme is rejected. Pepsi connects to this socket; it never starts, stops or confines the daemon behind it.- MILTER_NAME
(string, optional) Exported to the filter as the
{daemon_name}macro and used in log messages. Defaults to the stage’s own name.- ACCEPT_STAGE
(string, optional) Where
SMFIR_ACCEPTsends the message. Defaults to NEXT_STAGE, which makes accept and continue the same thing; set it to skip past the rest of a filter chain, which is what Postfix’s milter list does natively. pepsi-setup(1) checks it names an existing stage.- REJECT_STAGE
(string, optional) Where
SMFIR_REJECTsends the message, and where individually rejected recipients are fanned out to. Defaults to the section’sBOUNCE_STAGE. Point it at a pepsi-stage-discard(1) to drop rejected mail instead of bouncing it. pepsi-setup(1) checks it names an existing stage.- QUARANTINE_STAGE
(string, optional) Where
SMFIR_QUARANTINEsends the message. Unset means discard: Pepsi has no quarantine store, only a route. A quarantine request decides the routing regardless of the verdict that follows it, because a sendmail milter quarantines and then still returns an accept. pepsi-setup(1) checks it names an existing stage.- ON_FAILURE
(``tempfail``/``accept``/``reject``/``discard``, default ``tempfail``) What to do when the milter cannot be reached, times out, or speaks something unparsable — Postfix’s
milter_default_action, with the same default.tempfailpauses the message for retry, so a filter that is down delays mail rather than losing it or letting it through unfiltered. A message still deferred after MAX_LIFETIME (by this path or by the filter’s ownSMFIR_TEMPFAIL) goes to the section’sBOUNCE_STAGE, or is leftfailedfor pepsi-failure-bouncer(1) when there is none — never to REJECT_STAGE, which may drop mail silently.- ALLOW_ACTIONS
(string, default ``addhdrs chghdrs chgbody``) The
SMFIF_*modification actions offered to the filter, whitespace- or comma-separated:addhdrs,chghdrs,chgbody,addrcpt,delrcpt,addrcpt_par,chgfrom,quarantine; plus the shorthandsallandnone. There is noinshdr: libmilter gates header insertion onSMFIF_ADDHDRStoo.With no sandbox around the filter this mask is the one blast-radius lever available, and the default is where it earns its keep: every content filter works under it, while a filter that wants to rewrite the envelope has to be granted that explicitly. A filter requiring an action that was not offered is reported by name and takes the ON_FAILURE path, rather than having its modifications silently dropped the way Postfix drops them.
SMFIF_SETSYMLISTis deliberately not listed and needs no grant: it authorises no change to the message.- PROTOCOL_VERSION
(integer 2–6, default 6) The highest milter protocol version offered; the filter negotiates down from it. Postfix’s
milter_protocol.- CONNECT_TIMEOUT
(duration, default ``30 s``) Bound on establishing the connection and completing the option negotiation — both halves of “can this filter be reached at all”. Postfix’s
milter_connect_timeout.- COMMAND_TIMEOUT
(duration, default ``30 s``) Bound on the reply to any single command up to end-of-body. Postfix’s
milter_command_timeout.- CONTENT_TIMEOUT
(duration, default ``300 s``) Bound on the end-of-message verdict, where a content filter does its real work. An
SMFIR_PROGRESSfrom the filter restarts the clock. Postfix’smilter_content_timeout.Each of the three timeouts must be greater than zero and at most
24 h.- RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR / MAX_LIFETIME
(durations / number, optional) The shared retry schedule for the tempfail path, with the same meaning as in the relay stages. Defaults:
300 s,1 h,2,120 h.- MACROS_CONNECT / MACROS_HELO / MACROS_MAIL / MACROS_RCPT / MACROS_DATA / MACROS_EOH / MACROS_EOM
(string, optional) The sendmail macros exported at each protocol phase, whitespace- or comma-separated. The defaults are Postfix’s, so a filter’s own documentation applies unchanged:
j {daemon_name} {daemon_addr} v _,{tls_version} {cipher} {cipher_bits} {cert_subject} {cert_issuer},i {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer},i {rcpt_addr} {rcpt_host} {rcpt_mailer}, andifor the remaining three. A macro whose value this deployment does not know is not sent at all. Set a list to the sentinelnoneto export nothing at that phase — an empty value reads as absent and re-applies the default. A filter that names its own list withSMFIF_SETSYMLISToverrides these.
85.2.1.1.14.24. pepsi-stage-route Options¶
A stage with PROGRAM = pepsi-stage-route chooses a next stage for each
envelope recipient from that recipient’s domain, splitting the message onto
sibling rows when the recipients disagree. It exists for a gateway sitting in
front of another mail system (see the manual’s “Microsoft Exchange as a gateway”
chapter): the domains behind such a gateway are exactly the domains whose public
MX is the gateway, so routing one of them to a direct-to-MX relay is a mail
loop rather than a delivery failure.
Lookup order per recipient is ROUTES, then the managed-domain set, then NEXT_STAGE.
- ROUTES
(string, optional) Whitespace- or comma-separated
<domain-pattern>=<stage>pairs, consulted first and in the order written. Patterns are matched case-insensitively and may contain*, which matches any run of characters; they are globs, not regular expressions, and are anchored at both ends, so*.example.orgmatchesmail.example.orgbut neitherexample.orgnormail.example.org.evil.test. A value that looks like a regular expression is rejected rather than silently matching nothing. Explicit rules are consulted before the managed set so one subdomain can be carved out without removing its parent from MANAGED_DOMAINS.- MANAGED_DOMAINS
(string, optional) The domains behind this gateway, whitespace- or comma-separated,
*globs permitted. Defaults to[pepsi-ingress] ACCEPTED_DOMAINS, so the set cannot drift from what ingress actually accepts. At least one domain must resolve, from either source.- MANAGED_STAGE
(string, optional) Where a recipient at a managed domain goes — in a gateway deployment, the stage that relays to the system behind it. Without it, managed recipients take NEXT_STAGE, which pepsi-setup(1) then has to prove is not a loop.
- NEXT_STAGE
(string, required) Where every other recipient goes. Required: the stage never delivers, bounces or drops a message, so every recipient must have somewhere to be.
- RECIPIENT_CHECK
(``none``, ``probe`` or ``map``, optional) How pepsi-ingress(1) may learn whether an address at a managed domain exists on the system behind the gateway, so that it can refuse an unknown one at
RCPTtime (see[pepsi-ingress] VERIFY_RECIPIENTS). The route itself never reads it; it is set here because this section describes the backend. Only used together with MANAGED_STAGE.none(the default) leaves the managed domains unverified: every address is accepted, and the backend’s own bounce is the answer.probeasks the backend:MAIL FROM:<>,RCPT TO,RSET, with no message sent. Only a5.1.xrefusal (or a bare550) counts as “no such user”. Anything else — the backend unreachable, a deferral, a full mailbox — accepts. Answers are cached, a “yes” for an hour and a “no” for five minutes. A backend that accepts every address and bounces later (Exchange with recipient filtering off) always answers “yes”; for such a backend usemap, which reads the valid addresses from RECIPIENT_MAP.- PROBE_HOST, PROBE_PORT, PROBE_TLS, PROBE_TIMEOUT
(optional; PROBE_HOST required for ``probe``) Where and how to probe: the backend’s host name or address, its port (default
25),opportunistic(the default),starttls,tlsornone, and the per-command timeout (default10 s). The probe carries an address, never a message, so opportunistic TLS accepts any certificate, as delivery to an MX does;starttlsandtlsverify it.- RECIPIENT_MAP
(path; required for ``map``) The valid addresses, one per line: an address, or
@domainfor every address at a domain. Anything after the first word is ignored, so the source file of a Postfixrelay_recipient_mapstable (alice@example.org OK) can be used as it is.#starts a comment. The file is read on each check. If it cannot be read, every address is accepted.
pepsi-setup(1) checks every target names an existing stage, and refuses
a configuration in which a message for a managed domain could reach
pepsi-stage-relay-to-internet(1) — walking forward through NEXT_STAGE
and any further routing stages, but deliberately not through BOUNCE_STAGE (a
bounce is a new message about a delivery that already failed, and legitimately
reaches a relay).
85.2.1.1.14.25. pepsi-stage-aliases Options¶
A stage with PROGRAM = pepsi-stage-aliases expands the envelope recipients of
a message through an alias mapping file before forwarding it:
- ALIASES
(string, required) The alias map file — a filesystem path taken verbatim, so unlike a path-typed option it is not
$-expanded — parsed like a Postfix virtual(5) table: each line maps a key (a full address, an@domaincatch-all, or a*wildcard such assales-*@example.org) to one or more comma/whitespace-separated target addresses;#comments and blank lines are ignored. Recipients matching a key are replaced by its targets (transitively, with a loop guard) and the result is de-duplicated; on competing wildcards the most specific match wins. The file is re-read only when its modification time changes; a missing file is treated as an empty map. pepsi-setup(1) syntax-checks it at install time when present. See pepsi-stage-aliases(1).- NEXT_STAGE
(string, required) Where the (expanded) message advances. pepsi-setup(1) checks it names an existing stage.
85.2.1.1.14.26. pepsi-stage-edit-settings Options¶
A stage with PROGRAM = pepsi-stage-edit-settings reads:
- EDITABLE_STAGES
(list, required) Whitespace/comma-separated stage names whose options a control e-mail may change. At least one is required, each must name an existing stage (pepsi-setup(1) checks), and a body addressing any stage not listed here is refused.
- RESPONSE_STAGE
(string, required) Stage at which the reply is injected (typically the start of the outbound signing/relay path), so it is DKIM-signed and delivered. Must resolve to an existing stage.
- NEXT_STAGE
(string, required) Where a non-control message is advanced. A stage with no NEXT_STAGE errors at run time on the first ordinary message.
- SUBJECT
(string, optional) The exact subject that marks a control message, and the subject of the reply. Default
Pepsi.- CONTROL_LOCAL_PART
(string, optional) Local part of the control address. Default
pepsi.- RESPONSE_FROM
(string, optional)
From:header of the reply. Default<CONTROL_LOCAL_PART@domain>for the controlled domain the message was addressed to.- UNRESTRICTED_UNSAFE_STAGES
(boolean, optional) Lift the safety restriction on what a control e-mail may set. Default
NO.With the default, two things are refused however permissive EDITABLE_STAGES is. An assignment that sets a stage’s
PROGRAMis accepted only when the value is a barepepsi-stage-*command with no path component, so an account owner cannot point one of their editable stages at an arbitrary executable; and an assignment that sets an identity option —SIGNING_DOMAIN, which overrides the message’s ownFrom:domain and so would have an account’s mail signed as any domain this host holds a key for — is refused outright, there being no safe value to allow. Setting this toYESremoves both checks — the option is named after what it does, and there is no reason to turn it on that does not begin with trusting every address whose overrides are editable.
85.2.1.1.14.27. pepsi-stage-vks-confirm Options¶
A stage with PROGRAM = pepsi-stage-vks-confirm follows the confirmation link
in a key server’s address-verification mail, so an uploaded key becomes findable
by address. It sits on the inbound path and advances everything it does not
act on — nearly all mail — untouched. See
pepsi-stage-vks-confirm(1) for the full list of conditions it checks before
fetching anything.
- NEXT_STAGE
(string, required) Where every message this stage does not consume goes. Required rather than optional: a stage on the inbound path that silently dropped what it did not recognise would lose ordinary mail. pepsi-setup(1) checks it names an existing stage.
- VKS_HOST
(string, optional) The one host a verification mail may come from and a link may point at. Defaults to the host of
[pepsi-keys] VKS_SERVER, and pepsi-setup(1) refuses any other host: uploads go to that server, so its verification mail is the only kind the stage exists to act on. Matched exactly: a subdomain of a key server is not the key server. Optional only because of that fallback — with neither this option nor aVKS_SERVERhost the stage refuses to start, having no way to tell a key server’s mail from anybody else’s.- MAX_LINKS
(integer, optional) How many links one message may cost. Default
3, minimum1(0is refused: to follow no links at all, take the stage out of the pipeline). A genuine verification mail carries one; the bound is what stops a message that got past every other check from becoming an arbitrary number of requests.- DISCARD_CONFIRMED
(boolean, optional) Whether a confirmation mail the stage acted on is deleted rather than delivered. Default
YES: it is machine mail about an action Pepsi took, addressed to a mailbox whose owner did not ask for it, and it has already been acted on by the time it would arrive. Only a mail after which at least one identity was verified as published is deleted; when following its links verified nothing, it is delivered regardless.- REQUIRE_AUTHENTICATION
(boolean, optional) Whether SPF or DMARC must have passed, not merely the envelope sender matching VKS_HOST. Default
YES. Note that a DKIM pass deliberately does not satisfy this: it says a signature verified, not whose.
There is no BOUNCE_STAGE: the stage never fails a message.
85.2.1.1.15. The [pepsi-dispatch] Section¶
Options for pepsi-dispatch(1). Per-stage worker-pool sizing
(PARALLELISM, MAX_MESSAGES, QUEUE_LIMIT) lives in each
[stage-<name>] section above, not here. Every duration below must be greater
than zero and at most 168 h; pepsi-setup(1) and the dispatcher refuse
anything else.
- CONFIG_FILE
(path, optional) The configuration file passed to spawned workers via
-c. Set it to this file’s path whenever the dispatcher is started with an explicit-c(the loaded path cannot be recovered otherwise); if unset, workers use the default configuration search.- POLL_INTERVAL
(duration, optional) Safety-net heartbeat: the longest the dispatcher sleeps when no sooner
pausedretry, worker reap or spawn-cooldown is due, at which point it re-scans for work and requeues any duepausedrows. Default 30 s. It is an absolute deadline, not a quiet period: message traffic, and a shorter STATS_INTERVAL, do not push it back, sopausedrows are requeued on time even on a busy pipeline.- MAX_RUNTIME
(duration, optional) Maximum real time a worker may spend on a single message. A worker that exceeds it is killed (and replaced) and the message set to
timeout. Default 300 s.- WORKER_IDLE_TIMEOUT
(duration, optional) How long an idle worker process is kept before it is stopped. Total concurrency is set per stage (PARALLELISM), not globally. Default 5 s.
- STATS_INTERVAL
(duration, optional) How often the in-memory pipeline statistics (exported by pepsi-httpd(1) at
/metrics) are flushed to the database, in a single transaction. Also flushed as soon as the pipeline goes idle (at most once a second) and on shutdown; an idle dispatcher with nothing new to record writes nothing. Default 60 s.- DB_POOL_SIZE
(number, optional) Maximum number of PostgreSQL connections in the dispatcher’s database pool. Unlike a single-connection stage worker, the dispatcher processes many workers’ results (advancing/failing their messages) concurrently with its own coordinator queries, so it keeps a small bounded pool. The per-statement work is brief, so a handful of connections absorbs heavy worker churn. Defaults to
8; raise it only if the coordinator is connection-starved, and keep the sum of every component’s pool below PostgreSQL’smax_connections.- FAIR_SCHEDULING
(boolean, optional) Share each stage’s worker time fairly among senders (the authenticated account, else the connecting address, an IPv6 one by /64; see “Fair scheduling across senders” in pepsi-dispatch(1)) instead of claiming strictly oldest first, so that one sender’s burst does not delay everybody queued behind it. Default
yes;norestores oldest-first.- FAIR_HALF_LIFE
(duration, optional) How quickly a sender’s accumulated lead in worker time fades: it halves every FAIR_HALF_LIFE, so the penalty for a past burst wears off once the sender stops. Default 60 s.
- FAIR_FIFO_PERCENT
(number, optional) Percentage of every stage’s claimed slots reserved for its oldest waiting messages regardless of sender — the guarantee that no message waits for ever, however many other senders keep arriving.
1to100;100is oldest-first with extra bookkeeping (use FAIR_SCHEDULING= nofor that). Default 10.
85.2.1.1.16. The [pepsi-list] Section¶
Options for pepsi-list(1) and the mailing-list subsystem, read by that tool, by the list stages and by pepsi-setup(1). Every option has a default, so a site that hosts no lists needs no section at all.
Pepsi’s mailing-list subsystem is a reimplementation of GNU Mailman 3; its data
model, REST API, e-mail command vocabulary and notice-template names are the
GNU Mailman project’s design, copyright the Free Software Foundation. See
COPYING and vendor/PEPSI-VENDORING.md for the files taken from
upstream, which remain under the GPL.
Per-list configuration is not here. Lists are created at run time by people
who are not the operator, potentially in their thousands; this file is
operator-owned and reloaded as a unit. The 99 per-list attributes live in the
mailing_list table and are managed with pepsi-list(1). What follows is
only what the site owns.
- SITE_OWNER
(address, optional) Where a list’s notices go when the list has no owner of its own. Without it, a list with no owner is an error from
pepsi-list checkrather than a warning, because its held messages and moderation requests would reach nobody at all.- BASE_URL
(URL, optional) Public URL the archive links and the
List-*headers point at, for domains that do not set their ownbase_url. Without it noList-Archive:header can be written.- API_USER, API_PASS
(string, optional) HTTP Basic credentials for the GNU Mailman 3 REST API – upstream’s
[webservice] admin_user/admin_passunder a Pepsi name. Set both or neither: half a credential authenticates nobody, andpepsi-list checkreports one without the other as an error. The API is served only on a listener flaggedLIST_API = yes.- API_BASE_URL
(URL, optional) The absolute origin every
self_linkandLocationthe REST API emits is built from, e.g.http://localhost:8001; a trailing/is stripped. Unset, the request’s ownHostis used, which is right behind a reverse proxy.- API_RATE_LIMIT
(integer, optional) Requests per minute per source address on the REST API. Default
3000, deliberately high so a compatibility suite is not throttled; a429from that API is this limiter.- DEFAULT_LANGUAGE
(string, optional) The language a new list starts in, and the bottom of the preference chain (
GET /<api>/system/preferences). Defaulten.- RELEASE_STAGE
(string, optional) The
[stage-<name>]section running pepsi-stage-list-post(1), where a moderator’s accept of a held message re-enters the pipeline. Without it held messages can be read and discarded but not accepted, and pepsi-setup(1) warns.- NOTICE_STAGE
(string, optional) The stage the periodic bounce sweeps of
pepsi-list tasksinject their member notices at. pepsi-setup(1) requires it, and checks that it names an existing stage, as soon as a pepsi-stage-list-bounce(1) stage exists: without it a member’s warning count rises without the member ever being told.- BOUNCE_PROBES
(boolean, optional) Whether a member whose bounce score crosses the list’s threshold is sent a probe (the score reset while it is outstanding, and a bounce of the probe disabling the member) rather than disabled at once. Default
NO. See pepsi-stage-list-bounce(1).- BOUNCE_EVENT_RETENTION
(integer, optional) How long a processed bounce event is kept, in days. Default
1.- UNSUBSCRIBE_SECRET
(string, optional) Secret keying the RFC 8058 one-click unsubscribe token, which pepsi-stage-list-deliver(1) computes and pepsi-httpd(1) verifies. Without it the
https:one-click form is not emitted and themailto:form stands alone. pepsi-setup(1) writes it tosecrets.d/pepsi-list.secret(mode 0640,pepsi-httpd:pepsi) and references it with@inline-secret@.- TEMPLATE_FETCH_TIMEOUT, TEMPLATE_FETCH_MAX_BYTES, TEMPLATE_FETCH_ALLOW_INTERNAL
(duration, integer, boolean; optional) Limits on fetching a list’s
list_templateURI override, which a list owner rather than the operator sets: the whole-fetch deadline (default5 s), the largest template accepted (default65536bytes), and whether a host resolving to an internal address may be fetched (defaultNO; every address the host resolves to is checked).- WEB_RATE_LIMIT, WEB_SEARCH_RATE_LIMIT
(integer, optional) Requests per minute per source address on the public list web interface (a
[pepsi-httpd-listener-*]withLISTS = yes), and the separate, tighter budget for its search route. Defaults600and30; a value below1is raised to1.- MAX_MESSAGE_SIZE
(integer, optional) Site ceiling on a list’s
max_message_size, in kilobytes;0(the default) means no site ceiling. A per-list value above it is clamped, andpepsi-list checkreports it.- MAX_LIST_HOPS
(integer, optional) How many times a message may pass through a list before it is treated as a subscription cycle between two lists and discarded. Default
5. An umbrella list – a list subscribed to another list – is a supported configuration; this is what stops two of them subscribed to each other from amplifying. The counter travels as anX-Pepsi-List-Hops:header rather than in the message state, because an umbrella list’s mail leaves over SMTP and comes back as a brand-new message;pepsi-ingressstrips the header from mail that did not arrive over an authenticated or local submission path, so it cannot be reset by a stranger.- INVITE_RATE
(integer, optional) Cutover invitations per hour. Default
300. A migrating site’s first act is a mass mailing to its entire membership from an IP with no reputation, so the invitation run is rate-limited and resumable rather than a loop.- ARCHIVE_RETENTION
(integer, optional) Default archive retention in days;
0(the default) keeps everything. Overridden per list withpepsi-list list set-ext <list> archive_retention_days <n>, where0keeps that list’s archive whatever this says. Applied daily by the retention sweep ofpepsi-list tasks --once(the pepsi-list-tasks.timer unit).- SEARCH_TRIGRAM
(off | short | full, optional) Site ceiling on how far a list’s trigram (substring and fuzzy) search index may reach. Default
short.A ceiling, not a default: each list chooses
off,shortorfullup to it, so lowering this value shrinks the index at the nextpepsi-archive reindexrather than only affecting lists created afterwards.shortindexes the subject, the thread subject and the sender’s name and address;fulladds the message body and is likely to be the largest object in the database.Trigram search needs the
pg_trgmextension, which ships inpostgresql-contrib. A site without it installs and runs normally with full-text search only;pepsi-list checksays which mode the site is in.- LIST_FANOUT_BATCH
(integer, optional) Sibling rows materialised per fan-out batch. Default
256.A post to a list is delivered as one queue row per recipient, so that each copy can carry that member’s own one-click unsubscribe URI. This bounds how many exist at once, and therefore how much transient storage a large attachment to a large list occupies: a 5 MB post to a 5 000-member list peaks at about 1.3 GB rather than 25 GB. Raising it makes a large send drain faster and cost more space.
- TOMBSTONE_RETENTION
(integer, optional) How long a purge tombstone is kept, in days. Default
90.Purging a message from the archive keeps its
Message-IDfor this long, so that a resend or a re-run import cannot silently re-archive the thing that was purged.pepsi-archive purge --forgetskips the tombstone for the case where even ninety days is unwanted.- ARCHIVE_PARTITIONS
(integer, optional) Hash partitions for the archive tables. Default
32. Read only by pepsi-setup(1), and fixed at install.PostgreSQL has no online re-partitioning: changing the modulus means a new table and a full copy of every archived message.
pepsi-setupcreates the partitions once and refuses to change them silently on a later run, andpepsi-list checkreports a configuration that disagrees with what is on disk – the installed value is the authority, because the tables cannot be edited and this file can. Pick it once.
85.2.1.1.17. The [pepsi-failure-bouncer] Section¶
Options for pepsi-failure-bouncer(1), which moves failed/timeout
messages to a bounce stage so the sender is notified. Read only by that tool.
- BOUNCE_STAGE
(string, required when the tool is used) The
[stage-<name>]label a stuck message is moved to (and reset topending). Normally the same bounce stage the pipeline already routes permanent failures to (e.g.bounce). A message already at this stage is left untouched, so a bounce that fails at the bounce stage does not loop. pepsi-setup(1) checks the named stage exists. Needed on every deployment that runspepsi.target: itspepsi-failure-bouncer.timerruns the tool every ten minutes, and each run fails without this option. The shipped configuration and the wizard’s set it tobounce.- MIN_AGE = DURATION
How long a message must have been
failed/timeoutbefore it is moved (default1 h): the window in which an operator can put it back with pepsi-queue(1) before its sender is told. Measured fromstate.failed_at, which the database stamps when the row fails; a row without the stamp is moved at once.0 smoves every failure straight away.
85.2.1.1.18. The [pepsi-sendmail] Section¶
Options for pepsi-sendmail(1), the Sendmail-compatible local submission
client the Debian package installs as /usr/sbin/sendmail. Every option has a
default, so a site needs no section at all.
The client is unprivileged and asserts no identity: it hands the message to a UNIX-domain ingress listener, which derives the sender’s account from the connecting process’s kernel-reported credentials (AUTH_PEERCRED, above).
- SOCKET
(path, optional) The
pepsi-ingressUNIX-domain submission socket. Default/run/pepsi/submission.sock.Despite living in the client’s section, this option is read by both ends:
pepsi-ingressserves this socket unconditionally and synthesises its listener from this path, so there is no[pepsi-ingress-listener-*]section for it and no second declaration that could name a different path.On systemd the descriptor is inherited when
pepsi-ingress.socketalready binds this path — matched by the address the descriptor is bound to, not by an FD_INDEX — so a change here needs a matchingListenStream=in the unit. Otherwise the server binds the path itself, mode0666, and refuses to start if it cannot. That mode grants nothing by itself: opening the socket conveys no right to send, because the kernel supplies the caller’s identity and USERNAME_MAP decides what it may send as.The special value
nonedisables local submission entirely: no socket is served and pepsi-sendmail(1) refuses to run, so local programs must authenticate on 587/465. pepsi-setup(1) reports this once.- EHLO_NAME
(string, optional) Name announced in
EHLO. Defaults to[pepsi-ingress] HOSTNAME. Cosmetic: the server authenticates the peer by its credentials, not by what it claims here.- DEFAULT_DOMAIN
(string, optional) Domain appended to the invoking account’s login to form the envelope sender when neither
-fnor-rwas given. Defaults to[pepsi-ingress] HOSTNAME(and tolocalhostif even that is unset).Set it when the addresses your
USERNAME_MAPrecognises are at some other domain: the server maps the peer’s uid to a login and then to an address, so a mismatch here is what makescronmail arrive from an envelope sender the site does not own.
85.2.1.1.19. The [pepsi-whitelist] Section¶
Options for pepsi-whitelist(1), the tool that manages the sender whitelist
and can seed one from a user’s mailbox (import). Read only by that tool; every
option is optional, so a site that only uses add/list/remove needs no
section at all.
- LOCAL_DOMAINS
(string, optional) Domains whose senders count as the user’s own mail when a mailbox is scanned: a message whose
From:/Sender:is at one of them is one the user sent, so its recipients become whitelist entries. Comma- or space-separated. Defaults to[pepsi-ingress] ACCEPTED_DOMAINS, and is the same option pepsi-stage-relay-to-maildir(1) and pepsi-stage-dot-forward(1) use.- TARGETS, RECIPIENT_DELIMITER
(string, optional) As for the delivery stages.
TARGETSbounds which local accountsimport --all-usersseeds (default: the/etc/login.defsUID_MIN..``UID_MAX`` range);RECIPIENT_DELIMITERstrips a sub-address before an address is compared.- HOSTERS_FILE
(path, optional) File listing public e-mail hosters, one domain per line (
#comments; a leading*.covers a subdomain tree). Domains in it are never proposed as an*@domainwildcard byimport --auto-wildcard, however many correspondents the user has there — “everyone atgmail.com” is not a set of people the user knows, and whitelisting it would hand every spammer with a free account a bypass of the anti-spam gate. Their individual addresses are still imported. Defaults to${DATADIR}/hosters.txt, whichmake installwrites; point this at your own copy to extend the list, since the installed one is overwritten on upgrade.- WILDCARD_THRESHOLD
(number, optional) How many distinct addresses at one domain make
import --auto-wildcardpropose a single*@domainentry for it. Default10.- MAX_ENTRIES
(number, optional) Most rows one
importmay add. Default5000. Every row of a whitelist is matched against theFrom:of every inbound message that consults it, so this is a bound on a permanent per-message cost, not just on table size.- SCAN_HELPER
(string, optional) The mailbox-scan helper binary, located on
$PATH. Defaultpepsi-helper-mailbox-scan(see pepsi-helper-mailbox-scan(1)).- MAILBOX
(path, optional) Mailbox to scan when the user names none. Defaults to the user’s
~/Maildirif it exists, else/var/mail/<login>.
85.2.1.1.20. The [pepsi-httpd] Section¶
Options for pepsi-httpd(1). The server also reads the served domains
(ACCEPTED_DOMAINS), the MTA-STS mx host (the ingress HOSTNAME) and the
[pepsi] MTA_STS_* options.
- MAX_CONNECTIONS
(number, optional) Maximum number of HTTP connections served concurrently. Default 256.
- MAX_CONNECTIONS_PER_IP
(number, optional) Maximum simultaneous connections from one client address (an IPv4 address or an IPv6
/64); a connection over it is closed at once. Connections over a UNIX socket — from a reverse proxy — carry no client address and are not counted, so behind a proxy this limit belongs in the proxy. Default 32;0disables it.- DB_POOL_SIZE
(number, optional) Maximum number of PostgreSQL connections in the HTTP server’s database pool. The server handles requests concurrently, so unlike the single-connection stage workers it can use more than one. Defaults to
1; raise it only if the endpoints are bottlenecked on the single connection, and keep the sum of every component’s pool below PostgreSQL’smax_connections.- RESUME_AUTHORIZATION_TOKEN
(string, optional) Bearer token guarding
POST /resume(compared in constant time); the endpoint is disabled (404) when unset. Use the Talersecret-token:form so the same value can be reused as the merchantpepsi-resumewebhook’sAuthorizationheader (see pepsi-stage-anti-spam(1) and the[pepsi-payments]section).- ADDIN
(boolean, optional) Whether the Outlook add-in routes (
GET /addin/manifest.xmlandGET /addin/taskpane.html) answer. Defaultno: a deployment that is not fronting Exchange has no use for them, and a manifest naming a gateway nobody configured is a support question waiting to happen. See the manual’s “Microsoft Exchange as a gateway” chapter.- ADDIN_URL
(string, optional) The public
https://origin substituted into the add-in assets. Unset (the default) derives it from each request’sHostheader, which is what works both whenpepsi-httpdterminates TLS itself and when it sits behind a reverse proxy — in the latter case it sees plain HTTP over a UNIX socket and cannot tell the public scheme from the connection. Set it when theHostreaching Pepsi does not name the gateway. The scheme must behttps, and this is enforced: Outlook refuses to load add-in resources over plain HTTP, so anhttp://origin could only produce a manifest that fails at load time, andpepsi-httpdtherefore refuses to start with one rather than serving it. The single exception is a loopback host –localhost, anything under.localhost,127.0.0.0/8or[::1]– where plainhttp://is accepted so a developer or a test needs no certificate. The value is checked whenever it is set, whether or notADDINis on, so a wrong one is reported before the feature is switched on.
Each [pepsi-httpd-listener-<name>] section binds one socket, with the same
SERVE (tcp/unix/systemd) and transport options as a
[pepsi-ingress-listener-<name>] section, except a tcp listener defaults to
PORT 443 and MODE is plain or tls (no STARTTLS). A tls
listener may set TLS_CERT/TLS_KEY as a fallback certificate. It
additionally accepts:
- ADMIN =
yes|no Whether the administrative surface — the
/api/v1API, the/uiadministration console and the/metricspage — is served on this listener. Optional; defaults tono. On a listener without it those routes answer a plain404— byte-for-byte what any unknown path gets — so publishing the public listener does not publish administration./metricsis in that set because per-stage queue depths, crash counters and stage names describe the pipeline; unlike the other two it takes no credential (a Prometheus scraper has none to present), so on a metrics listener this flag is the only access control. See pepsi-httpd(1).pepsi-httpd(1) refuses to serve them on a flagged listener that would carry them in cleartext off this host: plaintext
tcpis accepted only on a loopback address, whiletlslisteners andunixsockets always qualify. A plaintextSERVE = systemdlistener never qualifies — the unit decided where the inherited descriptor listens and the server cannot see that address, so the case it cannot judge is refused rather than allowed. A flagged listener that fails the test logs a warning at start-up and serves the public endpoints only, and pepsi-setup(1) reports the same at validation time.The shipped configuration flags one
unixlistener, which is the shape that needs no credentials at all: a connecting process is identified bySO_PEERCRED. See[pepsi-admin]below.- LIST_API =
yes|no Whether the GNU Mailman 3 compatible REST API (the
/3.0/and/3.1/routes) is served on this listener. Optional; defaults tono, in which case those routes are a plain404. It is subject to the same binding test as ADMIN (TLS, aunixsocket, or plaintexttcpon loopback), and its credential is[pepsi-list] API_USER/API_PASS.- LIST_API_TESTING =
yes|no Whether that API additionally offers the test-only
/3.1/reserved/resethook, which empties the list and archive tables. Optional; defaults tono, and has no effect unless LIST_API is usable on the listener. For a compatibility test bed only.- LISTS =
yes|no Whether the public mailing-list web interface (archives, subscription pages) is served on this listener. Optional; defaults to
no. Unlike the two flags above it is meant to be reachable from the open internet, so the binding test does not apply to it. Its rate limits are[pepsi-list] WEB_RATE_LIMITandWEB_SEARCH_RATE_LIMIT.
Each [pepsi-httpd-cert-<name>] section registers one certificate for SNI
selection: SNI (one or more host names), TLS_CERT and TLS_KEY.
85.2.1.1.21. The [pepsi-admin] Section¶
Options for the administrative API served by pepsi-httpd(1) on a listener
flagged ADMIN = yes. Every option has a default, so the section may be
omitted entirely. The authentication model, the scope table and the endpoint
reference are in the manual’s “The administrative API” chapter.
The same options govern the /ui administration console, which is a client of
that API and introduces no configuration of its own: it shares the session
timeouts, the rate limits, the body cap, the page size and the
configuration-write connection. See the manual’s “The administration console”
chapter.
- ADMIN_GROUP
(string, optional) UNIX group whose members are local administrators over a UNIX-socket listener, identified by
SO_PEERCRED.rootalways is, group or no group. Defaultpepsi-admin.- SESSION_IDLE
(duration, optional) Idle timeout of a browser session, pushed forward on every request. Default
30 m.- SESSION_LIFETIME
(duration, optional) Hard lifetime of a browser session, never extended. Default
12 h. There is no “remember me”: this console can revoke a key and read who corresponds with whom, and local administrators — who log in most often — authenticate bySO_PEERCREDand have no session to expire.- EVENT_RETENTION_DAYS
(number, optional) How long audit records are kept, in days. Default
90.pepsi-httpd pruneapplies both retentions, run daily bypepsi-log-prune.timeras thepepsi-httpdaccount — the one database role holdingDELETEon the logs, since no component that processes mail may erase them. The timer is independent of the web server: retention holds whether or notpepsi-httpd.serviceruns.- MAIL_LOG_RETENTION_DAYS
(number, optional) How long mail-log records are kept, in days, when
[pepsi] MAIL_LOGis on. Default30.Both retentions are integers of days rather than durations because
Section::durationparses throughjiff::SignedDuration, which accepts onlyh/m/sand below.- RATE_LIMIT
(number, optional) Requests per minute per source address (a UNIX-socket listener shares one key for the whole host).
0disables the limit. Default120.- LOGIN_RATE_LIMIT
(number, optional) Login attempts per minute, counted per account and per source address — many passwords against one account and one password against many accounts are different attacks.
0disables the limit. Default10.- MAX_BODY
(number, optional) Largest accepted request body, in bytes. Default
1048576.- PAGE_LIMIT
(number, optional) Default page size of a listing endpoint. A caller may ask for up to 1000; every listing honours
limitandoffsetin its SQL and reports an exacttotalbeside the page, and nothing returns an unbounded result set. Default100; a configured value outside1..``1000`` is clamped into that range rather than rejected.- CONFIG_DB
(PostgreSQL connection string, optional) Connection used for configuration writes. Unset by default, in which case
PUT/DELETEon/api/v1/configanswer503with the reason and every other endpoint is unaffected.Writing
pepsi.config_overridebelongs to thepepsi-configdatabase role, which pepsi-setup(1) grants and explicitly revokes from every service account: a component that processes mail must not be able to rewrite the pipeline it runs in. pepsi-httpd is such an account, and peer authentication keys off the effective uid, so its own connection cannot be that role. Point this at a connection that authenticates aspepsi-config— a password in asecrets.dfragment readable only by thepepsi-httpdaccount, or apg_identmap. The server checks withSELECT current_userat start-up and refuses a connection that authenticates as anything else, because a boundary the code merely believes in is not a boundary. The refusal is not fatal: it is logged, the configuration-write path stays off, and the503carries the role it actually got — a server that started with a privilege it was not meant to have would be the worse outcome.The online setup enqueues through the same connection and for the same reason: only
pepsi-configmayINSERTintopepsi.setup_task, and the privileged applier refuses any row that says otherwise. WithoutCONFIG_DBthe setup endpoints answer503too — the server can serve the questions and watch a task, and cannot ask for one.- APPLY_SOCKET
(path, optional) Where to ring for the privileged setup applier. Default
/run/pepsi/setup-apply.sock.After enqueuing a setup task, pepsi-httpd connects to this socket and closes it. That connection is the entire message: nothing is written to it and nothing is ever read from it, so it cannot become a command channel. What it does is let systemd’s socket activation start
pepsi-setup apply, which takes its work from the database — where every row is validated and audited — and exits again when there is none.A socket rather than a signal or a spawn because pepsi-httpd runs unprivileged and has no way to start a root process, and should not acquire one. This is the one mechanism that lets it cause a privileged process to run without being able to influence what that process does. A failure to connect is not an error: the task is durably queued either way, and
pepsi-setup apply --oncefrom cron or a console picks it up.This path is also what
APPLIER = autoprobes; see below.- APPLIER
(
auto|yes|no, optional) Whether a privileged setup change can happen from this server at all. Defaultauto.The applier’s systemd units are packaged separately (Debian pepsi-httpd-admin, or
make install INSTALL_ADMIN_UNITS=nofrom source), so that a deployment which is administered from a terminal can remove the one mechanism by which an HTTP request reaches/etc/pepsi. With them gone nothing drainspepsi.setup_taskand an insert into it is inert — so the console must not go on offering forms whose submissions would silently never be applied.autoProbe two things: that
pepsi-setup-apply.socketexists as a unit file where systemd looks for one (installed), and thatAPPLY_SOCKETexists, is a socket and is writable by this account (armed — connecting to a UNIX socket needs write permission). The socket is never connected to: connecting is the doorbell ring, which would start a root process on every page render. The probe fails closed: any uncertainty, including a path that cannot be examined and a socket node left behind by a removed package, reads as “no applier”.yesAssume the applier is reachable and skip the probe. For a deployment that drains the queue some other way —
pepsi-setup apply --oncefrom cron, a hand-written unit, a host without systemd — where the probe would report a capability that does exist. It is a promise the operator makes; nothing verifies it.noRefuse every privileged setup request whatever is installed. A way to make the console read-only without changing the package set.
When there is no applier, pepsi-httpd still serves the setup interview, the staged answers and the task history — read-only, with the controls disabled — and answers the mutating endpoints
503setup_applier_unavailable. See pepsi-httpd(1).
85.2.1.1.22. The Egress Identity Option: PUBLIC_IP¶
- PUBLIC_IP = ADDR [ADDR …]
Whitespace- or comma-separated list of public IPv4 and/or IPv6 addresses that send mail as your domains. Each becomes an
ip4:orip6:mechanism in thev=spf1record pepsi-setup(1) suggests. No stage reads this option at run time — it is purely an input to the generated SPF record.PUBLIC_IPis a per-stage option set in a relay stage’s[stage-<name>]section, and pepsi-setup(1) collects it from every stage whosePROGRAMis pepsi-stage-relay-to-internet(1) or pepsi-stage-relay-to-smarthost(1) — detected byPROGRAM, never by the section name — and unions the results. A deployment may therefore have several relay stages, each carrying its ownPUBLIC_IP:On a pepsi-stage-relay-to-internet stage it is this host’s own public sending IP(s): the host delivers directly to recipients’ MX from them.
On a pepsi-stage-relay-to-smarthost stage it is the smarthost’s egress IP(s): mail leaves the internet from the smarthost, so SPF must authorise it (many providers instead publish an
include:you add to the record by hand).
If no relay stage sets
PUBLIC_IPthe generated record is a barev=spf1 -all— which tells receivers no host may send for your domains and makes your own outbound mail fail SPF — and pepsi-setup(1) logs a warning. A receive-only deployment (no relay stage) has noPUBLIC_IPand is not warned.Each address must be a globally-routable public IP — the address receivers see mail arrive from. pepsi-setup(1) warns (at validation and in the DNS output) if a
PUBLIC_IPis a non-public address (loopback, RFC 1918 / unique-local, link-local, or CGNAT): behind NAT, use the host’s public egress IP, not its LAN address, or outbound mail over that address family will fail SPF.Reverse DNS (PTR). Each pepsi-stage-relay-to-internet
PUBLIC_IP— this host’s own egress IP — must also have a forward-confirmed reverse DNS record that names the stage’sSERVER_NAME: aPTRfor the address whose host name in turn resolves (viaA/AAAA) back to the same address, and which is the very name the stage announces inEHLO. Many receiving mail servers treat a missing, non-forward-confirmed, or generic/dynamic-lookingPTRas a spam signal and reject or junk the mail, and a large family of them additionally compares theEHLOname against thePTRand refuses the session when the two name different hosts (HELO host does not match rDNS). Being inside yourACCEPTED_DOMAINSdoes not satisfy that test — a host answering to several names can announce one and reverse-resolve to another, both of them its own, and still be refused. pepsi-setup(1) warns for everyPUBLIC_IPwhose reverse DNS is not correct in this sense, including that mismatch, and prints the remediation (see pepsi-setup(1)). Reverse DNS for aPUBLIC_IPlives in the IP owner’s zone (in-addr.arpa/ip6.arpa) — usually your ISP or hosting provider — so unlike the forward records it is often not something you can publish yourself; where it cannot be changed, setSERVER_NAME(and[pepsi-ingress]HOSTNAME, the MX record and the TLS certificate) to the name thePTRalready gives. A pepsi-stage-relay-to-smarthostPUBLIC_IPnames the smarthost’s egress IPs, whose reverse DNS the smarthost operator controls, so those are not checked.The unioned record is published identically for every served domain. That is correct but broad; in special setups where different domains send from different hosts, a hand-written per-domain SPF record (listing only that domain’s own sending IPs) can be tighter and still correct. pepsi-setup(1) does not compute that grouping automatically and notes this in its DNS output.
85.2.1.1.23. The [pepsi-stage-relay-to-smarthost] MTA Sections¶
85.2.1.1.23.1. The Smarthost (MTA) Sections¶
Each [pepsi-stage-relay-to-smarthost-mta-<name>] section defines one upstream
smarthost; the suffix <name> is a free-form label used in logs. These
sections are shared by every smarthost-relay stage. A recipient domain is routed
to the MTA whose DOMAINS lists it, or to the single CATCH_ALL MTA.
- HOST
(string, required) Host name or address of the smarthost to connect to.
- PORT
(number, required) TCP port to connect on (e.g.
587or465).- MODE
(optional) Transport security:
starttls(default),tls(implicit TLS) orplain(cleartext).- TLS_VERIFY
(boolean, optional) Whether the smarthost’s TLS certificate is verified against the system trust store. Default
yes;noaccepts any certificate (insecure, for testing only).- TLS_CA
(path, optional) An extra CA certificate (PEM) to trust in addition to the system store, e.g. for a private smarthost CA.
- TLS_CLIENT_CERT / TLS_CLIENT_KEY
(path, optional; both-or-neither) A client certificate (PEM chain) and its private key to present to the smarthost during the TLS handshake (mutual TLS). Loaded fresh on every connection. Requires an encrypting
MODE(tlsorstarttls) and applies to PKIX,TLS_VERIFY = noand DANE alike. The key is secret: keep it readable only by thepepsi-tokengroup the relay stage runs SGID under (on Debian, place cert + key in the SGID/var/pepsi/tlsdirectory). SetAUTH = externalto also authenticate the SMTP session with the certificate via SASLEXTERNAL.- DANE
(optional) DANE/TLSA (RFC 7672) enforcement towards this smarthost:
off,warn(the default) orstrict. When the smarthost publishes DNSSEC-validatedTLSArecords at_<PORT>._tcp.<HOST>, the certificate is authenticated against them, taking precedence overTLS_VERIFY(PKIX). Inwarna mismatch is logged and delivery proceeds; instricta usable-but-mismatching record, or a failedTLSAlookup, defers the message. Ignored whenMODE = plain. Requires a DNSSEC-validating resolver reached over a trusted path (Pepsi trusts the response AD bit); see the stage’sDNS_SERVERS.- ADDRESS_FAMILY
(optional) Which IP address families may be used to reach this smarthost:
any,ipv4(also spelledv4/v4only) oripv6(v6/v6only).Precedence is: this entry, then the owning
[stage-*]section, thenany. The per-entry level is what makes the option useful: one misbehaving destination can be pinned without forcing every other destination — including one that publishes onlyAAAA— onto the same family.The motivating case is Microsoft Exchange Online, which rejects mail from a sending IPv6 address with no
PTRrecord (550 5.7.1 ... S820). A dual-stacked host prefers IPv6, so a machine with correct IPv4 reverse DNS and undelegated IPv6 reverse DNS delivers fine everywhere except there.With
any(the default) the host name is handed to the system resolver. Any other value makes Pepsi resolve the host itself and try each address of the chosen family in turn. A destination that publishes no address of the chosen family is a transient failure, not a bounce: that is an operator error worth retrying past, not one worth destroying queued mail over.- AUTH
(optional) SMTP authentication mechanism:
none(default),plain,login,cram-md5,digest-md5,scram-sha-1,scram-sha-256,scram-sha-1-plus,scram-sha-256-plus,auto,external,oauth,ntlmorgssapi.plain/login/cram-md5/digest-md5/thescram-*family/autoall requireUSERNAMEandPASSWORD;oauthrequiresUSERNAMEandTOKEN_FILE;external(SASLEXTERNAL, RFC 4422) requiresTLS_CLIENT_CERT/TLS_CLIENT_KEYand authenticates by that certificate (no password sent), with an optionalUSERNAMEas the SASL authorization identity.cram-md5(RFC 2195) andscram-sha-1/scram-sha-256(RFC 5802 / RFC 7677) are challenge/response: the password is never sent, and SCRAM additionally authenticates the server to Pepsi (a bad server signature is a permanent failure). The-plusSCRAM variants addtls-server-end-pointchannel binding (RFC 5929).autopicks the strongest mechanism the smarthost advertises (SCRAM-SHA-256-PLUS>SCRAM-SHA-256>SCRAM-SHA-1-PLUS>SCRAM-SHA-1>CRAM-MD5>LOGIN>PLAIN); it never selects the deprecateddigest-md5,ntlmorgssapi.digest-md5(RFC 2831) is also a challenge/response with server authentication (rspauth) but is deprecated (RFC 6331 moved it to Historic) and provided only for legacy smarthosts; selecting it logs a warning from pepsi-setup(1) and at runtime.ntlmis the de-facto Microsoft mechanism (no RFC; NTLMv2 only, withtls-server-end-pointEPA channel binding) requiringUSERNAME/PASSWORD/NT_DOMAIN; it is weak and does not authenticate the server, so pepsi-setup(1) warns.gssapiis SASLGSSAPI/Kerberos (RFC 4752); it takes no password, obtaining a service ticket forSERVICE_NAMEfrom a credential cache (KRB5CCNAME). All are offered only over an encrypted transport (MODE=tls/starttls);ntlmandgssapirejectMODE = plainat configuration time.- USERNAME / PASSWORD
(string, required when AUTH is plain/login/cram-md5/digest-md5/scram-*/auto/ntlm) The SMTP-AUTH credentials. Keep
PASSWORDin a mode-restricted file merged with@inline-secret@. For thescram-*mechanisms both are normalised with SASLprep (RFC 4013).USERNAMEis also the optional authorization identity forAUTH = externalandAUTH = gssapi.- NT_DOMAIN / NT_WORKSTATION
(string; NT_DOMAIN required when AUTH = ntlm) The Windows domain of the NTLM account, and an optional cosmetic client workstation name.
- SERVICE_NAME
(string, optional; AUTH = gssapi) The Kerberos host-based service principal of the smarthost, in
service@hostform. Defaults tosmtp@<HOST>.- KRB5CCNAME
(string, optional; AUTH = gssapi) The Kerberos credential cache to read for this smarthost (e.g.
FILE:/var/pepsi/krb5/smarthost.cc), overriding the ambientKRB5CCNAME. Pepsi only reads the cache; an external keytab +k5start/cronjob must keep the ticket fresh, and the cache must be readable by the relay worker (the SGIDpepsi-tokengroup; see/var/pepsi/krb5). A missing/expired ticket defers the message (transient failure) rather than bouncing it.- TOKEN_FILE
(path, required when AUTH = oauth) File holding the current OAuth 2.0 access token (whitespace-trimmed), re-read on every delivery. The SASL mechanism is chosen from the smarthost’s
EHLO(OAUTHBEARERpreferred, elseXOAUTH2). The relay stage only reads this file; keep it current with pepsi-helper-token-refresh(1) (see the[pepsi-helper-token-refresh-*]sections) or any equivalent external job. A missing/expired/rejected token defers the message (transient failure) rather than bouncing it.- HELO_NAME
(string, optional) Name announced in
EHLOto this smarthost. Defaults to the stage’sSERVER_NAME.- DOMAINS
(string) Whitespace/comma-separated recipient domains routed to this MTA (case-insensitive). Required unless
CATCH_ALL = yes; a domain may be routed by at most one MTA.- CATCH_ALL
(boolean, optional) Whether this MTA receives every domain not matched by a
DOMAINSentry. Defaultno; at most one MTA may set it. An MTA must listDOMAINSor setCATCH_ALL = yes.
85.2.1.1.24. The [pepsi-helper-token-refresh] Sections¶
These configure pepsi-helper-token-refresh(1), the optional service that
keeps the OAuth access tokens of AUTH = oauth smarthosts current. The set of
tokens to maintain is derived from the smarthost MTA sections: for each
[pepsi-stage-relay-to-smarthost-mta-<name>] with AUTH = oauth the service
looks for a matching [pepsi-helper-token-refresh-<name>] section. The service
is not part of pepsi.target; enable it explicitly.
Because the per-target sections hold OAuth client secrets, keep them in a file
readable only by the pepsi-helper-token-refresh user and include each with the
@inline-secret@ directive (which a reader that cannot open the file — the
relay stage, run as pepsi — silently skips, so the same pepsi.conf parses
for both). See pepsi-helper-token-refresh(1) and the Extending the
Pipeline manual chapter.
The optional base section [pepsi-helper-token-refresh] holds service-wide,
non-secret options:
- STATE_DIR
(path, optional) Private directory (mode
0700) for per-target rotated refresh tokens. Default/var/pepsi/token-refresh.
Each [pepsi-helper-token-refresh-<name>] section (<name> matching an OAuth
smarthost MTA) holds:
- TOKEN_ENDPOINT
(string, required) The provider’s OAuth 2.0 token endpoint URL (HTTPS). It is also how the service tells a missing secret section from an unreadable one: an
@inline-secret@file this account cannot open simply is not there, so an absentTOKEN_ENDPOINTskips that target with a warning instead of failing the service. The remaining options below are then errors, not skips.- GRANT
(optional)
refresh_token(the default; e.g. Google) orclient_credentials(e.g. Microsoft 365 app-only).- CLIENT_ID / CLIENT_SECRET
(string, required) The OAuth client credentials, sent in the request body.
- REFRESH_TOKEN
(string, required for GRANT = refresh_token) The long-lived refresh token. A rotated value returned by the endpoint is persisted under
STATE_DIRand then takes precedence.- SCOPE
(string, required for GRANT = client_credentials; optional otherwise) The requested scope.
- REFRESH_MARGIN
(duration, optional) How long before a token’s stated expiry to refresh it. Default
5 m. Usesh/m/sunits only.
85.2.1.1.25. The [pepsi-payments] Section¶
This optional section configures the GNU Taler payment integration. When it is
absent (or MERCHANT_BACKEND_URL is unset) pepsi-setup(1) performs no
merchant checks and no webhook is provisioned. The same section is read at
run time by pepsi-stage-anti-spam(1) to create per-message payment orders.
- MERCHANT_BACKEND_URL = URL
Base URL of the Taler merchant backend, optionally including the instance (
…/instances/$ID). pepsi-setup(1) requestsGET /configat the backend root (any trailing/instances/$IDis stripped) to confirm it is ataler-merchantbackend, andGET /private/orders?limit=1at this URL to confirm the access token. Required to enable the integration.It must be an
https://URL, or anhttp://one whose host is the loopback interface (localhost,127.0.0.0/8,::1).MERCHANT_ACCESS_TOKENis attached to every request as a bearer credential, so a cleartext backend anywhere else — including “it is only on the LAN” — hands that long-lived credential to whatever sits on the path. Requests are bounded (10 s to connect, 30 s in total), so an unresponsive backend fails the message rather than the stage.- MERCHANT_ACCESS_TOKEN = TOKEN
Access token for the merchant backend, in the Taler
secret-token:…form, sent as anAuthorization: Bearercredential. It needs theorders-readandwebhooks-read/webhooks-writepermissions. Required whenMERCHANT_BACKEND_URLis set.
The provisioned pepsi-resume webhook fires on the pay event and
POSTs {"message_id":"{{order_id}}"} to
https://<host>/resume (the shortest [pepsi-httpd-cert-*] SNI host),
authenticated with [pepsi-httpd] RESUME_AUTHORIZATION_TOKEN. The merchant
order_id must therefore equal the message’s external token.
85.2.1.1.26. The [pepsi-vacation-default-message] Section¶
The per-language vacation notices pepsi-stage-vacation(1) falls back to, one option per language:
[pepsi-vacation-default-message]
EN = Hello {{SENDER_NAME}}, I am away until {{VACATION_END}}.
DE = Hallo {{SENDER_NAME}}, ich bin bis {{VACATION_END}} abwesend.
The option name is the language tag (EN, DE, PT_BR — either separator),
and the value is a Mustache template. Which
language is used, which fields expand, and why two spaces become a newline are
all documented in pepsi-stage-vacation(1).
This section normally already exists. make install ships it as
${DATADIR}/config.d/vacation.conf, and taler’s configuration loader parses every
file in ${DATADIR}/config.d before pepsi.conf. So a stage configured with
no message of its own still has ten languages available, and an operator who wants
different wording redefines just the language they care about in pepsi.conf:
later definitions win option by option, never section by section, so
overriding EN leaves the other nine in place. Editing the shipped file works too,
but a package upgrade overwrites it.
The section name is only a default (DEFAULT_MESSAGE_SECTION), so an operator who
wants several message sets — a formal one for a support address, a terse one for
everyone else — can define more sections and point different stages, domains or
addresses at them.
An account owner never touches this section: their own text goes in
MESSAGE_<LANG> in the stage’s own section, which the settings layer can carry
and which wins per language.
85.2.1.1.27. The [pepsi-autoconfig] Section¶
This optional section publishes mail account autoconfiguration
(draft-ietf-mailmaint-autoconfig): the XML document a mail client fetches,
knowing only the user’s address, so it can configure itself instead of asking
for eight settings the user usually cannot supply. It is served by
pepsi-httpd(1) at /mail/config-v1.1.xml on autoconfig.<domain>, and
at /.well-known/autoconfig/mail/config-v1.1.xml on the mail domain itself.
The feature is off until an incoming server is named. Omit the section — or
set neither IMAP_HOST nor POP3_HOST — and both endpoints answer 404.
A client that finds a configuration document stops walking the fallback chain,
so publishing one that cannot tell it how to read mail leaves the user worse
off than publishing nothing at all.
Pepsi does not serve mailboxes (see pepsi-stage-relay-to-lmtp(1)), so the incoming half must name whatever MDA the deployment pairs with, typically Dovecot on the same host.
Two things outside this file are needed as well. An autoconfig.<domain>
DNS record must resolve to this server, and the TLS certificate must cover that
name — clients try the https://autoconfig.… URL first, and a certificate
error there is a failed lookup. The certificate half is handled for you when
pepsi-setup(1) provisions certificates: with this section naming an
incoming server it adds autoconfig.<domain>, for every served domain, to the
names it requests for the TLS [pepsi-httpd-listener-*] certificate. The DNS
record is not, since it is published in your zone. A deployment unwilling to add
the name can rely on the /.well-known/ form alone, at the cost of being found
only by clients that try it.
- IMAP_HOST, POP3_HOST
(hostname, optional) The mailbox server to advertise. Setting either enables the feature; setting neither disables it. No default — an invented hostname would be worse than silence.
- IMAP_PORT, POP3_PORT
(number, optional) Defaults
993and995, the implicit-TLS ports.- IMAP_SOCKET, POP3_SOCKET
(``SSL`` | ``STARTTLS`` | ``plain``, optional) Transport security, in the draft’s own vocabulary (
SSLmeans implicit TLS);tlsandnoneare accepted as spellings ofSSLandplain. DefaultSSL.- IMAP_AUTH, POP3_AUTH
(optional) One of the draft’s authentication values:
password-cleartext,password-encrypted,NTLM,GSSAPI,TLS-client-cert,OAuth,client-IP-address,none. Defaultpassword-cleartext— which, overSSL, is the ordinary password-over-TLS arrangement rather than anything sent in the clear. An unrecognised value is a start-up error, not a silently published typo.- SMTP_HOST, SMTP_PORT, SMTP_SOCKET, SMTP_AUTH
(optional) The submission server to advertise. Normally omit all four: they are derived from the
[pepsi-ingress-listener-*]section flaggedSUBMISSION = yes— its port, itsMODEand its authentication mechanism — so moving submission from STARTTLS on 587 to implicit TLS on 465 updates the published document by itself, and the two cannot drift apart. Set them only when the public submission endpoint is not the listener, for instance behind a load balancer. SettingSMTP_HOSTswitches off the derivation entirely, so give the others too if they differ from587/STARTTLS/password-cleartext.A UNIX-socket or systemd-activated submission listener is never advertised: a remote client cannot use the first, and the second has no port in the configuration to publish.
- DISPLAY_NAME, DISPLAY_SHORT_NAME
(text, optional) What the client shows the user. Both default to the first
[pepsi-ingress] ACCEPTED_DOMAINSentry (the ingressHOSTNAMEwhen no domain is configured).- DOCUMENTATION_URL
(URL, optional) A help page for the account settings, published as the draft’s
<documentation>element. Omitted when unset.
[pepsi-autoconfig]
IMAP_HOST = mail.example.org
POP3_HOST = mail.example.org
DISPLAY_NAME = Example Mail
DISPLAY_SHORT_NAME = Example
# SMTP_* deliberately omitted: derived from the submission listener.
85.2.1.1.28. The [pepsi-tlsrpt] Section¶
This optional section configures SMTP TLS Reporting (RFC 8460). It has two independent halves; omit the section to disable both. pepsi-setup(1) reads it for the advertising half; the sender half is read at run time by the relay stages (session recording) and pepsi-tlsrpt(1) (the daily report job).
- RUA = URI [, URI …]
Reporting address(es) advertised in the
_smtp._tls.<domain>TXT record pepsi-setup(1) emits for each domain, so other senders report their TLS results toward our domains to us. Amailto:orhttps:URI, or a comma-separated list, stored verbatim. When unset, no record is published.- SEND_REPORTS =
yes|no Whether the relay stages record each outbound TLS session in
pepsi.tls_session. Optional; defaults tono. It is the switch for the recording half only: pepsi-tlsrpt(1) does not test it, it simply finds no sessions to report when nothing records them.- REPORT_FROM = ADDRESS
Envelope sender and
From:of the report e-mails pepsi-tlsrpt(1) sends. Required to send reports (its domain is the report submitter). Reports are not null-sender (RFC 8460 §5.3: they must be DKIM/DMARC-alignable); the report-about-a-report loop is prevented by the injectedstate.tlsrptflag.- REPORT_STAGE = STAGE
Pipeline stage report e-mails are injected at (so they are signed and relayed), like the pepsi-stage-anti-spam(1)
BLOCK_RESPONSE_STAGE. Must reference an existing[stage-*]. Required to e-mail (mailto:) reports.- ORGANIZATION = NAME
organization-nameplaced in emitted reports. Optional; defaults to theREPORT_FROMdomain.- CONTACT = ADDRESS
contact-infoplaced in emitted reports. Optional.- RETAIN_DAYS = DAYS
How many days pepsi-tlsrpt prune keeps
pepsi.tls_sessioncounters. Optional; defaults to7.pepsi-tlsrpt-prune.timerruns the prune daily.
85.2.1.1.29. The [pepsi-telemetry-client] Section¶
This optional section configures the local feature-telemetry aggregator,
pepsi-telemetry-client(1). All options have working defaults; the whole
feature is gated by [pepsi] SHARE_TELEMETRY above, which is off unless the
operator turned it on — so nothing here has any effect until it is. (The central
collector, pepsi-telemetry(1), reads the separate [pepsi-telemetry]
section instead.)
- UNIXPATH = PATH
Local UNIX socket the daemon listens on for feature events; the same path the producer programs connect to. Optional; defaults to
/run/pepsi-telemetry/socket.- UNIXPATH_GROUP = GROUP
Group the socket is
chgrp-ed to (modeUNIXPATH_MODE), so thepepsi(stage workers) andpepsi-ingressusers can connect. Optional; defaults topepsi-telemetry.- UNIXPATH_MODE = OCTAL
Permission bits of the socket. Optional; defaults to
660.- SUBMIT_INTERVAL = DURATION
Interval between submissions to the collector (
h/m/sunits only — see the duration note above). Optional; defaults to60 s. Zero is a configuration error, not a busy loop.- MAX_FEATURES = N
Cap on the number of distinct feature names held in memory, bounding memory against a misbehaving local producer. Optional; defaults to
4096.
85.2.1.1.30. Stored Data¶
For every accepted message, pepsi-ingress(1) inserts one row into the
pepsi.workqueue table containing a local identifier (workqueue_id), a random
token, the reception timestamp (received_at), the envelope sender
(mail_from; the empty string for the null sender <>), the envelope
recipient list (rcpt_to), and the parsed from_header and subject
headers. The message itself is stored split in two — a headers column
(the RFC 5322 header block up to but not including the blank-line separator,
with the RFC 5321 §4.4 trace Received: header and, above it, the
Authentication-Results header prepended; the ARC set is added later by
pepsi-stage-arc(1)) and a body (everything after the blank line; empty for
a header-only message). The reconstruction invariant is
raw = headers || CRLF || body. Splitting the message lets stages that only
touch the headers or envelope load (and rewrite) just the headers column and
never pull the (potentially large) body.
The body is not a column of pepsi.workqueue but a row of its own table,
pepsi.workqueue_body (body_id, body, and the generated size
octets), which the message row references by body_id. That is what
keeps a multi-recipient message from being stored once per recipient: every
row split from it — per-address settings, a relay stage’s one row per
recipient, a mailing-list fan-out, a per-recipient bounce or DSN — references
the same body row, so a 25 MiB post to a thousand members is one 25 MiB
body and a thousand small rows. A stage that rewrites a body (decryption, a
list’s footer) stores a new body row and repoints only its own message
(copy-on-write), so no rewrite can reach a sibling. A body is deleted by a
trigger as soon as the last row referencing it is deleted or repointed, and
pepsi-dispatch(1) sweeps the rare orphan a race leaves behind
(pepsi.workqueue_body_gc()). header_octets is the matching generated size
of headers; the two size columns are what the queue admission check
(MAX_QUEUE_BYTES, pepsi.queue_usage()) sums, readable by roles that may
not read the message itself. The
computed SPF, DKIM and DMARC verdicts are stored in the row’s state under an
auth key, and the authserv_id under state.origin; the arc verdict
starts as none and is filled in by the ARC stage. The dispatcher is not
notified from that transaction: pepsi-ingress(1) coalesces admissions and
issues at most one wake per DISPATCH_WAKE_INTERVAL, out of band and after the
rows commit, with an empty payload (see that option’s description above).
A LISTEN workqueue consumer therefore gets no per-message notification and no
payload to parse.
Each row additionally carries its processing state for the stage pipeline that
runs after ingress: a stage (the [stage-<stage>] section of the program
currently responsible for advancing the message), a status (one of
pending, running, paused, failed or timeout), a state
(the JSON object carried with the message, documented in pepsi.state(7)),
and a timeout (when a paused
message should be re-queued). Every new message is inserted explicitly at the
initial stage — stage = init, status = pending — so it enters the
pipeline at [stage-init]. These columns are inspected and repaired with
pepsi-queue(1).
pepsi.workqueue (with its bodies in pepsi.workqueue_body) is the only queue
of messages in flight; the schema has some sixty tables in all. The one every deployment exercises beside the queue is
pepsi.dns_address: a cache of resolved A/AAAA addresses for recipient
mail-exchanger hosts, with per-address connect health, maintained by
pepsi-stage-relay-to-internet(1). Each row records a host, one resolved
address, the DNS-TTL expires_at, a failed flag and the
last_success_at time, so a working address is preferred until its TTL elapses
and a host is re-resolved once all its cached addresses have failed or expired.
It carries no message data and is not managed by pepsi-queue(1).
The rest are the feature tables, each described with the component that owns it:
the pipeline counters stage_stats and dispatch_stats (pepsi-dispatch(1), exported by pepsi-httpd(1) at /metrics), whitelist
(pepsi-whitelist(1)), tls_session (pepsi-tlsrpt(1)), settings
(pepsi-settings(1)), vacation_reply (pepsi-stage-vacation(1)),
payment_request_reply (pepsi-stage-anti-spam(1)),
secretary_challenge and secretary_sent (pepsi-stage-secretary(1)),
mailbox_quota (pepsi-quota(1)), config_override (the database
configuration layer, pepsi-config(1)), origin_nonce and
auto_pay_spend (pepsi-stage-auto-pay(1)), telemetry (pepsi-telemetry(1)),
secure_message and secure_access (pepsi-secure-link(1)), the key
store crypto_identity / peer_key / peer_has_own_key / ca_trust /
key_request (pepsi-keys(1) and pepsi-keydisc(1)), the
mailing-list tables mailing_list and list_* (pepsi-list(1)), the
list archive archive_* (pepsi-archive(1)), the logs event_log and
mail_log, and the administrative surface admin_account /
admin_session / api_token / setup_task / setup_task_log
(pepsi-httpd(1) and pepsi-setup(1)). All of them are created by
pepsi-setup(1). Apart from secure_message (ciphertext awaiting its
recipient), the list archive, whose purpose is to keep posts, and the messages a
list holds for moderation, none holds message content.
The state column is the JSON object carried with the message from ingress
to its terminal stage; it is the channel through which stages communicate.
Ingress seeds it with the connection’s SMTP-origin provenance, the inbound
SPF/DKIM/DMARC verdicts and any RFC 3461 DSN parameters; later stages merge in
their own verdicts and scratch. Its full layout — every key, who writes it and
who consumes it — is documented in pepsi.state(7).
85.2.1.1.31. Example¶
A complete forwarder: receive on the three canonical SMTP ports, ARC-seal on arrival, SRS-rewrite, deliver direct-to-MX, and bounce failures (signing the bounce before delivery):
[pepsi]
KEY_DIR = /var/pepsi/keys
DKIM_SELECTOR = pepsi
ARC_DOMAIN = example.org
ARC_ALGORITHM = rsa
[pepsi-postgres]
CONFIG = postgres:///pepsi
# SQL_DIR defaults to ${DATADIR}/sql; set it when running from a checkout.
[pepsi-ingress]
HOSTNAME = mail.example.org
ACCEPTED_DOMAINS = example.org example.com
MAX_MESSAGE_SIZE = 26214400
# The three canonical SMTP listeners. TLS_CERT/TLS_KEY are omitted so
# pepsi-setup auto-fills the certbot path and obtains the certificate.
# Port 25: cleartext with opportunistic STARTTLS.
[pepsi-ingress-listener-mx]
SERVE = tcp
BIND_TO = 0.0.0.0
PORT = 25
MODE = starttls
MYNETWORKS = 127.0.0.0/8 ::1/128 10.0.0.0/8
# Port 465: implicit TLS (SMTPS), authenticated submission via Dovecot SASL.
[pepsi-ingress-listener-submissions]
SERVE = tcp
BIND_TO = 0.0.0.0
PORT = 465
MODE = tls
SUBMISSION = yes
SASL_TYPE = dovecot
SASL_PATH = /run/dovecot/auth-client-pepsi
# Port 587: cleartext submission upgraded with STARTTLS.
[pepsi-ingress-listener-submission]
SERVE = tcp
BIND_TO = 0.0.0.0
PORT = 587
MODE = starttls
SUBMISSION = yes
SASL_TYPE = dovecot
SASL_PATH = /run/dovecot/auth-client-pepsi
[pepsi-srs]
SRS_DOMAIN = srs.example.org
SECRET_FILE = /etc/pepsi/srs.secret
# init: ARC-seal the message as received, then hand to SRS.
[stage-init]
PROGRAM = pepsi-stage-arc
NEXT_STAGE = srs
[stage-srs]
PROGRAM = pepsi-stage-srs
NEXT_STAGE = deliver
# deliver: direct-to-MX; failures go to the bounce stage.
[stage-deliver]
PROGRAM = pepsi-stage-relay-to-internet
SERVER_NAME = mail.example.org
POSTMASTER = postmaster@example.org
BOUNCE_STAGE = bounce
# This host's public sending IP(s); pepsi-setup unions PUBLIC_IP from every
# relay stage into the SPF record.
PUBLIC_IP = 203.0.113.7 2001:db8::25
# bounce: build an unsigned DSN, sign it, then deliver it.
[stage-bounce]
PROGRAM = pepsi-stage-bounce
SERVER_NAME = mail.example.org
POSTMASTER = postmaster@example.org
NEXT_STAGE = bounce-sign
BOUNCE_MESSAGE = default
[stage-bounce-sign]
PROGRAM = pepsi-stage-dkim-sign
NEXT_STAGE = deliver
To relay via a smarthost instead, replace the deliver stage’s PROGRAM
with pepsi-stage-relay-to-smarthost and add
[pepsi-stage-relay-to-smarthost-mta-*] sections, e.g.:
[pepsi-stage-relay-to-smarthost-mta-smarthost]
HOST = smtp.relay.example.net
PORT = 587
CATCH_ALL = yes
85.2.1.1.32. Files¶
Every Pepsi component reads the same file. When a component is started without –config, the first existing path from the following list is used:
$XDG_CONFIG_HOME/pepsi.conf$HOME/.config/pepsi.conf/etc/pepsi/pepsi.conf/etc/pepsi.conf
pepsi-setup --wizard writes /etc/pepsi/pepsi.conf; make install
places a reference pepsi.conf.sample beside it, which documents every option
but is not itself a working configuration.
Before that file is read, every *.conf in ${DATADIR}/config.d is parsed, so
packaged defaults are in place and pepsi.conf overrides them option by
option (a section defined in both is merged, not replaced). Two are shipped:
vacation.conf— the per-language vacation notices; see The [pepsi-vacation-default-message] Section above.thunderbird.conf—[pepsi] CRYPTO_ALLOW_DOWNGRADE = yes, so S/MIME goes out as AES-256-CBCEnvelopedData, which Thunderbird can read and our compiled-in AES-256-GCM default cannot be read as. The file explains itself at length and is meant to be deletable: removing it restores authenticated encryption with no other edit.
These files belong to the
package: redefine the options you want in pepsi.conf rather than editing them,
or an upgrade will discard the change.
85.2.1.1.33. See Also¶
pepsi-ingress(1), pepsi-dispatch(1), pepsi-httpd(1), pepsi-setup(1), pepsi-config(1), pepsi-queue(1), pepsi-status(1), pepsi-sendmail(1), pepsi-whitelist(1), pepsi-settings(1), pepsi-tlsrpt(1), pepsi-quota(1), pepsi-failure-bouncer(1), pepsi-list(1), pepsi-keys(1), pepsi-keydisc(1), pepsi-secure-link(1), pepsi-detect-language(1), pepsi-telemetry(1), pepsi-telemetry-client(1), pepsi-helper-token-refresh(1), pepsi-helper-auto-pay(1), pepsi-helper-mailbox-scan(1), pepsi-stage-arc(1), pepsi-stage-srs(1), pepsi-stage-encrypt(1), pepsi-stage-decrypt(1), pepsi-stage-autocrypt-learn(1), pepsi-stage-secure-link(1), pepsi-stage-vks-confirm(1), pepsi-stage-route(1), pepsi-stage-dkim-sign(1), pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-lmtp(1), pepsi-stage-relay-to-maildir(1), pepsi-stage-dot-forward(1), pepsi-stage-bounce(1), pepsi-stage-discard(1), pepsi-stage-anti-spam(1), pepsi-stage-auto-pay(1), pepsi-stage-check-whitelist(1), pepsi-stage-auto-whitelist(1), pepsi-stage-detect-language(1), pepsi-stage-block-language(1), pepsi-stage-if(1), pepsi-stage-milter(1), pepsi-stage-aliases(1), pepsi-stage-vacation(1), pepsi-stage-edit-settings(1), pepsi-archive(1), pepsi-stage-list(1), pepsi-stage-list-post(1), pepsi-stage-list-deliver(1), pepsi-stage-list-command(1), pepsi-stage-list-bounce(1), pepsi.state(7), systemd.socket(5)
85.2.1.1.34. Bugs¶
Report bugs to the Pepsi issue tracker.