This commit is contained in:
Paul Lützner committed 2026-07-28 00:00:03 +02:00
1 parent 8e1a365eae
commit 3796e0e80a
5 files changed
+37 -15

No files matched your search

+3
View File
@@ -101,6 +101,9 @@ JWT_PASSPHRASE=
# 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 service instead (see truenas.compose.yaml x-config). 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=
+2 -2
View File
@@ -1515,8 +1515,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }
* @psalm-type MercureConfig = array{
* hubs?: array<string, array{ // Default: []
* url?: scalar|Param|null, // URL of the hub's publish endpoint // Default: null
* public_url?: scalar|Param|null, // URL of the hub's public endpoint
* url?: scalar|Param|null, // URL of the hub's publish endpoint
* public_url?: scalar|Param|null, // URL of the hub's public endpoint // Default: null
* jwt?: string|array{ // JSON Web Token configuration.
* value?: scalar|Param|null, // JSON Web Token to use to publish to this hub.
* provider?: scalar|Param|null, // The ID of a service to call to provide the JSON Web Token.
+9 -1
View File
@@ -34,7 +34,7 @@ final class GenerateVapidKeysCommand extends Command
$keys = VAPID::createVapidKeys();
$io->section('Add to .env.local (never to .env — it is tracked)');
$io->section('Local development — add to .env.local (never .env, it is tracked)');
$io->writeln('###> plmail/web-push ###');
$io->writeln('VAPID_SUBJECT=mailto:you@example.com');
$io->writeln('VAPID_PUBLIC_KEY='.$keys['publicKey']);
@@ -47,6 +47,14 @@ final class GenerateVapidKeysCommand extends Command
'Devices bind to the public key at registration and must re-register.',
]);
$io->section('Container deployment — set as environment variables instead');
$io->writeln('There is no .env.local to edit in a container image. Pass these as env');
$io->writeln('vars on the php, messenger-worker and imap-supervisor services; a real');
$io->writeln('environment variable overrides the .env.local.php that "composer');
$io->writeln('dump-env prod" bakes into the image. On TrueNAS they live in the');
$io->writeln('x-config block of truenas.compose.yaml.');
$io->newLine();
$io->note('VAPID_SUBJECT must be a mailto: or https: URL identifying you, per RFC 8292.');
return Command::SUCCESS;
+8
View File
@@ -193,6 +193,14 @@ supplied URL, and the subscription receives nothing until the client echoes the
code back. plMail's own PWA is not exempt — `public/sw.js` posts the code to
`/settings/push/verify`.
VAPID keys come from `VAPID_SUBJECT` / `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY`
(`app:push:generate-vapid-keys` mints a pair). Locally they go in `.env.local`;
in a container there is no such file, so they are set as environment variables —
a real env var overrides the `.env.local.php` baked in by `composer dump-env
prod`. Blank keys disable push cleanly: `WebPushSender::isConfigured()` returns
false, the Session advertises an empty `vapidPublicKey`, and the settings UI
hides the toggle.
`StateManager` collapses dirty `(account, type)` pairs in memory and
`JmapPushSubscriber` drains them once per request (`kernel.terminate`) or per
worker message, so a sync importing 50 messages sends **one** notification.
+15 -12
View File
@@ -30,6 +30,18 @@ x-config:
# -- Public origin (your reverse-proxy hostname) --
mercure_public_url: &mercure_public_url "https://mail.yourdomain.com/.well-known/mercure"
# -- Web Push (browser/PWA notifications + JMAP PushSubscription) --
# Generate a keypair once, on any machine with the image:
# docker run --rm ghcr.io/karatektus/pl_mail:latest php bin/console app:push:generate-vapid-keys
# then paste the two keys below. Leave them blank to disable push entirely —
# the app detects that and hides the notification toggle.
# Rotating them invalidates every existing subscription; every device has to
# re-enable notifications afterwards.
# vapid_subject must be a mailto: or https: URL identifying you (RFC 8292).
vapid_subject: &vapid_subject "mailto:you@yourdomain.com"
vapid_public_key: &vapid_public_key ""
vapid_private_key: &vapid_private_key ""
# -- Gmail (leave blank for IMAP-only) --
google_client_id: &google_client_id ""
google_client_secret: &google_client_secret ""
@@ -47,18 +59,6 @@ x-config:
path_caddy_config: &path_caddy_config /mnt/yourPool/yourDataSet/caddy-config
path_mercure_data: &path_mercure_data /mnt/yourPool/yourDataSet/mercure-data
path_mercure_config: &path_mercure_config /mnt/yourPool/yourDataSet/mercure-config
# Mail data written by the workers and READ BY THE WEB CONTAINER. All three
# services must bind the same host paths or downloads 404: the sync workers
# write attachments and raw messages, the web container serves them.
#
# These may be subdirectories of a single dataset — Docker creates any that
# do not exist, owned by root, which is what the container runs as. No
# chown needed.
#
# ZFS uses NFSv4 ACLs, so the entrypoint's setfacl on var/ cannot apply here
# and logs "Operation not supported". That is expected and harmless; the
# entrypoint no longer treats it as fatal (see frankenphp/docker-entrypoint.sh).
# It does mean this fix only takes effect once the image is rebuilt.
path_attachments: &path_attachments /mnt/yourPool/yourDataSet/attachments
path_raw: &path_raw /mnt/yourPool/yourDataSet/raw
path_uploads: &path_uploads /mnt/yourPool/yourDataSet/uploads
@@ -73,6 +73,9 @@ x-app-env: &app-env
MESSENGER_TRANSPORT_DSN: "doctrine://default?auto_setup=0"
MERCURE_URL: "http://mercure/.well-known/mercure"
MERCURE_PUBLIC_URL: *mercure_public_url
VAPID_SUBJECT: *vapid_subject
VAPID_PUBLIC_KEY: *vapid_public_key
VAPID_PRIVATE_KEY: *vapid_private_key
MERCURE_JWT_SECRET: *mercure_jwt
GOOGLE_OAUTH_CLIENT_ID: *google_client_id
GOOGLE_OAUTH_CLIENT_SECRET: *google_client_secret