Files
pl_mail/CHANGELOG.md
T
Paul LütznerandClaude Opus 5 9833695a27 Add /healthz and app:backup
Two gaps that only matter on the day something goes wrong, which is the
worst day to discover them.

Every compose service but Mercure had `healthcheck: disable: true`, and
the image's own HEALTHCHECK probed Caddy's metrics port — which answers
as soon as the web server is listening, well before PHP can reach the
database. A stack whose database was unreachable reported itself healthy,
and `depends_on: service_healthy` waited for nothing worth waiting for.
/healthz answers the question that was actually being asked, and the
image now probes it.

It is unauthenticated, because a probe needing a session is useless to
Docker and to every monitoring tool, and therefore deliberately vague: it
returns verdicts and nothing else. HealthTest asserts that shape, so a
well-meant "add the counts, they're useful" fails the suite rather than
quietly turning a public endpoint into a window on the instance.

app:backup makes the README's oldest warning actionable. It has always
said to back up the database and the encryption key separately and then
left the reader to work out how — the most dangerous instruction in the
project to get wrong, because both halves fail silently. A dump alone
restores every mailbox with credentials nothing can decrypt.

The command checks rather than assumes: on an install that supplies
APP_ENCRYPTION_KEY from the environment there is nothing to copy, so it
says the key is *not* in the backup instead of printing a reassuring
warning about a file that does not contain it. That case is the one where
a silent success would be worst.

pg_dump comes from PGDG pinned to the server's major version. Debian's
postgresql-client trails 18 and pg_dump refuses to dump a newer server,
so the distro package would install cleanly and fail at the only moment
it was ever needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:36:37 +02:00

4.2 KiB

Changelog

Notable changes per release. plMail runs its migrations automatically on boot, so anything that changes the schema irreversibly is called out explicitly.

The published image tags: latest follows the most recent release below, main follows the tip of the default branch, and sha-… pins one commit.

Unreleased

Added

  • Snooze. Put a conversation away and have it come back later — from the message row, the list toolbar, or a JMAP client via the Thread/set extension. Snoozed conversations get their own sidebar entry and view. app:mail:wake-snoozed runs every minute to bring them back.
  • JMAP draft attachments. Email/set now accepts uploaded blobs in attachments, per RFC 8621; drafts created over JMAP previously dropped them silently.
  • Login throttling. Five attempts per account per fifteen minutes on the password form, and a separate limit on the two-factor code form — which the firewall's throttling does not cover, and which is the form an attacker reaches holding a stolen password.
  • Admin user management. plMail's data model has always been multi-user, but there was no way to create the second user: the setup wizard and app:setup both make the first one and then refuse. Admin → Users adds, edits, promotes and removes people. An administrator deliberately cannot change an existing user's password or remove anyone's second factor — both would make an admin session a way into someone else's mailbox.
  • GET /healthz — reports database, queue and worker health without a session, so Docker healthchecks and uptime monitors can use it. The image's healthcheck now points here instead of at Caddy's metrics port, which answered before PHP could reach the database.
  • app:backup — one command for the database dump, the stored files and the generated secrets, and it says explicitly when APP_ENCRYPTION_KEY is not in the backup because the install supplies it from the environment.
  • PHPStan at level 5, with a baseline, in CI. Dependabot for composer, npm and GitHub Actions.
  • LICENSE. The project has always said AGPL-3.0 in its README; the licence text is now actually distributed with it, and composer.json no longer contradicts it by claiming "proprietary".

Fixed

  • Snoozing from the web UI did nothing but set a timer. No labels moved and nothing propagated, so the conversation stayed in the inbox — locally and at the provider — while its row vanished from the list. The sweep would then "wake" a thread that had never left.
  • The snooze button on a message row cleared a snooze instead of setting one, and the toolbar's snoozed everything to a hardcoded 8am tomorrow.
  • "Test connection" in account settings crashed with an ArgumentCountError whenever the form was incomplete or the password blank — the two cases it exists to report.
  • The admin Graph category sync caught an exception class that does not exist, so Graph failures propagated instead of degrading quietly.
  • User search in the admin area returned everything, and soft-deleted users were visible to every query that claimed to exclude them. Both were the same mistake: a Doctrine expression built and never passed to andWhere().
  • Gmail label changes ran inside the HTTP request. ApplyGmailLabelsMessage was never routed to the async transport, so archive, trash, star and mark-read made a live Google API call while the user waited — where the IMAP and Graph equivalents were queued. A Gmail outage surfaced as a 500 on a UI action rather than a retry on the worker.
  • JMAP Email/set rejected valid mailbox patches. Current mailboxIds were emitted as label ids where the protocol expects per-account binding ids; since both are autoincrement ints, a patch removing one mailbox failed with "No such Mailbox".

Changed

  • latest is published from release tags only. It previously followed every push to main, which meant the tag the README tells people to pull could carry unreviewed work and automatic schema migrations.
  • The login form no longer shows a "forgot password?" link. There is no reset flow — recovery is a console command — and the link was an href="#" that did nothing.