85.1.42. pepsi-sendmail¶
submit mail locally through the Sendmail interface
- Manual section:
1
85.1.42.1.1. Name¶
pepsi-sendmail - hand a locally-written message to Pepsi, providing the
traditional /usr/sbin/sendmail interface.
85.1.42.1.2. Synopsis¶
sendmail [-t] [-i] [-f SENDER] [-F NAME] [-v] [-N NOTIFY] [-R RET] [-V ENVID] [-c CONFIG] [RECIPIENT …]
mailq
newaliases
85.1.42.1.3. Description¶
pepsi-sendmail reads a message on standard input and submits it to
pepsi-ingress over a local UNIX-domain socket. It exists so that the software
which expects an MTA to provide /usr/sbin/sendmail — cron, mail(1),
git send-email, logwatch, PHP’s mail() — can send mail through Pepsi
without being reconfigured.
The Debian package installs it under the paths Debian Policy §11.6 requires of a
mail transport agent, each a symlink to the unified pepsi binary — except
/usr/lib/sendmail, which points at /usr/sbin/sendmail — which dispatches
on the name it was invoked under:
Path |
Behaves as |
|---|---|
|
the submission client described here |
|
the same (historical path) |
|
|
|
|
|
the submission client (UUCP delivery) |
A source install does not claim these paths, because they belong to whatever
MTA the system already has; pass INSTALL_MTA_LINKS=yes to make install to
take them over deliberately.
85.1.42.1.4. Authentication¶
pepsi-sendmail holds no privilege and asserts no identity. It is not setuid
or setgid, and it needs no entry in MYNETWORKS.
Instead, pepsi-ingress reads the connecting process’s user id from the kernel
(SO_PEERCRED, enabled per listener with AUTH_PEERCRED), resolves it to a
login through the passwd database, and looks that login up in USERNAME_MAP
exactly as it would a SASL username arriving on the submission port. The RFC 6409
§6/§8.1 rules then apply unchanged: a local program may only use an envelope
sender and From: its own account is mapped to, and an account mapped to no
address is refused outright.
Two consequences follow:
The socket may be world-writable. Being able to open it grants nothing, because it does not determine who you can send as. Deny an account by mapping it to no address in
USERNAME_MAP, not by adjusting permissions.Nothing on the network gains any rights. This is the substantive difference from trusting the loopback interface in
MYNETWORKS, which authenticates a network position rather than a user: under that arrangement any local process can send as anyone, and a message this host relays to itself re-enters as a fresh local submission, which can loop.
Because submissions are authenticated, they are recorded with
state.local_origin set, so the pipeline treats them as this host’s own mail —
DKIM-signing them as the author’s domain and allowing them to relay.
85.1.42.1.5. The message¶
Local producers write messages the way a text file is written, so the client tidies them up before anything else looks at them:
Every bare LF becomes CRLF. SMTP is CRLF-framed and
pepsi-ingressenforces it (a bare LF inDATAis refused with500 5.6.0 invalid line framing), so this cannot be left to the server.A
From:field is prepended when the message has none, built from -F and the envelope sender; a display name containing RFC 5322 specials is quoted.Every
Bcc:field is removed, whether or not -t was used — leaving it in would disclose the blind recipients to everyone else on the envelope.A recipient with no
@—cron’sTo: root— is completed with[pepsi-sendmail] DEFAULT_DOMAIN, before duplicates are dropped (case-insensitively), so the same address given twice is submitted once.
The transaction itself is the minimum an on-host socket needs: read the greeting,
EHLO, MAIL FROM (with BODY=8BITMIME when the message holds 8-bit bytes
and the server advertised 8BITMIME, and with the -R/-V parameters
only when it advertised DSN), one RCPT TO per recipient (carrying -N
under the same condition), DATA with RFC 5321 §4.5.2 dot-stuffing applied, and
a best-effort QUIT — a failure to say goodbye cannot turn an accepted message
into an error. No STARTTLS, no SASL and no MTA-STS: the peer is a filesystem
socket on this host, the transport cannot be observed, and authentication is the
kernel’s business.
85.1.42.1.6. Options¶
- -t
Read the recipients from the message’s
To:,Cc:andBcc:headers. They are added to any recipients given on the command line (matching Postfix), and an address already on the command line is not repeated.Bcc:headers are removed from the message either way, with or without -t.- -f SENDER, -r SENDER
Set the envelope sender. Subject to
USERNAME_MAP: the server rejects a sender the invoking account is not entitled to. When omitted, the invoking account’s login qualified with[pepsi-sendmail] DEFAULT_DOMAINis used (and the literalnobody@that domain if the login cannot be resolved). It is a courtesy default, not a claim: the server re-derives the identity from the peer credentials either way.- -F NAME
Display name for a
From:header generated when the message has none.- -i, -oi
Do not treat a line containing only a dot as end of input. Accepted for compatibility; input is always read to end-of-file.
- -v
Print the SMTP dialogue on standard error, and restore the ordinary Pepsi log output. Without it the program logs only warnings and errors: a
sendmailbinary runs fromcronand from scripts that capture standard error and mail it back, so the usual informational chatter would turn every successful message into a second message.- -c CONFIG, –config CONFIG
Read the Pepsi configuration from CONFIG instead of the usual location (see pepsi.conf(5)). A Pepsi extension, not part of the
sendmailgrammar; the value may be attached (-c/etc/pepsi/pepsi.conf) or separate.Safe to honour despite this being the local submission interface, because the program holds no privilege of any kind: pointing it at another configuration changes only which socket it connects to, and the server still derives the sender’s identity from the caller’s uid via
SO_PEERCRED. It is spelled-crather than sendmail’s-Cbecause-Calready means an alternatesendmail.cf, which is a different thing and is refused (see below).- -N NOTIFY, -R RET, -V ENVID
RFC 3461 delivery status notification parameters, passed to the server when it advertises the
DSNextension. See pepsi-stage-bounce(1).NOTIFY is
NEVERor a comma-separated list ofSUCCESS,FAILUREandDELAY(RFC 3461 §4.1); RET isFULLorHDRS(§4.3). Anything else is a usage error rather than a501from the server. ENVID isxtext-encoded on the wire (§4.4) and truncated to the 100 characters the grammar allows.
A value given to -f, -F, -N, -R or -V, or a recipient, that
contains a control character is refused with EX_USAGE. Those values are
interpolated into SMTP command lines and into a generated From: field, and
SMTP commands are CRLF-delimited (RFC 5321 §4.1.1), so a carriage return or line
feed in one of them would inject an extra command — a recipient the caller never
declared — or an extra header field. This matters most for a caller that passes
user input straight through, such as a web application using -f.
- -bm
Read and submit a message. The default.
- -bp
Print the queue. Pepsi’s queue lives in the database rather than in a spool directory, and reading it needs a role an ordinary user does not hold, so this prints a pointer on standard error and exits
70; use pepsi-queue(1) for the queue itself and pepsi-status(1) for a summary. Note for monitoring:mailqtherefore always “fails” on a healthy Pepsi installation.- -bi
Rebuild the alias database. Pepsi’s alias map is a plain text file that pepsi-stage-aliases(1) re-reads whenever its modification time changes, so there is nothing to rebuild and this exits successfully without doing anything — before the configuration file is even looked for, since package maintainer scripts run
newaliasesunconditionally and it must not fail on a host whose configuration is unreadable or not yet written.- -I
Synonym for -bi: a successful no-op, as newaliases is.
- -h N, -L TAG, -X FILE, -B TYPE, -p PROTOCOL, -O NAME=VALUE
Accepted and ignored, as Postfix’s
sendmail(1) accepts them. They describe hop counts, logging and delivery-scheduling decisions the pipeline makes for itself; each consumes its value, attached or separate, so the value is not mistaken for a recipient. Only queue-management and daemon flags are refused, because those would misrepresent what the program does.- -m, -U, -n, -s, -G, -Ac, -Am, -oX, -d…
Accepted and ignored, taking no value of their own.
-oX is the old-style option spellingcronstill emits (-odi,-oem), and-dis sendmail’s debug flag, whose level is attached to it.- -bs, -bv
Accepted by the parser but not supported: each prints why on standard error and exits
70.-bs(speak SMTP on standard input) has no purpose here — connect to the submission socket directly.-bv(address verification) cannot be answered without the routing tables, and inventing an answer would be worse than declining.- -bd, -bD, -q…
Refused at parse time. Pepsi’s listening server is pepsi-ingress(1) and its queue is drained continuously by pepsi-dispatch(1); accepting these silently would suggest a daemon or a queue run that does not exist.
-C is refused rather than treated as -c: in the sendmail grammar it
names an alternate sendmail.cf, which has no counterpart here, and quietly
reading a Pepsi configuration from a path meant for something else would be
worse than declining. The Pepsi spelling is -c/--config, documented
above; with neither, the configuration file is located the usual way (see
pepsi.conf(5)).
85.1.42.1.7. Configuration¶
[pepsi-sendmail]
- SOCKET
Path of the
pepsi-ingressUNIX-domain submission socket. Default/run/pepsi/submission.sock. pepsi-ingress reads this same option to decide what to serve, so the two ends cannot be pointed at different paths and no listener section is needed.The special value
noneswitches local submission off entirely: the server binds no socket and this program refuses to run.- EHLO_NAME
Name announced in
EHLO. Defaults to[pepsi-ingress] HOSTNAME, orlocalhostif that is unset. Cosmetic: the server authenticates the peer by its credentials, not by this name.- DEFAULT_DOMAIN
Domain appended to a bare, unqualified address — the
rootthat cron and system tools hand tosendmailwithout an@. Defaults to[pepsi-ingress] HOSTNAME, orlocalhostif that is unset.It is also the domain of the default envelope sender when -f is omitted (
<login>@<DEFAULT_DOMAIN>), which is the fallback the server’s own submission-identity map applies to a username it has no entry for — so with noUSERNAME_MAPthe two agree by construction. That is still only how the message is addressed: who it may be from comes from the peer’s uid viaSO_PEERCREDand theUSERNAME_MAP, and setting this option cannot widen it.
The matching server side needs no configuration at all. pepsi-ingress serves
this socket unconditionally, reading the very same SOCKET option to decide
what to bind — so the client and the server cannot be pointed at different
paths, and there is no listener section to write, to keep in step, or to forget.
The listener it synthesises is fixed, because none of it is a choice:
MODE = plain, SUBMISSION = yes (so the RFC 6409 header fixups apply) and
AUTH_PEERCRED = yes, on a socket with mode 0666.
An optional [pepsi-ingress-listener-*] section would instead make the
facility depend on three separate things agreeing — the section existing, its
UNIXPATH matching SOCKET, and the runtime directory being writable by the
server — each individually easy to get wrong, with the same symptom every time:
cron mail silently stops. A host running an MTA is expected to provide
/usr/sbin/sendmail.
Under systemd the descriptor is inherited rather than bound, when the shipped
pepsi-ingress.socket already carries a ListenStream= for the same path.
That is not about privilege — this is not a privileged port — but about the
directory. /run/pepsi is shared with pepsi-httpd and the setup-apply
doorbell, and systemd gives a RuntimeDirectory= to the declaring unit’s own
User=, chowning it on every start; an unprivileged pepsi-ingress creating
its own socket there fails as soon as another of them restarts. Bound by the
.socket unit, the node is created by systemd as root before the service
starts, so the server needs no write access to the directory at all — and the
socket stays available while the service restarts, so a concurrent
/usr/sbin/sendmail waits rather than failing.
The descriptor is matched by the address it is bound to, not by an
FD_INDEX. An index is a position among the unit’s ListenStream= entries,
so it changes whenever a port is added or removed — a second declaration to keep
in step with a file it cannot see. Asking the kernel what each passed descriptor
is bound to has no such failure mode: adding or removing an SMTP port renumbers
nothing, an outbound-only host that drops port 25 needs no edit, and the unit and
the configuration cannot disagree about which socket this is. If no passed
descriptor matches — no systemd, or a SOCKET the unit does not bind —
pepsi-ingress binds the path itself.
If you change SOCKET, change the unit’s ListenStream= to match or remove
it: an entry nothing accepts on is worse than none, because the caller connects
and then blocks until its own read timeout expires (two minutes, then
EX_TEMPFAIL) instead of being refused at once. pepsi-ingress logs a
warning at start-up naming every passed descriptor no listener claimed.
Failure to bind the socket is fatal: pepsi-ingress refuses to start
rather than come up serving only some of what it was asked to serve. A server
that runs with local submission quietly missing is the failure this arrangement
exists to prevent, and it is invisible until mail goes missing.
Writing your own listener section for the same path is supported, and is how you give the socket different options — a group, a stricter mode, no peer credentials. A section serving that path replaces the built-in listener rather than colliding with it.
85.1.42.1.8. Turning it off¶
SOCKET = none disables local submission entirely: pepsi-ingress serves no
socket, and pepsi-sendmail refuses to run (exit EX_SOFTWARE, before it
reads the message — a permanent misconfiguration, so cron must not retry it).
Programs on the host must then authenticate on 587 or 465 like any other client.
pepsi-setup reports this once, since a mail server with no local submission
path is unusual enough that an operator who did not mean it should hear about it.
85.1.42.1.9. Exit status¶
The traditional sysexits.h values, which cron and other callers act on:
0 |
the message was accepted |
64 |
usage error ( |
65 |
the message was unusable, e.g. |
66 |
the message could not be read from standard input |
67 |
a recipient was rejected as unknown — an RFC 3463 |
69 |
any other permanent rejection by the server ( |
70 |
internal error, e.g. the configuration could not be parsed, or
|
75 |
temporary failure — the socket was unreachable, a command timed out,
the server closed the connection or answered malformed SMTP, or it
deferred with a |
77 |
the server refused on policy grounds — an RFC 3463 |
A permanent rejection is classified by the enhanced status at the start of
the server’s reply, never by searching its prose, so 550 5.1.1 unknown user
(see the 5.7.1 policy page) is reported as an unknown recipient rather than as
a policy refusal.
85.1.42.1.10. Files¶
/run/pepsi/submission.sockThe submission socket, unless
[pepsi-sendmail] SOCKETnames another path. Both ends read that one option, so the client andpepsi-ingresscannot disagree about where it is.
When -c/–config is not given, the configuration is the first existing file from the following list — the same list every Pepsi component searches:
$XDG_CONFIG_HOME/pepsi.conf$HOME/.config/pepsi.conf/etc/pepsi/pepsi.conf/etc/pepsi.conf
85.1.42.1.11. Examples¶
Send a message, taking the recipients from its headers:
printf 'To: ops@example.org\nSubject: nightly\n\nall good\n' | sendmail -t
Watch the dialogue while diagnosing a refusal:
sendmail -v -f alice@example.org bob@example.net < message.eml
85.1.42.1.12. See also¶
pepsi-ingress(1), pepsi-queue(1), pepsi-status(1), pepsi-stage-aliases(1), pepsi.conf(5)