.. This file is part of PEPSI. Copyright (C) 2026 Pepsi contributors PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. ==================== pepsi-stage-vacation ==================== *Answer mail that arrives while the recipient is away.* Role ==== ``pepsi-stage-vacation`` is the out-of-office responder. When the envelope recipient is away on the day the stage runs, it sends that recipient's vacation notice to the sender and tags the forwarded copy's ``Subject:``. The message itself **always** continues to ``NEXT_STAGE``: the stage adds a reply, it never holds, drops or bounces mail. Reference: :manpage:`pepsi-stage-vacation(1)`. Whose vacation ============== The stage reads its options through the per-address override layer, so they are the **envelope recipient's**. That is what makes one stage serve both cases the feature has to cover: .. code-block:: text [stage-] in pepsi.conf the operator's default: normally nobody away < config_override 'global' a national holiday, for everybody < config_override 'domain:' a holiday for one served domain < config_override 'address:' a leave the operator enters for someone < pepsi.settings[] the account owner's own leave dates The chain is not this stage's invention — :doc:`../configuration` describes it, and every stage gets it — but this is the stage that uses all of it. Operators enter a holiday with :doc:`pepsi-config`, a user's leave with :doc:`pepsi-settings`; listing ``vacation`` in :doc:`pepsi-stage-edit-settings`'s ``EDITABLE_STAGES`` lets account owners set their own dates by e-mail. A row that still carries several recipients sharing one configuration (two aliases of a person, or everybody during a holiday) is split one recipient per row before any notice is built: each notice is *from* one address, and each rate limit is *for* one address. When a notice is sent ===================== Today — in the server's time zone, resolved when the stage runs — falls in one of the ``VACATION_RANGES`` (inclusive whole days; ``2026-08-01:`` is an open-ended leave), **and** the message is one it is proper to answer. The full RFC 3834 rule set is in the man page; the short version is that Pepsi does not answer bounces, mailing-list mail, anything marked ``Auto-Submitted:`` or ``Precedence: bulk``, service senders such as ``MAILER-DAEMON`` or ``-request``, delivery reports, mail an upstream stage marked as spam, mail the recipient sent themselves, or mail that does not name the recipient in ``To:``/``Cc:``. On top of that, one correspondent is answered at most once per ``SUPPRESS_DAYS`` (default 7). An out-of-office responder is the one component that emits a message per received message, so four things bound it at once: the notice's **null envelope sender** (nothing can reply to it), the ``Auto-Submitted: auto-replied`` it carries, the per-correspondent rate limit (a responder that ignores both still costs one message a week), and the ``UNIQUE`` queue token that makes a retried stage unable to send a second copy. The message =========== The notice text is a Mustache template held in an **INI option** rather than in a ``TEMPLATE_DIR`` file, because this is the message users write themselves and the settings layer carries options, not files. Per candidate language the stage prefers ``MESSAGE_`` in the stage's own section (the user's own text) and falls back to ```` in ``DEFAULT_MESSAGE_SECTION`` — by default ``[pepsi-vacation-default-message]``, which ships in ``${DATADIR}/config.d`` with ten languages, so a working default exists before anyone configures anything. The language is chosen from ``state.language`` (written by :doc:`pepsi-stage-detect-language`) and falls back to ``DEFAULT_LANGUAGE``. The fields are ``{{SENDER_NAME}}``, ``{{VACATION_START}}``, ``{{VACATION_END}}`` and ``{{EMERGENCY_CONTACT}}``; the last two are *absent* rather than empty when they do not apply, so a template can branch. Since an INI option is one line, **two spaces become a newline** (four, a paragraph break). Placement ========= After :doc:`pepsi-stage-detect-language` (for the language), after :doc:`pepsi-stage-check-whitelist` and :doc:`pepsi-stage-anti-spam` (so spam and unpaid mail draw no reply), and after :doc:`pepsi-stage-aliases` if aliases are in use (the configuration is keyed on the envelope recipient, which must be the user's own address by then). Rewriting the ``Subject:`` invalidates the originator's DKIM signature and Pepsi's own ARC message signature, both of which cover it — harmless on a branch ending in local delivery, which is the placement above and the one :doc:`pepsi-stage-decrypt` already relies on. On a branch that relays the message onward, set ``VACATION_TAG = none``. Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-vacation``, ``NEXT_STAGE`` (mandatory), ``RESPONSE_STAGE`` (mandatory — where the notice is injected to be signed and relayed), ``VACATION_RANGES``, ``DEFAULT_LANGUAGE``, ``DEFAULT_MESSAGE_SECTION``, ``EMERGENCY_CONTACT``, ``VACATION_TAG``, ``SUPPRESS_DAYS``, ``REQUIRE_ADDRESSED_TO`` and ``SUBJECT_PREFIX``. ``pepsi-setup`` rejects a bad date range, an unresolvable ``NEXT_STAGE``/``RESPONSE_STAGE``, and a configuration with no message for ``DEFAULT_LANGUAGE``. See :manpage:`pepsi-stage-vacation(1)` and :doc:`../configuration`. State ===== * **Inputs:** ``state.language`` (for the notice's language), ``state.local_origin`` (a locally submitted message is never answered), ``state.spam``. * **Outputs:** ``state.vacation`` recording that a notice went out, for whom, for which range and in which language — so :doc:`pepsi-stage-if` can branch on it and :doc:`pepsi-queue` shows it. Nothing else in ``state`` is touched, and ``state.dsn`` is preserved. * **Transitions:** always advance to ``NEXT_STAGE``; a separate null-sender notice is injected at ``RESPONSE_STAGE``. Database ======== ``pepsi.vacation_reply`` (one row per answered correspondent) and ``vacation_should_reply()``, which claims and records the right to answer in one statement so two workers cannot both answer the same correspondent. Aged rows are pruned in passing; there is no job to schedule. See also ======== :doc:`pepsi-stage-detect-language`, :doc:`pepsi-stage-edit-settings`, :doc:`pepsi-settings`, :doc:`pepsi-config`, :doc:`../features`, :manpage:`pepsi-stage-vacation(1)`.