{
        # Five seconds to finish what is in flight when told to stop, then the
        # connections are closed. Caddy's default is to wait for every one, and
        # this server proxies the live-update stream, which never ends by
        # itself: with one browser tab open, "Restart now" in the admin panel
        # stopped PHP and then sat there, neither serving nor exiting, until
        # the tab was closed — and compose cannot restart a container that has
        # not exited. `docker stop` only ever got out of it by killing the
        # process after its own timeout. The hub has the same five seconds for
        # the same reason (BackgroundProcesses); browsers reconnect.
        grace_period 5s

        # The certificate a browser gets when it asked for a name this server
        # holds none for — `default_sni` for a handshake that names nothing,
        # which is every one to an IP address, `fallback_sni` for a name
        # nobody listed, such as the machine's own hostname on a LAN.
        #
        # Without them Caddy has nothing to offer and ends the handshake. The
        # browser reports that as a protocol error, not a certificate error,
        # and a protocol error has no "proceed anyway": an install on a
        # headless machine could not be opened from any other machine, and the
        # README's "open https://localhost" was the only address that worked.
        # With them the answer is the self-signed `localhost` certificate and
        # the warning every browser lets a person click through.
        #
        # Inert wherever there is no such certificate to hand out: an install
        # serving a real domain never issues one, and SERVER_NAME ":80" does
        # no TLS at all. compose.yaml's default SERVER_NAME is the other half.
        default_sni localhost
        fallback_sni localhost

        {$CADDY_GLOBAL_OPTIONS}
        frankenphp {
                {$FRANKENPHP_CONFIG}
        }
}
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
        log {
                format filter {
                        wrap console
                        fields {
                                uri query {
                                        replace authorization REDACTED
                                }
                        }
                }
        }

        root public/

        encode zstd br gzip

        # The headers a plMail install gets whether or not anyone put a proxy in
        # front of it.
        #
        # None of these existed anywhere in the repository — not here, not in
        # config/, not in a listener — so the ones a running instance showed
        # were its operator's reverse proxy, and the README's own install
        # instructions are `docker compose up -d` and "open https://localhost".
        # An instance set up the documented way ran with no clickjacking
        # defence, no HSTS and a full Referer going out to every link a mail
        # points at. The operator had nothing telling them to fix that.
        #
        # `defer` so an application response that sets one of these itself wins:
        # the attachment and image-proxy routes send their own, stricter,
        # per-response policies and must not have them overwritten from here.
        header {
                defer

                # Two years, and includeSubDomains, is the usual answer. It is
                # only sent over HTTPS — Caddy omits it on plaintext — so an
                # instance reached over http on a LAN is unaffected rather than
                # locked out of its own hostname.
                Strict-Transport-Security "max-age=63072000; includeSubDomains"

                # SAMEORIGIN rather than DENY: the reading frame is our own
                # document embedding our own route, and DENY would break it.
                X-Frame-Options "SAMEORIGIN"
                X-Content-Type-Options "nosniff"

                # A mail client sends its users to other people's links all day.
                # Without this the full URL of the page they were on goes with
                # them; with it, the origin does, and only to https.
                Referrer-Policy "strict-origin-when-cross-origin"

                # Nothing here asks for any of them, and a compromised page
                # should not be able to start.
                Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), interest-cohort=()"

                # The version banner. Not a vulnerability, just free
                # reconnaissance.
                -Server
        }

        # Mercure hub — proxied to the container that runs it. `mercure` is the
        # name plMail's own compose files give it; MERCURE_UPSTREAM is for a
        # stack that names its containers by rules of its own.
        #
        # The subscriber cookie loses its `__Secure-` prefix on the way. The
        # browser holds the prefixed one on an https install and the bare one
        # otherwise, and which it is follows the public address, which can
        # change under a running hub. So the hub always reads the bare name
        # and this is where the two are made the same; see BackgroundProcesses.
        handle /.well-known/mercure* {
                reverse_proxy {$MERCURE_UPSTREAM:mercure:80} {
                        header_up Cookie "__Secure-mercure_access_token=" "mercure_access_token="
                }
        }

        php_server {
                try_files {path} index.php
        }
}
