70.1.24. pepsi-stage-vacation¶
answer mail that arrives while the recipient is away
- Manual section:
1
70.1.24.1.1. Name¶
pepsi-stage-vacation - the out-of-office stage of the Pepsi pipeline.
70.1.24.1.2. Synopsis¶
pepsi-stage-vacation [GLOBAL-OPTIONS] worker
70.1.24.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.ingress 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.
70.1.24.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 all of it is genuinely 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.
70.1.24.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:orCc:(RFC 3834 §3), 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).
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.
70.1.24.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.
70.1.24.1.7. Vacation ranges¶
VACATION_RANGES is a comma-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.
70.1.24.1.8. The message¶
The notice text is a Mustache template held in an
INI option, not in a file. That is deliberate: 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).
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.
70.1.24.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.
70.1.24.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.)
70.1.24.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).
70.1.24.1.9. Options¶
All of these are per-address overridable.
- VACATION_RANGES
Comma-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.- REQUIRE_ADDRESSED_TO
Only answer when the recipient appears in
To:orCc:(RFC 3834 §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.
70.1.24.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.
70.1.24.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. Rows older than the
recipient’s own window are pruned as they are passed, with a one-year floor for a
recipient who stops going away; there is no cron job to run.
70.1.24.1.12. Subcommands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.
70.1.24.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.
70.1.24.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, a misconfigured stage — a bad date range, a missing RESPONSE_STAGE or NEXT_STAGE — or a database error). The reason is written to the log.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.
70.1.24.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}}.
70.1.24.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).
70.1.24.1.17. Bugs¶
Report bugs to the Pepsi issue tracker.