70.1.33. pepsi-sendmail

submit mail locally through the Sendmail interface

Manual section:

1

70.1.33.1.1. Name

pepsi-sendmail - hand a locally-written message to Pepsi, providing the traditional /usr/sbin/sendmail interface.

70.1.33.1.2. Synopsis

sendmail [-t] [-i] [-f SENDER] [-F NAME] [-v] [-N NOTIFY] [-R RET] [-V ENVID] [RECIPIENT …]

mailq

newaliases

70.1.33.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/sendmailcron, 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, 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.

70.1.33.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 are worth stating plainly:

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

70.1.33.1.5. 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). Bcc: headers are removed from the message before it is sent.

-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 is used.

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

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

-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, so this reports where to look; use pepsi-queue(1) for the queue itself and pepsi-status(1) for a summary.

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

-bd, -bD, -q

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

Unlike every other Pepsi program, pepsi-sendmail takes no -c/--config option: the sendmail grammar predates GNU option conventions and assigns -C a conflicting meaning. The configuration file is located the usual way (see pepsi.conf(5)).

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

This affects only how the message is addressed. It does not decide who the message is from: that 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.

This is a change from earlier versions, where local submission was an optional [pepsi-ingress-listener-*] section that pepsi-setup --wizard emitted only if asked. The facility then depended on three separate things agreeing — the section existing, its UNIXPATH matching SOCKET, and the runtime directory being writable by the server — and each was 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, so it is no longer a question.

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

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

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

70

internal error, e.g. the configuration could not be parsed

75

temporary failure — the socket was unreachable or the server deferred; the caller should retry rather than treat the mail as lost (EX_TEMPFAIL)

77

the server refused this sender for the invoking account

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

70.1.33.1.10. See also

pepsi-ingress(1), pepsi-queue(1), pepsi-status(1), pepsi-stage-aliases(1), pepsi.conf(5)