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 but no (RFC 3834 §5) — the field exists so responders recognise each other, and every notice Pepsi sends carries auto-replied, which is what makes two Pepsi instances safe to point at one another;

    • Precedence: bulk, list or junk;

    • X-Auto-Response-Suppress: naming All, OOF or AutoReply;

    • 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/report body, 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 = true verdict: answering spam is backscatter, and it confirms a live mailbox to whoever sent it.

  • The recipient appears in To:, Cc:, Resent-To: or Resent-Cc: (RFC 3834 §2/§3; Bcc: and Resent-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:

  1. The null envelope sender. A remote responder has nowhere to reply: the address it would answer does not exist.

  2. ``Auto-Submitted:``, honoured on the way in and set on the way out.

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

  4. The queue token. The notice is injected under the token <token>-vacation, and that column is UNIQUE, 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:

  1. MESSAGE_<LANG> in the stage’s own section (MESSAGE_EN, MESSAGE_DE_CH, …) — per-address overridable, hence the user’s own text;

  2. <LANG> in the section named by DEFAULT_MESSAGE_SECTION (default [pepsi-vacation-default-message], shipped in ${DATADIR}/config.d so it exists untouched). That section is read from the base configuration: the per-address settings layer can only carry [stage-*] options, so only the MESSAGE_<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 the From: 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&#39;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-DD spans (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 value none disables 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 Sieve vacation default, RFC 5230 §4.1). 0 answers every message; the maximum is 3650 (ten years).

REQUIRE_ADDRESSED_TO

Only answer when the recipient appears in To:, Cc:, Resent-To: or Resent-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.