mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
* Single sign-on only: hide the password form and refuse password sign-ins Adds Admin -> SSO 'Single sign-on only' (and OIDC_DISABLE_PASSWORD_LOGIN). The login page drops the password form and LoginFormAuthenticator refuses a password sign-in before looking the user up. It only applies while a whole provider is in force, the saving administrator must already be linked to the provider, and app:sso:allow-password-login brings the form back from the console. Config restore leaves it off. Off by default; one boolean column on oidc_config. Closes #169 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Single sign-on only: require a link to the provider being saved The admin form let the box be ticked by an administrator linked to any provider, including the one the page held before. Switching providers and ticking it in one save made a provider they had never used the only way in. It now compares the stored issuer with the one on the page, without a trailing slash, as OidcDiscovery does. Tests for what had none: the console command (clears the setting, keeps the provider), a restore over a row that had it on, the link to another provider, and a control showing the same password signs in with the setting off. The "right credentials" test now posts the right password. Docs: there is no iOS app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: put the iOS app back in the single sign-on page The previous commit dropped it on the wrong assumption that there is no iOS app. There is one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Paul Lützner <karatektus@gmail.com>
289 lines
15 KiB
Bash
289 lines
15 KiB
Bash
# In all environments, the following files are loaded if they exist,
|
|
# the latter taking precedence over the former:
|
|
#
|
|
# * .env contains default values for the environment variables needed by the app
|
|
# * .env.local uncommitted file with local overrides
|
|
# * .env.$APP_ENV committed environment-specific defaults
|
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
|
#
|
|
# Real environment variables win over .env files.
|
|
#
|
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
|
# https://symfony.com/doc/current/configuration/secrets.html
|
|
#
|
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
|
|
|
###> symfony/framework-bundle ###
|
|
APP_ENV=dev
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# READ THIS BEFORE PUTTING A SECRET IN THIS FILE.
|
|
#
|
|
# docker compose reads .env — this file — to fill in ${VAR} in compose.yaml.
|
|
# That makes anything set here an environment variable inside every container,
|
|
# which in turn tells the entrypoint "the operator supplied this" and switches
|
|
# off the first-run generation for it. A committed value here is therefore a
|
|
# secret shared by every plMail install in existence.
|
|
#
|
|
# So the three that matter are blank, and generated on first start into
|
|
# var/secrets/generated.env. Fill one in only if you mean to manage it
|
|
# yourself — in your shell, or here — and know that it then applies to every
|
|
# service.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
# Signs remember-me cookies, among other things; changing it logs everyone out.
|
|
APP_SECRET=
|
|
APP_SHARE_DIR=var/share
|
|
|
|
# Where the generated secrets live. Must be on a volume EVERY plMail service can
|
|
# see; one that cannot read it mints its own key and stops being able to read the
|
|
# others' data, which `app:secrets:init` detects and refuses to start on. Back
|
|
# this up: losing APP_ENCRYPTION_KEY makes every stored mail credential
|
|
# permanently unreadable.
|
|
APP_SECRETS_FILE=var/secrets/generated.env
|
|
|
|
# Where blobs live, relative to the project root: attachments, raw messages,
|
|
# uploads and avatars all sit under it. One knob so a deployment can put the
|
|
# whole lot on a single mount, as the TrueNAS app does with var/data.
|
|
# Attachment paths are stored relative to the project root, so changing
|
|
# this on an install with mail in it orphans what is already there.
|
|
APP_STORAGE_DIR=var
|
|
# The address plMail is reached at from outside — what Google and Microsoft
|
|
# call back to with push notifications. Blank on purpose: it is asked for on the
|
|
# setup screen and written to var/secrets/generated.env, because a worker
|
|
# building a push subscription has no request to infer a hostname from. Set it
|
|
# here or in the environment to manage it yourself.
|
|
APP_PUBLIC_URL=
|
|
# The clock a user who has never picked one in Settings is shown. Timestamps
|
|
# are stored in UTC and only ever converted for display, so changing this
|
|
# re-renders existing mail rather than rewriting it. NOT the container's PHP
|
|
# timezone, which is pinned to UTC on purpose — an install left on UTC shows
|
|
# every European user times that are plausibly wrong by their own offset.
|
|
APP_DEFAULT_TIMEZONE=Europe/Berlin
|
|
###< symfony/framework-bundle ###
|
|
|
|
###> symfony/routing ###
|
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
|
DEFAULT_URI=http://localhost
|
|
###< symfony/routing ###
|
|
|
|
###> doctrine/doctrine-bundle ###
|
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
|
#
|
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
|
# No password on purpose — see the note at the top of this file. The entrypoint
|
|
# builds the real one from the generated POSTGRES_PASSWORD and exports it, which
|
|
# wins over this.
|
|
#
|
|
# Not blank, though: Doctrine reads the driver out of the scheme, and an empty
|
|
# DSN has none — which fails the prod cache warmup during the Docker build with
|
|
# "could not find driver". A DSN carrying no credentials is not a secret.
|
|
DATABASE_URL="postgresql://app@database:5432/app?serverVersion=18&charset=utf8"
|
|
###< doctrine/doctrine-bundle ###
|
|
|
|
###> symfony/messenger ###
|
|
# Choose one of the transports below
|
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
|
# The `bulk` queue only, if it should live somewhere other than the line above.
|
|
# Unset it and it follows MESSENGER_TRANSPORT_DSN, which is what every real
|
|
# deployment wants; it exists so the browser suite can make that one queue a
|
|
# real, consumed one while the rest stay in memory (see compose.test.yaml).
|
|
# MESSENGER_BULK_DSN=doctrine://default?auto_setup=0
|
|
###< symfony/messenger ###
|
|
|
|
###> symfony/mailer ###
|
|
MAILER_DSN=null://null
|
|
###< symfony/mailer ###
|
|
|
|
###> app/encryption ###
|
|
# Base64-encoded 32-byte libsodium secretbox key used by the `encrypted_string`
|
|
# Doctrine type to encrypt account credentials at rest. SEPARATE from APP_SECRET.
|
|
#
|
|
# Blank on purpose — see the note at the top of this file. Generated on first
|
|
# start, and worth backing up: it is the only thing that can read the mailbox
|
|
# passwords and OAuth tokens in your database.
|
|
APP_ENCRYPTION_KEY=
|
|
###< app/encryption ###
|
|
APP_DEV_USER_EMAIL=
|
|
APP_DEV_USER_PASSWORD=
|
|
|
|
###> app/demo ###
|
|
# Turns the whole instance into a public demo: every visitor is handed a
|
|
# throwaway user with a seeded mailbox, sending is swallowed and answered by a
|
|
# script, and a button delivers scripted mail on demand.
|
|
#
|
|
# Instance-wide rather than per-user on purpose. Demo mode's promise is that
|
|
# nothing here reaches the network, and that is only checkable if there is one
|
|
# switch — a per-account flag would mean auditing every outbound path for
|
|
# whether it honoured it. With this on, mail sync, push renewal, calendar sync
|
|
# and real account creation are all off for everybody.
|
|
#
|
|
# NEVER set this on an instance holding real mail. See docs/demo-mode.md.
|
|
APP_DEMO_MODE=0
|
|
# How long a visitor's throwaway user lives before app:demo:reap removes it.
|
|
APP_DEMO_TTL=PT2H
|
|
|
|
# Impressum / legal notice, linked from the login page in demo mode.
|
|
#
|
|
# A public demo hosted in Germany needs one (§ 5 TMG), and it has to name a real
|
|
# person or company — so these are blank here and the page says loudly that they
|
|
# are unset rather than inventing a plausible-looking operator. Nobody but the
|
|
# operator can fill these in.
|
|
#
|
|
# Ignored entirely when APP_DEMO_MODE is off: the route does not exist there.
|
|
APP_DEMO_IMPRESSUM_NAME=
|
|
APP_DEMO_IMPRESSUM_ADDRESS=
|
|
APP_DEMO_IMPRESSUM_EMAIL=
|
|
# Named in the privacy notice as the processor running the server. Blank shows
|
|
# a warning on the page rather than a guess — see APP_DEMO_IMPRESSUM_NAME.
|
|
APP_DEMO_PRIVACY_HOST=
|
|
###< app/demo ###
|
|
|
|
###> symfony/mercure-bundle ###
|
|
# See https://symfony.com/doc/current/mercure.html#configuration
|
|
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
|
|
MERCURE_URL=http://mercure/.well-known/mercure
|
|
MERCURE_PUBLIC_URL=https://localhost/.well-known/mercure
|
|
# Blank on purpose — see the note at the top of this file. Generated on first
|
|
# start, and read by both the app and the hub container from the same file.
|
|
MERCURE_JWT_SECRET=
|
|
###< symfony/mercure-bundle ###
|
|
|
|
###> google oauth ###
|
|
GOOGLE_OAUTH_CLIENT_ID=
|
|
GOOGLE_OAUTH_CLIENT_SECRET=
|
|
GMAIL_PUBSUB_TOPIC=projects/your-project-id/topics/gmail-push
|
|
GMAIL_PUBSUB_VERIFICATION_TOKEN=
|
|
###< google oauth ###
|
|
|
|
###> microsoft/graph ###
|
|
MICROSOFT_OAUTH_CLIENT_ID=
|
|
MICROSOFT_OAUTH_CLIENT_SECRET=
|
|
# common | organizations | consumers | <tenant-guid>
|
|
# Must match the app registration's supported-account-types, or consent fails
|
|
# with AADSTS50194. `organizations` avoids the consumer-account edge cases
|
|
# (no immutable ids, reduced $filter support) if you don't need outlook.com.
|
|
MICROSOFT_OAUTH_TENANT=common
|
|
###< microsoft/graph ###
|
|
|
|
###> plmail/oidc ###
|
|
# Single sign-on through an OpenID Connect provider — Authentik, Keycloak,
|
|
# Authelia and the like. Set up under Admin → SSO; these are the fallback, read
|
|
# only while no complete provider is saved there. Off while any of the first
|
|
# three is blank and nothing is saved; otherwise the login page offers a "Sign
|
|
# in with …" button beside the password form. Register this redirect URI at the
|
|
# provider:
|
|
# https://<your plMail address>/login/oidc/callback
|
|
#
|
|
# The issuer is the provider's, exactly as its discovery document states it —
|
|
# in Authentik, the "OpenID Configuration Issuer" on the provider's page, e.g.
|
|
# https://auth.example.com/application/o/plmail/ . It must be https.
|
|
OIDC_ISSUER=
|
|
OIDC_CLIENT_ID=
|
|
OIDC_CLIENT_SECRET=
|
|
# What the button calls the provider ("Sign in with Authentik"). Blank says
|
|
# "single sign-on".
|
|
OIDC_BUTTON_LABEL=
|
|
# Space-separated; `openid` is always added. `email` is what lets a first
|
|
# sign-in find the account it belongs to.
|
|
OIDC_SCOPES="openid email profile"
|
|
# Create an account for somebody the provider lets in and plMail has never
|
|
# seen. Off, an administrator creates the account first, with the same address.
|
|
OIDC_AUTO_CREATE=false
|
|
# Link an existing account by address only when the provider says it verified
|
|
# that address (`email_verified`). Turning this off trusts the provider's
|
|
# address blindly: whoever can edit their own address there can claim any
|
|
# account here that has not signed in through the provider yet. Only `false`,
|
|
# `0`, `no` or `off` turn it off; blank or anything else leaves it on.
|
|
OIDC_REQUIRE_VERIFIED_EMAIL=true
|
|
# Make single sign-on the only way in: the login page shows no password form and
|
|
# a password sign-in is refused. Ignored while no provider is configured above
|
|
# (or under Admin → SSO), so it cannot lock an install out of a provider that
|
|
# is not there. Only `true`, `1`, `yes` or `on` turn it on. Switching it off
|
|
# again in an emergency: `bin/console app:sso:allow-password-login`.
|
|
OIDC_DISABLE_PASSWORD_LOGIN=false
|
|
###< plmail/oidc ###
|
|
|
|
APP_DB_LOG_LEVEL=warning
|
|
APP_CONTAINER_NAME=web
|
|
|
|
# WIDE ON PURPOSE, AND WORTH NARROWING. Loopback plus every private range,
|
|
# because a reverse proxy on the same Docker network or LAN has an address in
|
|
# one of them that cannot be known here — and a proxy that is NOT in this list
|
|
# breaks sign-in with OAuth, shares one login allowance between everybody and
|
|
# strips `secure` off cookies, none of which says why.
|
|
#
|
|
# The cost of the width: anything else with a private address that can reach
|
|
# plMail directly — another machine on the LAN, another container — is believed
|
|
# when it sends X-Forwarded-For, and can pose as any address it likes. That
|
|
# gets it past the PER-ADDRESS limits on the login form and the public booking
|
|
# page. It does not get it past the per-account login limit, which ignores the
|
|
# address. Once your proxy works, replace this with the proxy's own address or
|
|
# subnet; docs/install/reverse-proxy.md says how to find it.
|
|
TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
|
|
|
###> lexik/jwt-authentication-bundle ###
|
|
# Beside the generated secrets rather than in config/jwt: the keys are not in
|
|
# the image (they are gitignored), so each service would otherwise generate its
|
|
# own and reject tokens the others signed. Generated by `app:secrets:init`.
|
|
#
|
|
# Deliberately empty: config/bootstrap_generated_secrets.php puts both in a
|
|
# jwt/ directory next to APP_SECRETS_FILE, so they follow the secrets onto
|
|
# whichever volume a deployment keeps those on. Set either to override.
|
|
JWT_SECRET_KEY=
|
|
JWT_PUBLIC_KEY=
|
|
# Real passphrase lives in .env.local (untracked); config/jwt/*.pem is gitignored.
|
|
JWT_PASSPHRASE=
|
|
###< lexik/jwt-authentication-bundle ###
|
|
|
|
###> plmail/web-push ###
|
|
# VAPID keypair for JMAP PushSubscription + browser Web Push (RFC 8292).
|
|
# Generate with: bin/console app:push:generate-vapid-keys
|
|
# The PRIVATE key belongs in .env.local (untracked) — never here.
|
|
# In a container deployment there is no .env.local: set these as environment
|
|
# variables on the php and worker services instead. A real
|
|
# env var overrides whatever "composer dump-env prod" baked into the image.
|
|
VAPID_SUBJECT=mailto:admin@example.com
|
|
VAPID_PUBLIC_KEY=
|
|
VAPID_PRIVATE_KEY=
|
|
# Comma-separated hosts a push endpoint may point at even though they resolve
|
|
# to a private address. Every other private, loopback or link-local endpoint is
|
|
# refused, because the server POSTs to it on every change. Set it to your
|
|
# SERVER_NAME if you run the bundled ntfy (compose --profile push) on a LAN or
|
|
# Tailscale address, e.g. "192.168.1.10" or "mail.lan".
|
|
PUSH_ALLOWED_HOSTS=
|
|
###< plmail/web-push ###
|
|
|
|
###> plmail/integrations ###
|
|
# Integration provider credentials live in the database, not here — admins edit
|
|
# them under /admin?section=integrations. These two are the SSRF guard rails
|
|
# around the server address a user may type in for a self-hosted service.
|
|
#
|
|
# Self-hosted Nextcloud and Immich on a LAN often have no certificate, so
|
|
# allowing plain http is common. It is off by default so that sending an app
|
|
# password in the clear stays a deliberate choice rather than an accident.
|
|
INTEGRATIONS_ALLOW_HTTP=false
|
|
# Comma-separated hosts exempt from the private-range block, for reaching a
|
|
# service on the same Docker network or LAN, e.g. "nextcloud.lan,10.0.0.5".
|
|
# Everything else resolving into loopback, link-local or RFC1918 is refused —
|
|
# that block is what stops a user aiming plMail at the database container or
|
|
# at 169.254.169.254. It is checked on every request and every redirect, not
|
|
# only when the address is saved. A server address an admin pins under
|
|
# Admin → Integrations is exempt the same way, without being listed here.
|
|
INTEGRATIONS_ALLOWED_HOSTS=
|
|
###< plmail/integrations ###
|
|
|
|
###> plmail/whats-new ###
|
|
# Where "Send feedback" in the What's new dialog mails to. The mail leaves
|
|
# through the user's own account, like everything plMail sends, so this is only
|
|
# the recipient. Point it at your own address to collect the feedback of the
|
|
# people on your install instead of sending it to plMail's developer.
|
|
APP_FEEDBACK_TO=feedback@pluetzner.de
|
|
###< plmail/whats-new ###
|