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

/usr/sbin/sendmail

the submission client described here

/usr/lib/sendmail

the same (historical path)

/usr/bin/mailq

sendmail -bp

/usr/bin/newaliases

sendmail -bi

/usr/sbin/rmail

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-ingress enforces it (a bare LF in DATA is refused with 500 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’s To: 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: and Bcc: 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_DOMAIN is used (and the literal nobody@ 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 sendmail binary runs from cron and 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 sendmail grammar; 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 -c rather than sendmail’s -C because -C already means an alternate sendmail.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 DSN extension. See pepsi-stage-bounce(1).

NOTIFY is NEVER or a comma-separated list of SUCCESS, FAILURE and DELAY (RFC 3461 §4.1); RET is FULL or HDRS (§4.3). Anything else is a usage error rather than a 501 from the server. ENVID is xtext-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: mailq therefore 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 newaliases unconditionally 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 spelling cron still emits (-odi, -oem), and -d is 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-ingress UNIX-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 none switches 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, or localhost if 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 root that cron and system tools hand to sendmail without an @. Defaults to [pepsi-ingress] HOSTNAME, or localhost if 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 no USERNAME_MAP the two agree by construction. That is still only how the message is addressed: who it may be from comes from the peer’s uid via SO_PEERCRED and the USERNAME_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 (EX_USAGE)

65

the message was unusable, e.g. -t found no recipient

66

the message could not be read from standard input

67

a recipient was rejected as unknown — an RFC 3463 x.1.x status, or a bare 550 (EX_NOUSER)

69

any other permanent rejection by the server (EX_UNAVAILABLE)

70

internal error, e.g. the configuration could not be parsed, or SOCKET = none switched local submission off (EX_SOFTWARE); also the exit for the accepted-but-unsupported modes -bp/mailq, -bs and -bv, each of which explains itself on standard error

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 4xx; the caller should retry rather than treat the mail as lost (EX_TEMPFAIL)

77

the server refused on policy grounds — an RFC 3463 x.7.x status, typically a sender the invoking account may not use (EX_NOPERM)

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.sock

The submission socket, unless [pepsi-sendmail] SOCKET names another path. Both ends read that one option, so the client and pepsi-ingress cannot 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)