70.1.44. pepsi-helper-maildir-writer¶
drop a message into a local user’s Maildir/new/ as root
- Manual section:
1
70.1.44.1.1. Name¶
pepsi-helper-maildir-writer - privileged local-delivery helper that
writes a single message from standard input into a target user’s
Maildir/new/.
70.1.44.1.2. Synopsis¶
pepsi-helper-maildir-writer [OPTIONS] TARGET-UID < message
pepsi-helper-maildir-writer measure [OPTIONS] TARGET-UID
70.1.44.1.3. Description¶
pepsi-helper-maildir-writer is a minimal, security-hardened helper that
delivers exactly one message into a local user’s Maildir. It exists so that a
trusted but unprivileged caller (a member of the pepsi-maildir group) can
drop mail into any local user’s mailbox — and do nothing else.
The helper is installed setuid-root, owned root:pepsi-maildir with mode
4750 (rwxr-x--- plus the setuid bit). Consequently only members of the
pepsi-maildir group may execute it; when they do, the kernel runs it with an
effective uid of root.
Given the numeric uid of the target user, and the raw message on standard input, the helper:
resolves the target user’s passwd entry (its home directory and primary group), refusing uid
0and any uid below/etc/login.defsUID_MIN(default1000if unreadable) — system accounts are never valid delivery targets;fully and irreversibly drops privilege from
rootto the target user — real, effective and saved uid and gid — first dropping all supplementary groups, then the gid, then the uid, and asserts that root cannot be regained. Everything afterwards acts with the target user’s identity, so any file it creates is owned by that user;creates the standard Maildir layout (
tmp,newandcur, mode0700) under~/Maildir if it does not already exist;creates a uniquely named file in
Maildir/tmp/withO_CREAT|O_EXCL(so it has exclusive control over the file and never disturbs another delivery’s file), mode0600, following the Maildir naming convention<seconds>.M<microseconds>P<pid>Q<sequence>.<hostname>;copies the message from standard input into that file, using
splice(2) when the descriptors allow it (the common case, when stdin is a pipe) and a plain read/write loop otherwise, counting the bytes as they pass;fsync(2)s the file, and — when a quota is being enforced and--measurewas given — decides whether the mailbox has room for it, abandoning the delivery with exit status 3 if it has not; andrenames it into
Maildir/new/under a name carrying the Maildir++,S=size tag,fsync(2)s that directory, and reports what the mailbox now holds on standard output.
The helper reads no configuration, opens no network connection and contacts no database. Its narrow scope is the whole point of the privilege separation: the quota policy is decided by the caller and passed in as arguments, because by the time the helper could read anything it is the target user, who does not get to choose their own quota.
70.1.44.1.4. Mailbox quota¶
The helper is the only part of Pepsi that can see how full a mailbox is: a
Maildir is mode 0700, and this is the one process that becomes the user.
Everything else — the delivery stage’s decisions, pepsi-ingress(1)’s
refusal at RCPT, the pepsi-status(1) report — works from figures this
helper reported.
It measures only when asked (--measure). The caller keeps a running
estimate in pepsi.mailbox_quota and asks for a measurement when that estimate
suggests the account may be near its limit; because the estimate only ever
over-counts (nothing tells it when a user deletes mail over IMAP), “no
measurement needed” is a proof that there is room. An ordinary delivery to a
mailbox that is nowhere near full therefore costs one small append and no
directory walk.
Three accounting layers are used together, cheapest first:
maildirsizeThe Maildir++ size file at the root of the mailbox, in Courier’s format (a quota definition line, then appended
<bytes> <messages>deltas). Dovecot’squota = maildir:backend, Courier and Exim’smaildir_use_size_fileall read and write the same file, so a site running one of them for IMAP and Pepsi for delivery keeps one set of numbers. It is used when it is current, and rewritten from a full walk when it is missing, unparseable, computed against different limits, or has accumulated a long delta log. It lives in the user’s own directory, so it is advisory: a user can rewrite their ownmaildirsize.- A full tree walk
new/andcur/of the INBOX and of every Maildir++ folder (.Sent,.Trash, …) — the whole tree, which is what the user calls “my mailbox”.tmp/is not counted (a file there is a delivery in flight). The walk is onereaddir(3) per folder and nostat(2) calls, because Maildir++ encodes each message’s size in its file name (,S=n) — which is exactly why this helper tags the files it writes. A message left untagged by some other agent isstated individually.quotactl(2)With
--fs-quota, the kernel’s own accounting is consulted as well. This is the only tamper-proof layer, and its limit tightens the effective one: promising space thatEDQUOTwould refuse helps nobody. It works unprivileged becauseQ_GETQUOTAis permitted for a caller asking about its own uid, and by this point the helper has irreversibly become that user. Note the kernel counts every block the user owns on that filesystem, not just their mail, so its figures are reported separately rather than replacing the mailbox measurement. A filesystem that does not answer (XFS project quotas, ZFS, no quota support) simply yields nothing, and the other layers still apply.
Because the message’s size is unknown until standard input has been drained, the
message is buffered in tmp/ (which does not count towards the quota) before
the decision is made, and unlinked if it does not fit. Standard input is drained
in every case: a helper that exited early would leave its caller writing into a
broken pipe, which would be reported as a transport failure rather than the clean
refusal it is.
Two deliveries running at once may both see room and both write, overshooting the limit by up to one message each. Every implementation of Maildir++ quota accepts that; the kernel-quota layer, where a site has one, closes it exactly.
70.1.44.1.5. Arguments¶
- TARGET-UID
The numeric user id of the local user into whose
Maildir/new/the message is delivered. It must be a positive integer with a matching passwd entry; uid0(root) and any uid below/etc/login.defsUID_MINare refused.measureMeasure the mailbox and report, delivering nothing. Standard input is not read. Used by pepsi-quota(1)’s
measureandreconcile.--measureEstablish the mailbox’s true size before writing, and enforce the quota given by
--quota-bytes/--quota-countagainst it. Without this flag the helper delivers unconditionally and reports only what it added.--fs-quotaAlso consult
quotactl(2), report the kernel’s figures, and let the kernel’s limit tighten the effective one. Set by the delivery stage from[pepsi] MAILBOX_FS_QUOTA, which pepsi-setup(1) probes for.--quota-bytesNThe byte limit to enforce.
0or absent means no limit.--quota-countNThe message limit to enforce.
0or absent means no limit.
The message to deliver is read in its entirety from standard input.
70.1.44.1.6. Output¶
On standard output the helper writes one tagged, tab-separated record per line. An unknown tag is ignored by the caller rather than being an error, so a newer helper can report more to an older caller.
uTABbytesTABmessagesMeasured usage of the whole Maildir++ tree, including the message just delivered. The caller stores this as the account’s usage and resets its since-measurement counters.
fTABusedTABlimitTABinodesTABinode-limitThe
quotactl(2) figures, with0where the kernel enforces no limit.dTABbytesBytes this run added, reported instead of
uwhen no measurement was made, so the caller adds to its estimate rather than replacing it.sTAB(fsquota|maildirsize|scan)How the
ufigure was obtained.wTABtextA non-fatal problem (an unreadable folder, a
maildirsizethat could not be updated). Never fatal to the delivery.
A report is written even when the delivery was refused for want of space — especially then, since a fresh measurement is what licenses pepsi-ingress(1) to refuse the next message in the SMTP session rather than accepting it only to bounce it.
70.1.44.1.7. Installation¶
The binary must be installed setuid-root and group-restricted:
chown root:pepsi-maildir pepsi-helper-maildir-writer
chmod 4750 pepsi-helper-maildir-writer
make install does this automatically (its install-helper step), provided
it is run as root and the pepsi-maildir group exists; otherwise it prints the
exact commands to run by hand. If the helper is placed on a filesystem mounted
nosuid, the setuid bit is ignored at runtime and the helper will refuse to
run (it cannot gain root), so install it on a normal filesystem.
70.1.44.1.8. Exit Status¶
The exit code tells the calling stage whether retrying can ever help, so a message is neither bounced while it could still be delivered nor queued for days against a mailbox that will never accept it. A diagnostic is always written to standard error, and pepsi-stage-relay-to-maildir(1) both logs it and puts it into the bounce it generates.
- 0
The message was written to the target user’s
Maildir/new/and synced to disk.- 1
Transient failure: a later attempt may succeed, so the caller keeps the message queued and retries with backoff. This covers a full disk (
ENOSPC), an I/O error, a read-only filesystem, and a helper that is not (yet) installed setuid-root — all of which an administrator can repair underneath a queued message.- 2
Permanent failure: the mailbox is unusable and no retry will change that until an administrator fixes the account, so the caller bounces the message at once. This covers a
Maildirthat is not a directory (a legacy mbox file), ownership or a mode that locks the user out of their ownMaildir(EACCES/EPERM), a missing home directory (ENOENT), a uid with no passwd entry, a home directory that is not absolute, and a uid belowUID_MIN.- 3
The mailbox is over quota, so the message was not written. Deliberately neither transient nor permanent: whether a full mailbox is worth waiting for is a site policy (
[pepsi] MAILBOX_OVER_QUOTA), and the helper is in no position to know whether this user empties their mailbox daily or abandoned it a year ago. It reports the fact; pepsi-stage-relay-to-maildir(1) applies the policy.EDQUOTfrom the kernel is reported the same way — a full disk is the administrator’s problem and gets fixed underneath a queued message, whereas a full mailbox is the recipient’s and may never be.
When invoked by a user who is not a member of the pepsi-maildir group, the
kernel refuses to execute the binary at all (Permission denied); the helper
itself never runs.
70.1.44.1.9. Security¶
The helper trusts its caller to be authorised (membership of the
pepsi-maildir group is the access-control gate) and will deliver to any
regular local user (uid ≥ UID_MIN, never a system account). It drops
supplementary groups before changing uid and then sheds root completely (real,
effective and saved ids), asserting root cannot be regained before it does any
file I/O, and creates the delivery file exclusively so it can never be tricked
into writing over a file it does not own.
70.1.44.1.10. See Also¶
pepsi-stage-relay-to-maildir(1), pepsi-ingress(1), pepsi-dispatch(1), pepsi.conf(5), maildir(5)
70.1.44.1.11. Bugs¶
Report bugs to the Pepsi issue tracker.