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 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 senderMAILER-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: or Cc: (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:

  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.

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:

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

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

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

REQUIRE_ADDRESSED_TO

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