.. 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 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. ============================== Getting started on a cheap VPS ============================== This chapter is a single, end-to-end walkthrough: starting from a freshly rented virtual private server and a domain name you have just registered, it takes you all the way to a **first e-mail received and delivered into a local mailbox** — and a proper **bounce** sent back for anything that cannot be delivered. Every other chapter explains one piece in depth; this one strings the pieces into one concrete story you can follow top to bottom. What we will build ================== A small mail host that accepts mail for ``example.org`` on the standard SMTP port and delivers each message into the recipient's local ``Maildir`` on the same server. Anything it **cannot** deliver locally — an unknown recipient, a mailbox that is full or unwritable — is turned into a real **bounce** (a delivery-status notification) and sent back to the original sender, rather than silently disappearing into the queue. The inbound pipeline is short:: inbound SMTP -> [stage-init] -> [stage-local] (pepsi-ingress) pepsi-stage-arc pepsi-stage-relay-to-maildir (record the (deliver to the user's inbound verdict) ~/Maildir/new/) and whatever it cannot deliver is signed and mailed back to the sender over a short **outbound** path:: [stage-bounce] -> [stage-sign] -> [stage-internet] pepsi-stage-bounce pepsi-stage-dkim-sign pepsi-stage-relay-to-internet (build the DSN) (sign the bounce) (send it to the sender's MX) Replace ``example.org`` with your own domain and ``alice`` with a real account name throughout. Because the host now also *sends* that bounce — DKIM-signed, from an SPF-authorised address — this setup publishes the sender-authentication records that let receivers trust its outgoing mail: **DKIM**, **SPF** and **DMARC**, which a receive-only host would not need. (**MTA-STS** and **TLS reporting**, which harden *inbound* TLS and need the HTTPS server running, are left off here and added as optional hardening — see :ref:`gs-next-steps`.) A final section reuses exactly the same outbound path to add an authenticated submission port so your own users can *send* through the host too. .. note:: **Scope.** This is the simplest realistic deployment — *receiving* mail, delivering it **locally**, and bouncing what it cannot. It is not the forwarding showcase Pepsi is built for (ARC + SRS + DKIM re-signing on the way back out); see :doc:`introduction` and :doc:`programs/pepsi-stage-relay-to-internet` for that, and :ref:`gs-next-steps` for how to grow this host into one. Pepsi delivers *into* a Maildir but ships **no IMAP/POP server**, so once mail has landed you read it on the box itself (``mutt``, ``mail``) until you add a mailbox server such as Dovecot. See :ref:`gs-next-steps`. Prerequisites ============= * A VPS with a **public, static IPv4 address** (and ideally an IPv6 address) and root access. We use ``203.0.113.7`` / ``2001:db8::25`` as placeholders. * A registered **domain** whose DNS you can edit (``example.org``). * **TCP port 25 must reach the VPS both ways.** Inbound port 25 carries the mail you receive; **outbound** port 25 lets the host send bounces back to senders. Many budget providers block *outbound* 25 by default — if inbound mail never arrives, or bounces never leave the queue, that is the usual cause; ask the provider to open it. (If outbound 25 cannot be unblocked, send the bounces through a smarthost instead — see :doc:`programs/pepsi-stage-relay-to-smarthost`.) * **Reverse DNS (PTR).** Set the VPS's PTR record to ``mail.example.org`` — this is done in the VPS provider's control panel, *not* in your domain's zone. It must name the **same** host you announce in ``EHLO``: many receivers compare the two and refuse the session when they differ (*HELO host does not match rDNS*), which is easy to trip on a machine that answers to several names. ``pepsi-setup run`` warns about every sending address whose ``PTR`` is missing, unconfirmed or names a different host, and ``pepsi-setup --wizard`` proposes the ``PTR`` name as the hostname for exactly this reason. * The build prerequisites from :doc:`installation`: a Rust toolchain, PostgreSQL, and (for automatic TLS) ``certbot``. Install them with your platform's package manager. Step 1 — Delegate DNS and place the base records ================================================ Pepsi contains **no DNS server**; you publish records wherever your domain's zone is authoritative. You have two choices, and "delegation" means different things for each: * **Host the zone at a DNS provider** (your registrar, or a service such as a managed DNS host). Here the registrar already delegates ``example.org`` to that provider's name servers; you only add records in their panel. This is the simplest path. * **Run your own authoritative name server** (BIND, NSD, Knot, …) on the VPS or elsewhere. Then at the registrar you set ``example.org``'s ``NS`` records to your name servers and, if a name server is itself *inside* ``example.org``, add the matching **glue** ``A``/``AAAA`` records at the registrar so the delegation can be resolved:: ; at the registrar (delegation + glue) example.org. NS ns1.example.org. ns1.example.org. A 203.0.113.7 Either way, once the zone is under your control, add the two records the mail host needs immediately:: ; the mail host's address mail.example.org. A 203.0.113.7 mail.example.org. AAAA 2001:db8::25 ; mail for the domain goes to that host example.org. MX 10 mail.example.org. .. note:: The remaining records — **SPF**, **DKIM** and **DMARC** — are added in :ref:`gs-publish` *after* ``pepsi-setup`` has generated the DKIM key and printed the exact text to publish. (MTA-STS and TLS-reporting records are only added if you enable those optional features — see :ref:`gs-next-steps`.) Add the ``A``/``AAAA`` record now, though: the automatic TLS certificate in Step 5 requires ``mail.example.org`` to already resolve to this VPS. Step 2 — Install Pepsi ====================== Build and install from a source checkout (there is no binary package yet). Fetch the vendored submodule first, then install binaries, SQL and the sample config in one step:: git submodule update --init --recursive make install PREFIX=/usr SYSCONFDIR=/etc This installs the ``pepsi-*`` binaries, the SQL migrations and a sample ``/etc/pepsi/pepsi.conf``. See :doc:`installation` for what each step does and for the database-schema lifecycle; run ``make install`` as root so the local-delivery helper can be installed with its privileges (next step). Step 3 — Prepare the system =========================== Local delivery writes into other users' home directories, so Pepsi separates privileges with a dedicated service user and a tightly scoped setuid helper. Create the accounts (names are conventional; use your platform's user/group tools): * a ``pepsi`` **service user** the long-running daemons run as, and * a ``pepsi-maildir`` **group** that gates the delivery helper. The delivery path has two privilege bits, which ``make install`` sets automatically when run as root *and* the ``pepsi-maildir`` group already exists; otherwise set them by hand: * :manpage:`pepsi-helper-maildir-writer(1)` must be setuid-root and group-restricted (``chown root:pepsi-maildir … ; chmod 4750 …``), and * :doc:`programs/pepsi-stage-relay-to-maildir` must be setgid ``pepsi-maildir`` (``chmod 2755 …``), which is what lets the unprivileged ``pepsi`` worker exec the helper and nothing else can. See those two chapters for the rationale. Create the database. Pepsi connects over the local socket as the connecting system user (peer authentication), so create a ``pepsi`` role and a database it owns to match ``CONFIG = postgres:///pepsi`` below:: createuser pepsi createdb -O pepsi pepsi Finally, create at least one **local mailbox account** to receive mail:: useradd --create-home alice Step 4 — Write the configuration ================================ All components read one file, ``/etc/pepsi/pepsi.conf``. The sample installed in Step 2 is a full-featured forwarder; for this walkthrough you want the minimal local-delivery-plus-bounce configuration instead. There are two equivalent ways to produce it — let ``pepsi-setup`` generate it from a few answers, or write it by hand — and they build the **same pipeline**. (The format and every option are described in :doc:`configuration` and :manpage:`pepsi.conf(5)`.) .. tab-set:: .. tab-item:: Use the setup wizard ``pepsi-setup --wizard`` runs a short interview, writes a complete and already-validated ``/etc/pepsi/pepsi.conf``, and then offers to run the provisioning of Step 5 for you. Start it:: pepsi-setup --wizard Answer the prompts as follows. Press Enter to accept a ``[default]``; the prompts not listed here can all keep their defaults (they are *no* for this minimal host): * **Mail direction this host serves** — ``inbound`` * **Mail server hostname (MX / EHLO name)** — ``mail.example.org``. The default offered here is the name this host's public address reverse-resolves to; if that is not ``mail.example.org``, fix the ``PTR`` record before going further rather than overriding it here. * **Domain(s) we accept mail for** — ``example.org`` * **Postmaster address** — accept the default (``postmaster@example.org``) * **Public sending IP address(es) for SPF** — ``203.0.113.7 2001:db8::25`` * **PostgreSQL connection string** — accept the default (``postgres:///pepsi``) * **Deliver mail for local system accounts into their Maildir** — ``yes`` * **Also relay non-local recipients to an upstream smarthost** — ``no`` * **Serve an MTA-STS policy over HTTPS** — ``no`` (optional hardening, added later; see :ref:`gs-next-steps`) * **Send and advertise SMTP TLS Reports (TLSRPT)** — ``no`` The wizard validates the result, writes the file, and asks whether to run the full setup now: answer **yes** to fold Step 5 into this step, or **no** to run ``pepsi-setup run`` yourself afterwards. Your answers are recorded in a ``[pepsi-wizard]`` section, so re-running the wizard pre-fills them. The file it writes is the same pipeline as the hand-written one in the other tab, with a few extra production-sensible defaults (``DANE = warn``, message lifetimes). It also delivers to **any** regular login account; add ``TARGETS = alice`` to ``[stage-local]`` to restrict delivery to named accounts. **The same interview without a terminal.** ``pepsi-setup questions`` prints it as JSON — every question's identifier, shape, default and help text — and ``pepsi-setup --wizard --answers answers.json`` answers it from a file of those identifiers. It is the interactive interview with the prompts supplied, not a second implementation, so the branches and the per-field validation are the ones above. That is also the model the browser setup renders; see the "Setting up in a browser" section of :doc:`installation`. .. tab-item:: Edit the file by hand Replace ``/etc/pepsi/pepsi.conf`` with the following:: [pepsi] # Where pepsi-setup writes the generated DKIM/ARC keys. KEY_DIR = /var/pepsi/keys DKIM_SELECTOR = pepsi # Identity that signs the ARC seal recording the inbound verdict. ARC_DOMAIN = example.org # This minimal host does not serve an MTA-STS policy (that needs the # HTTPS server), so do not advertise one. See "Next steps" to add it. MTA_STS_MODE = none [pepsi-postgres] CONFIG = postgres:///pepsi [pepsi-ingress] HOSTNAME = mail.example.org ACCEPTED_DOMAINS = example.org # The MX listener on port 25. MODE = starttls with no TLS_CERT/TLS_KEY # lets pepsi-setup fill in the certbot certificate paths (Step 5). [pepsi-ingress-listener-mx] SERVE = tcp BIND_TO = 0.0.0.0 PORT = 25 MODE = starttls # New messages enter here. ARC-seal the SPF/DKIM/DMARC verdict Pepsi # computed at the boundary, then hand the message to local delivery. [stage-init] PROGRAM = pepsi-stage-arc NEXT_STAGE = local # Deliver to a local user's Maildir. By default any regular login account # (the /etc/login.defs UID range) may receive mail, so 'alice' is already # covered; set TARGETS to restrict delivery to an allow-list (e.g. # TARGETS = alice). Anything not deliverable here is sent to the bounce # path: an unknown recipient via NEXT_STAGE, a mailbox that fails to # accept it via BOUNCE_STAGE. [stage-local] PROGRAM = pepsi-stage-relay-to-maildir SERVER_NAME = mail.example.org NEXT_STAGE = bounce BOUNCE_STAGE = bounce # --- Outbound bounce path: turn an undeliverable message into a DSN and # mail it back to the original sender. Three short stages: build, sign, # send. # Rewrite the message in place into an (unsigned) RFC 3464 bounce # addressed to the original sender, then hand it to the signer. [stage-bounce] PROGRAM = pepsi-stage-bounce SERVER_NAME = mail.example.org NEXT_STAGE = sign # DKIM-sign the bounce so it authenticates at the sender's server. [stage-sign] PROGRAM = pepsi-stage-dkim-sign NEXT_STAGE = internet # Deliver the signed bounce directly to the sender's mail exchanger. [stage-internet] PROGRAM = pepsi-stage-relay-to-internet SERVER_NAME = mail.example.org # Read by pepsi-setup to build the SPF record authorising this host to # send (the bounces above, and any outbound mail you add later). [pepsi-stage-relay-to-smarthost] PUBLIC_IP = 203.0.113.7 2001:db8::25 A message for a recipient that is not a permitted local mailbox is turned into a **bounce** and mailed back to the sender, rather than silently failing: an *unknown* ``@example.org`` recipient takes ``[stage-local]``'s ``NEXT_STAGE`` to the bounce path, and a *known* mailbox that cannot be written (full disk, permissions) is retried and then bounced via ``BOUNCE_STAGE``. Both point at the same ``[stage-bounce]``. Note that ``NEXT_STAGE`` here goes to the **bounce** stage, **not** to ``[stage-internet]``. This host serves only local mailboxes, so *every* recipient it cannot deliver is an unknown user of our own domain, and relaying those onward would look up ``example.org``'s MX, find this very host, and post the mail straight back to us — a loop. Turning this host into a real *forwarder* — where an alias may legitimately expand to an address elsewhere — is a separate step (:ref:`gs-next-steps`); there ``NEXT_STAGE`` does reach a relay, and ``UNKNOWN_MAILBOX_STAGE`` keeps unknown local users on the bounce path (see :doc:`programs/pepsi-stage-relay-to-maildir`). Step 5 — Provision the schema, keys and certificate =================================================== If you used the wizard in Step 4 and answered **yes** to "run setup now", this is already done — re-run the command below at any time to capture the DNS records again (it is idempotent). Otherwise, run the bootstrap tool once, capturing the DNS records it prints:: pepsi-setup -c /etc/pepsi/pepsi.conf run > pepsi-dns.zone This **validates** the configuration, **installs** the ``pepsi`` schema, **generates** the per-domain DKIM/ARC keys under ``KEY_DIR``, **obtains** the TLS certificate for ``mail.example.org`` via ``certbot`` (this needs the ``A`` record from Step 1 to be live and inbound port 80 reachable), and **prints** the remaining DNS records to ``pepsi-dns.zone``. It is idempotent and safe to re-run. If you would rather manage the certificate yourself, pass ``--no-certbot`` and set ``TLS_CERT``/``TLS_KEY`` in the listener section. The full provisioning workflow is in :doc:`programs/pepsi-setup` and :doc:`installation`. .. _gs-publish: Step 6 — Publish the remaining records and verify ================================================= Open ``pepsi-dns.zone`` and publish the records it lists in your zone: * the **DKIM** public key (``pepsi._domainkey.example.org`` TXT), * the **SPF** policy listing this host's sending addresses, and * a **DMARC** policy (``_dmarc.example.org`` TXT). (Only these are printed for the minimal host; an MTA-STS or TLS-reporting record appears here too once you enable those features — see :ref:`gs-next-steps`.) The suggested DMARC policy is ``v=DMARC1; p=none``, which asks receivers to *report* on mail that fails authentication without acting on it. That is the right place to start and the wrong place to stay: add ``rua=mailto:
`` so the aggregate reports reach you, and once they show your own mail passing, tighten ``p=`` to ``quarantine`` and then ``reject``. Publish it by hand, carefully — a DMARC record that does not parse is discarded in silence, and a domain whose policy is one missing ``;`` away from valid looks protected and is not. That is precisely what the ``check`` below is for. Two more blocks appear once this host does **end-to-end cryptography**, and both are about letting correspondents find your users' keys (:doc:`key-management`): * an ``openpgpkey.example.org`` **A/AAAA** record pointing at this host, needed by the Web Key Directory's *advanced* method — the one every current client tries first. ``pepsi-setup`` also asks certbot to cover that name, so publish the record before the next certificate issuance. The *direct* method on the apex works without it, so a missing record is a degradation rather than a breakage; * an ``OPENPGPKEY`` (RFC 7929) or ``SMIMEA`` (RFC 8162) record per published identity. Publish these **only if your zone is DNSSEC-signed**: an unsigned key record is a key from whoever can answer for the zone, which is no improvement on having no key at all. The Web Key Directory needs no DNSSEC, because HTTPS authenticates the domain itself. Once DNS has propagated, check what is live against what Pepsi expects:: pepsi-setup -c /etc/pepsi/pepsi.conf check This is informational (it always exits 0); it flags a missing or mismatched DKIM or SPF record (and MTA-STS, once enabled) so you can fix the zone before relying on it. It also validates the DMARC record strictly and reports one that receivers would ignore as ``[INVALID]``, naming the offending tag — the one failure in this list that produces no bounce, no log line and no complaint from anyone. Step 7 — Start the services =========================== Two long-lived processes run continuously, both as the ``pepsi`` user: * ``pepsi-ingress serve`` — the inbound SMTP server, and * ``pepsi-dispatch serve`` — the stage coordinator (**exactly one** per host). Run each under your platform's service/process manager so they start at boot and restart on failure. The stage programs themselves are spawned by the dispatcher; you never start them by hand. Binding port 25 needs privilege — grant the ingress binary the capability to bind low ports, or use socket activation — see :doc:`installation` and :doc:`programs/pepsi-ingress`. Step 8 — Send the first e-mail ============================== From an account on another provider, send a message to ``alice@example.org`` (or use a tool such as ``swaks --to alice@example.org --server mail.example.org``). Watch it move through the pipeline:: pepsi-status -c /etc/pepsi/pepsi.conf A delivered message is removed from the queue, so a healthy run shows an empty backlog and an incremented delivery counter. Confirm the mail actually landed:: ls /home/alice/Maildir/new/ mutt -f /home/alice/Maildir # or: mail If the file is there, you have received your first e-mail. .. _gs-console: Step 9 — Open the console (optional) ==================================== Everything above is visible in a browser as well. Add an administrative listener bound to loopback:: [pepsi-httpd-listener-admin] SERVE = tcp BIND_TO = 127.0.0.1 PORT = 8443 MODE = plain ADMIN = yes and an account to sign in with — over the local socket a member of the ``pepsi-admin`` group needs none, but a browser cannot open a UNIX socket, so create one and reach the listener through an SSH tunnel:: ssh -L 8443:127.0.0.1:8443 root@mail.example.org Then open ``http://127.0.0.1:8443/ui``. The dashboard shows the same queue ``pepsi-status`` printed, the configuration pages show where each value came from, and the audit log records every change made from any surface. See :doc:`web-ui` for the page reference and :doc:`admin-api` for the API it is a client of. The console performs no service control: restarting a component is still ``systemctl``'s job. Sending mail too: add a submission port ======================================= So far the host only *receives*. To let your own users *send* mail through it from a mail client (Thunderbird, Evolution, KMail, or K-9 Mail on a phone), add an **authenticated submission listener** and an **outbound delivery path**. A client authenticates with a username and password; ``pepsi-ingress`` then records the message with ``state.local_origin = true``, which both allows it to relay to *any* domain (not only ``example.org``) and lets the pipeline tell it apart from inbound mail. Open inbound TCP ports **465** and/or **587** on the VPS firewall, the same way you opened port 25. Route inbound and outbound apart -------------------------------- You already built an outbound delivery path for the bounces — ``[stage-sign]`` → ``[stage-internet]``, with the SPF record covering this host. Authenticated submissions reuse exactly that path; you only need to *route* them onto it. Outbound mail must take a *different* path from inbound mail, and getting this wrong causes a mail loop: if you simply pointed ``[stage-init]`` at ``[stage-sign]``, *inbound* mail would be signed and relayed back out instead of delivered locally. Instead, branch on ``state.local_origin`` at the *entry point* with a :doc:`programs/pepsi-stage-if` stage — locally-originated (authenticated) mail goes to the outbound path, everything else to the inbound path. Crucially, the split must come **before** ``pepsi-stage-arc``: ARC preserves an upstream sender's authentication across your forwarding hop, so it is meaningless for — and must never touch — mail your own users originate (that mail is authenticated by ``[stage-sign]`` instead; ARC-sealing it would only publish the submission's own SPF ``fail``). So make the router the ``[stage-init]`` entry stage and move ARC onto the inbound branch:: # Entry: split by direction FIRST. Authenticated submissions (local_origin = # true) are outbound and reuse the signing path; everything else is inbound and # goes through ARC. This only moves the stage; it never changes the message. [stage-init] PROGRAM = pepsi-stage-if STATE_PATH = local_origin VALUE = true TRUE_STAGE = sign # outbound: our users' mail (reuses the bounce path) FALSE_STAGE = arc # inbound: ARC-seal, then deliver locally # ARC now sits on the inbound branch (received mail only), feeding local # delivery just as [stage-init] used to. [stage-arc] PROGRAM = pepsi-stage-arc NEXT_STAGE = local ``[stage-sign]``, ``[stage-internet]`` and the ``PUBLIC_IP`` line are already in your configuration from the bounce path, so submissions share them unchanged. ``[stage-arc]`` is the former ``[stage-init]`` body (unchanged but renamed), and ``[stage-local]`` is unchanged too — its ``NEXT_STAGE``/``BOUNCE_STAGE`` still bounce mail to unknown local users rather than relaying it anywhere. (The ARC stage also skips any ``state.local_origin`` message on its own, so a misplacement cannot seal a submission — but keeping it off the outbound branch avoids the wasted hop.) .. note:: The outbound path needs *outbound* port 25 (see *Prerequisites*), which budget providers often block. If yours is blocked, deliver through an upstream **smarthost** instead: replace ``[stage-internet]`` with a ``pepsi-stage-relay-to-smarthost`` stage and define the smarthost — see :doc:`programs/pepsi-stage-relay-to-smarthost`. To return non-delivery reports when an MX refuses your users' *outgoing* mail, give ``[stage-internet]`` a ``BOUNCE_STAGE = bounce`` as well. Provide a password backend (Dovecot) ------------------------------------- A submission listener needs a way to check passwords. Pepsi keeps no password database of its own; it verifies credentials through a **Dovecot** SASL socket. Install Dovecot and configure it to authenticate your accounts (the system users you created, or virtual users). Pepsi's inbound server runs as the unprivileged ``pepsi-ingress`` user, which cannot open Dovecot's shared, root-only ``auth-client`` socket, so give it a **dedicated** listener it owns — in ``/etc/dovecot/conf.d/10-pepsi.conf``:: service auth { unix_listener auth-client-pepsi { mode = 0600 user = pepsi-ingress } } You do not have to write this by hand: ``pepsi-setup run`` detects an unreachable socket and offers to install exactly this drop-in (validating it with ``doveconf`` and reloading Dovecot), or prints it if it cannot. Dovecot is also what you would add to let clients *read* their Maildirs over IMAP (:ref:`gs-next-steps`), so it earns its keep twice. Its full configuration is beyond this guide; see the Dovecot documentation. Add the submission listeners ---------------------------- Add an implicit-TLS listener on 465 (recommended for clients) and/or a STARTTLS listener on 587. ``SUBMISSION = yes`` makes authentication mandatory and applies the RFC 6409 fixups (a missing ``Date:``/``Message-ID:`` is added); ``SASL_TYPE``/``SASL_PATH`` wire in Dovecot. Authentication is only ever offered over TLS. As with the MX listener, leaving ``TLS_CERT``/``TLS_KEY`` unset lets ``pepsi-setup`` install the ``mail.example.org`` certificate automatically:: # Port 465: implicit TLS. [pepsi-ingress-listener-submissions] SERVE = tcp BIND_TO = 0.0.0.0 PORT = 465 MODE = tls SUBMISSION = yes SASL_TYPE = dovecot SASL_PATH = /run/dovecot/auth-client-pepsi # Port 587: cleartext upgraded with STARTTLS. [pepsi-ingress-listener-submission] SERVE = tcp BIND_TO = 0.0.0.0 PORT = 587 MODE = starttls SUBMISSION = yes SASL_TYPE = dovecot SASL_PATH = /run/dovecot/auth-client-pepsi Re-run the bootstrap so the new listeners' certificates and the updated SPF record are picked up, then restart the services:: pepsi-setup -c /etc/pepsi/pepsi.conf run > pepsi-dns.zone Publish the updated records (the SPF record now lists this host) and re-verify with ``pepsi-setup -c /etc/pepsi/pepsi.conf check``. Connect a mail client --------------------- In the client's account settings, configure the **outgoing (SMTP)** server: * **Server / host:** ``mail.example.org`` * **Port / security:** ``465`` with implicit TLS (SSL/TLS), or ``587`` with STARTTLS * **Authentication:** normal password * **Username / password:** the account Dovecot expects (e.g. ``alice`` or ``alice@example.org``) and its password If you also set up Dovecot for IMAP, point the **incoming** server at the same host (IMAP, port 993, TLS). Send a test message to an address at another provider, confirm it left the queue:: pepsi-status -c /etc/pepsi/pepsi.conf and check that it arrived. Outgoing deliverability depends on the same DNS hygiene as receiving — correct PTR/reverse DNS, the SPF record listing this host, and DKIM signing (all configured above); a brand-new IP is sometimes greylisted on first contact, so a short delay on the very first message is normal. Troubleshooting =============== Nothing arrives Check the ``MX`` record resolves to ``mail.example.org`` and that record's ``A``/``AAAA`` points at the VPS; confirm inbound port 25 is open (test from another host); confirm the PTR/reverse-DNS is set. Watch the ingress log. A message is stuck or ``failed`` Inspect it with :doc:`programs/pepsi-status` and :doc:`programs/pepsi-queue`. A recipient that is not in ``TARGETS`` or has no account is now *bounced* back to the sender (watch ``[stage-bounce]`` and ``[stage-internet]``), not left in the queue; a ``timeout``/paused row often means a filesystem problem (e.g. a full disk) in the recipient's home, retried before it bounces. A bounce that itself sticks usually means *outbound* port 25 is blocked (see below). The certificate step failed ``certbot`` needs ``mail.example.org`` to already resolve to this VPS and inbound port 80 reachable. Fix DNS/port 80 and re-run ``pepsi-setup … run``, or use ``--no-certbot`` and supply ``TLS_CERT``/``TLS_KEY`` yourself. "Permission denied" on delivery The privilege bits from Step 3 are wrong: the helper must be ``4750 root:pepsi-maildir`` and the relay-to-maildir stage ``2755`` setgid ``pepsi-maildir``. Re-run ``make install`` as root with the group present, or set them by hand. A client cannot authenticate or send Authentication is offered only over TLS — use port 465 (implicit TLS) or 587 with STARTTLS and "normal password". A ``530`` reply means the session was unauthenticated; check that the Dovecot auth socket (``SASL_PATH``) is readable by the ``pepsi`` user and that Dovecot accepts the credentials. Outgoing mail is stuck Direct internet delivery needs *outbound* port 25; if the provider blocks it, switch ``[stage-internet]`` to a smarthost (see the note above). Inspect stuck rows with :doc:`programs/pepsi-status` and :doc:`programs/pepsi-queue`. .. _gs-next-steps: Next steps ========== * **Read mail remotely.** Add an IMAP server (e.g. Dovecot) pointed at the same ``~/Maildir`` so mail clients can fetch it; Pepsi handles only delivery into the Maildir. If you already added Dovecot for submission authentication above, enabling IMAP is a small additional step. * **Turn this into a forwarder.** Insert :doc:`programs/pepsi-stage-srs` on the *inbound* path so mail re-sent to a third party passes SPF, re-using the outbound relay (``[stage-sign]`` → ``[stage-internet]``) already in this configuration — Pepsi's core purpose (:doc:`introduction`). * **Customise the bounces.** The bounce path is already wired in; you can localise or brand the DSN text with a ``BOUNCE_MESSAGE`` template — see :doc:`programs/pepsi-stage-bounce`. * **Harden inbound TLS (MTA-STS + TLS reporting).** Tell other senders to use validated TLS toward your MX by serving an **MTA-STS** policy, and collect **TLS Reports** (RFC 8460). Both need the HTTPS server (:doc:`programs/pepsi-httpd`) running and a certificate for each ``mta-sts.``; the easiest way to add them is to re-run ``pepsi-setup --wizard`` and answer **yes** to the two transport-security questions (it pre-fills your earlier answers). * **Filter and protect.** Layer in language blocking, sender whitelisting or the pay-to-send paywall — see :doc:`features` and the per-stage chapters. * **Front an existing Exchange or Microsoft 365 deployment.** Pepsi can sit in front of and behind Exchange as a transparent crypto gateway, so users get S/MIME and OpenPGP without touching their clients. That is a different topology from the one above — mail is routed to Exchange rather than delivered locally, which makes loop prevention mandatory rather than advisable — and it has its own chapter: :doc:`exchange`.