85.1.31. pepsi-stage-vacation¶
answer mail that arrives while the recipient is away
- Manual section:
1
85.1.31.1.1. Name¶
pepsi-stage-vacation - the out-of-office stage of the Pepsi pipeline.
85.1.31.1.2. Synopsis¶
pepsi-stage-vacation [GLOBAL-OPTIONS] worker
85.1.31.1.3. Description¶
pepsi-stage-vacation is a stage program run by pepsi-dispatch(1) as a
persistent worker reading message ids on standard input. It loads that
pepsi.workqueue row (refusing to act unless its status is running), reads
its [stage-<stage>] section, and — when the envelope recipient is away — sends
that recipient’s vacation notice to the sender.
The message itself always continues to NEXT_STAGE. This stage adds a reply;
it never holds, drops or bounces mail, and NEXT_STAGE is therefore mandatory.
When a notice was sent, the forwarded copy’s Subject: gains VACATION_TAG
(default [VACATION]) so the recipient can see on their return which mail was
answered for them.
The header block is loaded, the body never is: a notice quotes nothing of the original.
85.1.31.1.4. Whose vacation¶
The configuration is read through the per-address override layer, so it is the envelope recipient’s effective configuration. The layers, lowest precedence first:
[stage-<name>] in pepsi.conf the operator's default: normally nobody away
< config_override 'global' a national holiday, for everybody
< config_override 'domain:<d>' a holiday for one served domain
< config_override 'address:<a>' a leave the operator enters for someone
< pepsi.settings[<a>] the account owner's own leave dates
None of that is specific to this stage — pepsi-config(1) and pepsi-settings(1) build the same chain for every stage — but this is the stage where every layer is used: a per-user feature whose global default is “nobody is away” and whose global setting is a holiday. Listing the stage in the EDITABLE_STAGES of pepsi-stage-edit-settings(1) lets account owners set their own leave dates by e-mail.
When one message’s recipients disagree about this stage’s options, the scaffolding has already split it into one row per distinct configuration before the stage runs. A row may still carry several recipients that share a configuration (two aliases of one person, or everybody during a national holiday); if a vacation applies the stage splits those apart too, because each notice is from one address and each rate limit is for one address.
85.1.31.1.5. When a notice is sent¶
Today falls in one of the VACATION_RANGES, and every one of the following holds. Each is a documented reason not to answer, and each is logged when it fires, so “why did nobody get my out-of-office reply” is answerable from the log.
The message is inbound. A locally submitted message (
state.local_origin) is never answered — the settings layer keys such a message on its sender, so without this the account owner’s own leave dates would answer their own outgoing mail.No RFC 3834 rule forbids answering it:
the null sender
<>— a bounce is never answered;Auto-Submitted:with any value butno(RFC 3834 §5) — the field exists so responders recognise each other, and every notice Pepsi sends carriesauto-replied, which is what makes two Pepsi instances safe to point at one another;Precedence: bulk,listorjunk;X-Auto-Response-Suppress:namingAll,OOForAutoReply;any
List-Id:/List-Post:/List-Unsubscribe:/… field, i.e. the message came through a mailing list;a service sender —
MAILER-DAEMON,postmaster,noreply,owner-<list>,<list>-request,<list>-bounces+…(including the VERP forms) — which is nobody’s mailbox and often a robot that answers back;a
multipart/reportbody, which is a delivery report even when it arrives with a non-null envelope sender;the message being from the responding address itself;
an upstream
state.spam = trueverdict: answering spam is backscatter, and it confirms a live mailbox to whoever sent it.
The recipient appears in
To:,Cc:,Resent-To:orResent-Cc:(RFC 3834 §2/§3;Bcc:andResent-Bcc:are deliberately excluded), unless REQUIRE_ADDRESSED_TO is off. Mail that reaches a mailbox without naming it there arrived by blind carbon copy, through an alias the sender never saw, or from a harvested list.That correspondent has not already been answered within SUPPRESS_DAYS.
The notice is a null-sender text/plain message injected at RESPONSE_STAGE,
where it is signed and relayed like any other outbound mail. It is From: the
vacationing address, To: the envelope sender, carries Auto-Submitted:
auto-replied, and quotes the original’s Message-ID: in
In-Reply-To:/References: so it threads under the message it answers. Its
subject is SUBJECT_PREFIX followed by the original subject (RFC 5230 §4.5).
The forwarded copy records that it was answered under state.vacation
(notified, the recipient, the matching range and the language
used); see pepsi.state(7).
Nothing is sent, no query is made and no header is examined when VACATION_RANGES is empty, which is the default and the state of every recipient who has never configured a vacation.
85.1.31.1.6. Loop and flood control¶
An out-of-office responder is the one part of a mail system that generates a message per received message, so it is the one part that can build a loop out of nothing. Four independent mechanisms bound it:
The null envelope sender. A remote responder has nowhere to reply: the address it would answer does not exist.
``Auto-Submitted:``, honoured on the way in and set on the way out.
The rate limit. Even a remote responder that ignores both is answered at most once per SUPPRESS_DAYS, so a loop cannot run away — it costs one message a week.
The queue token. The notice is injected under the token
<token>-vacation, and that column isUNIQUE, so a stage retried after a crash cannot send a second copy.
85.1.31.1.7. Vacation ranges¶
VACATION_RANGES is a comma- or whitespace-separated list of
YYYY-MM-DD:YYYY-MM-DD spans. Both endpoints are inclusive whole days, so
2026-08-01:2026-08-14 covers the first and the fourteenth in full. Two shorthands: an empty end (2026-08-01:) is
open-ended, for a leave with no fixed return date, and a bare date
(2026-08-01) is that single day.
“Today” is the current date in the server’s time zone (from
/etc/localtime): an operator writing 2026-08-14 means the fourteenth where
they live, not the fourteenth in UTC, which east of Greenwich would end the holiday
during the previous afternoon. A deployment that wants UTC boundaries runs with
TZ=UTC, which is a decision about the machine rather than an option of one
stage.
The date is resolved when the stage runs, not when the message arrived, so a message held for two days by pepsi-stage-anti-spam(1) or a key lookup is judged against the day it finally reaches this stage — which is also the safe reading, since the alternative answers mail on behalf of someone already back at their desk.
Overlapping ranges are allowed; the first one that matches is the one whose dates the notice quotes. A range whose end precedes its start is a configuration error.
85.1.31.1.8. The message¶
The notice text is a Mustache template held in an
INI option, not in a file. This is the one message in Pepsi that people write
themselves, and the only channel an account owner has — their pepsi.settings
row, whether set with pepsi-settings(1) or by mail through
pepsi-stage-edit-settings(1) — carries options of this stage’s own section.
Two sources are consulted, in this order, for each candidate language:
MESSAGE_<LANG>in the stage’s own section (MESSAGE_EN,MESSAGE_DE_CH, …) — per-address overridable, hence the user’s own text;<LANG>in the section named by DEFAULT_MESSAGE_SECTION (default[pepsi-vacation-default-message], shipped in${DATADIR}/config.dso it exists untouched). That section is read from the base configuration: the per-address settings layer can only carry[stage-*]options, so only theMESSAGE_<LANG>form above is a user’s own text.
The first source wins per language, not wholesale: a user who writes only
MESSAGE_DE still gets the operator’s English text for English correspondents.
85.1.31.1.8.1. Choosing the language¶
Candidates are the sender’s detected languages — state.language, the
Accept-Language-shaped string pepsi-stage-detect-language(1) records — in
descending confidence, then DEFAULT_LANGUAGE. For each candidate the closest
available message is used: the exact tag, then its primary subtag (de-ch falls
back to de), then any tag that refines it (de accepts a lone DE_CH, so
an operator who wrote only Swiss German still reaches German speakers). Tags may be
spelled with either separator: DE_CH and DE-CH are the same language.
Note that pepsi-stage-detect-language(1) reports primary subtags only
(en, de), never regions, so a region-qualified message is reached through
the refinement rule or by setting DEFAULT_LANGUAGE to it.
85.1.31.1.8.2. Fields¶
{{SENDER_NAME}}The sender’s
From:display name, or their full address when theFrom:carries no name.{{VACATION_START}}First day away, as
YYYY-MM-DD.{{VACATION_END}}Last day away — absent for an open-ended leave.
{{EMERGENCY_CONTACT}}The configured EMERGENCY_CONTACT — absent when there is none.
The two that can be absent are absent, not empty, so a template branches on them
with {{#VACATION_END}}…{{/VACATION_END}} and
{{^VACATION_END}}…{{/VACATION_END}} rather than rendering a sentence with a
hole in it. Dates are ISO YYYY-MM-DD: unambiguous in every locale, which a
localised month name would not be.
Escaping is not a concern: these four fields render verbatim whichever brace style
is used, because the notice is text/plain. (Mustache’s {{x}} normally
HTML-escapes, which would greet a correspondent called O'Brien as
O'Brien.)
85.1.31.1.8.3. The two-space rule¶
An INI option is a single line, so two spaces become a newline — and four spaces two newlines, i.e. a paragraph break. The substitution is applied to the template before Mustache expands it, never to the result, so a display name that happens to contain two spaces cannot restructure the message.
The taler parser trims the whole line, so a trailing double space is lost unless
the value is quoted (MESSAGE_EN = "… " — exactly one surrounding pair of quotes
is stripped).
85.1.31.1.9. Options¶
All of these are per-address overridable.
- VACATION_RANGES
Comma- or whitespace-separated
YYYY-MM-DD:YYYY-MM-DDspans (see above). Empty by default, which switches the stage off for that address.- RESPONSE_STAGE
Stage the notice is injected at, so it is signed and relayed. Required.
- DEFAULT_LANGUAGE
Language used when none of the sender’s detected languages has a message. Default
en.- DEFAULT_MESSAGE_SECTION
Configuration section holding one inline message per language. Default
pepsi-vacation-default-message.- EMERGENCY_CONTACT
A single bare address to reach while the recipient is away, offered to the template. Unset by default, in which case a template’s
{{#EMERGENCY_CONTACT}}block is skipped.- VACATION_TAG
Appended to the forwarded message’s
Subject:when a notice was sent. Default[VACATION]; the valuenonedisables tagging. Appending is idempotent — a correspondent replying to a tagged message quotes the tag back, and a second pass does not collect a second one.An empty value does not disable it: the taler parser treats an empty value as an absent option, which re-applies the default. That is why there is a sentinel.
- SUPPRESS_DAYS
How long after answering one correspondent they may be answered again. Default
7(the Sievevacationdefault, RFC 5230 §4.1).0answers every message; the maximum is3650(ten years).- REQUIRE_ADDRESSED_TO
Only answer when the recipient appears in
To:,Cc:,Resent-To:orResent-Cc:(RFC 3834 §2/§3). Default on.- SUBJECT_PREFIX
Prepended to the original subject to form the notice’s subject. Default
Auto:(RFC 5230 §4.5). A message with no subject yields the prefix alone, and an already-prefixed subject is not prefixed twice.
85.1.31.1.10. Placement¶
On the inbound path:
after pepsi-stage-detect-language(1), which supplies
state.language, so the notice can be in the sender’s language;after pepsi-stage-check-whitelist(1) and pepsi-stage-anti-spam(1), so spam and unpaid mail draw no reply;
after pepsi-stage-aliases(1) if aliases are in use: the configuration is keyed on the envelope recipient, so it must be the user’s own address by then, not the alias.
One consequence of running after pepsi-stage-arc(1): rewriting the
Subject: invalidates the originator’s DKIM signature and the ARC message
signature Pepsi itself applied, since both cover (and over-sign) Subject. That
is harmless on a branch that ends in local delivery — the placement above, and the
one pepsi-stage-decrypt(1) already relies on for the same reason. On a branch
that relays the message onward, the next hop will see those signatures fail, so
set VACATION_TAG = none there.
85.1.31.1.11. Database¶
One table, pepsi.vacation_reply, holding (recipient, sender, last_sent), and
one function, vacation_should_reply(), which decides and records in the same
statement — two workers holding two messages from the same correspondent would
otherwise both find no previous notice and both send one. It also prunes as it
goes: this recipient’s rows older than their own window, plus a sweep of every
row over a year old, for a recipient who stops going away; there is no cron job
to run. A claim taken for a notice that then fails to build or queue is deleted
again, so the correspondent is not marked as answered without having been. A
message retried after its notice was queued sends no second one: within
SUPPRESS_DAYS the claim refuses it, and with SUPPRESS_DAYS = 0 the notice’s
token, derived from the message’s, makes the queue return the notice already
there.
85.1.31.1.12. Subcommands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.
85.1.31.1.13. Global Options¶
- -c FILE, –config FILE
Read the configuration from FILE instead of searching the default locations.
- -L LOGLEVEL, –log LOGLEVEL
Set the logging verbosity (default
info).- -v, –verbose
Show log messages from all sources.
- -h, –help; -V, –version
Print a usage summary / the version and exit.
85.1.31.1.14. Exit Status¶
- 0
The message was processed (forwarded, with or without a notice having been sent).
- 1
An error occurred (message not found or not
running, or a per-address setting that breaks the stage’s section — a bad date range, say). The reason is written to the log.A fault of the host (the database, a template or helper that cannot be used) is not reported as a failure: the message is paused and retried, as Stage errors in pepsi-dispatch(1) describes. A section that does not parse makes the worker refuse to start (status 78) instead of failing each message in turn.
A notice that cannot be built or queued is not an error: it is logged as a warning and the message is forwarded unchanged. The recipient’s mail matters more than the sender’s courtesy reply.
85.1.31.1.15. Examples¶
Answer one message (it must be running):
echo 42 | pepsi-stage-vacation -c /etc/pepsi/pepsi.conf worker
A pipeline section on the inbound path, with no vacation configured — the operator wires the stage once and users switch it on for themselves:
[stage-vacation]
PROGRAM = pepsi-stage-vacation
NEXT_STAGE = local
RESPONSE_STAGE = dkim-sign
A national holiday, for every address the instance serves:
pepsi-config -c /etc/pepsi/pepsi.conf set --scope global \
stage-vacation VACATION_RANGES 2026-12-24:2026-12-26
One user’s leave, entered by the operator:
pepsi-settings -c /etc/pepsi/pepsi.conf \
set alice@example.org vacation VACATION_RANGES 2026-08-01:2026-08-14
pepsi-settings -c /etc/pepsi/pepsi.conf \
set alice@example.org vacation EMERGENCY_CONTACT ops@example.org
…or by the user themselves, if the operator listed vacation in
pepsi-stage-edit-settings(1)’s EDITABLE_STAGES — a mail to
pepsi@example.org with the subject Pepsi and this body:
[stage-vacation]
VACATION_RANGES = 2026-08-01:2026-08-14
MESSAGE_EN = Hi {{SENDER_NAME}}, I am off sailing until {{VACATION_END}}.
85.1.31.1.16. See Also¶
pepsi-config(1), pepsi-settings(1), pepsi-stage-detect-language(1), pepsi-stage-edit-settings(1), pepsi-stage-aliases(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)
RFC 3834 (automatic responses), RFC 5230 §4 (the Sieve vacation action, whose
:days and subject rules this stage follows).
85.1.31.1.17. Bugs¶
Report bugs to the Pepsi issue tracker.