* fix(insight): let an older mail leave a card as a newer one made it The harvester's upsert lets the last mail it reads overwrite a card's status, payload, ETA and link, on the assumption that the last one read is the newest. A sync reads in date order, but the insights backfill walks by id, and the Graph import stored mail newest-first: a parcel's "out for delivery" mail was read before its "announced" one, and the older statement overwrote it, so a re-read left every card on the first status of its series. A mail strictly older than the one the card already stands on now changes nothing. Mail of the same instant keeps the old behaviour (the last read wins), and so does a card whose mail was deleted, which has nothing to be older than. land() reports whether it wrote, so the backfill's count does not include skipped drafts. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(insight): window undated cards on their mail's date, not their row's The undated cards (GitHub activity and the like) are listed for the last seven days, windowed and ordered on createdAt, the instant the harvester made the row. That is the mail's age only while the harvester runs as mail arrives. The insights backfill re-reads old mail and creates its cards now, so a card made from a month-old notification was a card of the last minute and took a place among the newest twenty for a week; the Graph import's newest-first ids put the same distortion into the order. The query now joins the card's mail and windows and orders on its receivedAt, which is the newest mail of the series because the card links that one. A card whose mail was deleted keeps its row's date, the only one it has. createdAt keeps meaning when the row was created, so nothing is stamped differently and no migration is needed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(insight): offer a code that states no expiry for fifteen minutes OtpExtractor sets a code card's happensAt to its expiry, so the radar drops it when that passes, and leaves it null when the mail states no lifetime, because a guessed default would retire a code that might still be good. The radar's list of undated cards then took such a code like any undated card (GitHub activity and the like): seven days, with the code readable on the card. Undated codes are now offered for fifteen minutes of their mail's date instead of the week. That is a guess about how long a code lives, made on the display side and not in the extractor, and it errs towards showing too little. The extractor's decision not to invent an expiry stands, the code is still on the mail itself, and codes that do state an expiry take the dated path as before. Every other undated kind keeps its week. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: mani <15911342+Flouss@users.noreply.github.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
plMail documentation
The handbook. README says what plMail is and gets you to a running instance; this covers everything after that — every feature and how to use it, how to install it on whatever you have, how to register the Google and Microsoft applications it talks to, and how the parts work underneath.
Mirrored to the GitHub wiki, which is generated from these files. Edit them here; browser edits to the wiki are overwritten by the next mirror.
| If you want to | Start at |
|---|---|
| Use a feature | Using plMail |
| Install or run it | Installing and running |
| Connect Gmail, Outlook or a CalDAV server | Providers |
| Understand or extend the internals | How it works |
Using plMail
One page per area. Each ends with links into How it works for the mechanism behind it.
| Page | What it covers |
|---|---|
| Reading, threads, labels, search and its operators, snooze, attachments, composing, signatures, emoji and inline images, scheduled send, read receipts, drafts, undo send | |
| Accounts and aliases | Adding Gmail, Outlook and IMAP accounts, sending aliases, per-account settings |
| Account health | What is broken and what fixes it, reconnecting an account without losing its mail, the two ways push breaks |
| Templates | Reusable messages, the folder tree that mirrors your accounts, variables for recipient, sender, signature and dates, inserting one while composing |
| Filters | Condition trees, actions, the plain-English restatement, applying a rule to mail that already arrived |
| Calendar | The four views and the time grid, creating and editing events, recurrence, editing one occurrence, the docked pane |
| Invitations and events from mail | RSVP, events extracted from invitations and from ordinary prose, proposals, Happening Soon |
| Reminders | Setting alerts, how they are delivered, what a fresh install needs before they arrive |
| Connected calendars | Subscribing to Google, Microsoft and CalDAV calendars, two-way sync, ICS import, export and feed subscriptions, duplicate meetings |
| Sharing and booking | Share links and what each reveals, appointment pages, how a booking arrives |
| Files and integrations | Attaching from and saving to Drive, Photos, OneDrive, Dropbox, Nextcloud, Immich and Paperless-ngx |
| Security | Changing your password, two-factor authentication, recovery codes, remembered devices, app passwords, sessions |
| Other clients | Connecting a JMAP client, per-app passwords, the PWA and browser notifications |
| Appearance | Themes, custom colours and background, the live preview, what a list row shows, typeface and text size, per-pane density, import and export, language |
| Administration | Users and roles, switching an account off, enabling integrations, monitoring, queues and the version chip |
Installing and running
| Page | What it covers |
|---|---|
| Docker Compose | The supported path, start to finish |
| Platform notes | Linux, Windows via WSL2, macOS, and NAS boxes |
| Behind a reverse proxy | TLS, APP_PUBLIC_URL, TRUSTED_PROXIES, and what breaks without them |
| Configuration reference | Every environment variable, what it does and what happens if it is wrong |
| Backup and restore | What to back up, the encryption key, and restoring onto a new host |
| Configuration backup | Carrying an install's settings and credentials to another one, as one encrypted file |
| Upgrading | Migrations run on boot, what that means, and how to tell which build is running |
| Demo mode | Hosting a public demo: throwaway mailboxes, scripted mail, and what it switches off |
| Troubleshooting | Health checks, the queue, logs, and the failures that have actually happened |
Providers
Each page names the exact console, the exact checkboxes and the exact redirect URIs.
| Page | What it covers |
|---|---|
| Cloud project, OAuth client, scopes for mail and calendar, Pub/Sub for Gmail push, calendar watch channels and domain verification | |
| Microsoft | Azure app registration, redirect URIs, delegated permissions, tenant choice, Graph subscriptions |
| IMAP and SMTP | Plain mailboxes, IDLE, and the settings servers disagree about |
| CalDAV | Discovery, app passwords, and the servers that have been tested |
| ICS feeds | Subscribing to a published calendar by URL, and why some addresses are refused |
How it works
Deeper than CONTRIBUTING's notes, and aimed at somebody auditing or extending plMail rather than running it.
| Page | What it covers |
|---|---|
| Architecture | The layers, what lives where, and the rules that keep it that way |
| Mail ingest | The pipeline from provider to database, threading, categorisation |
| The calendar model | JSCalendar in jsonb, projected columns, occurrences, recurrence and overrides |
| The sync engine | The driver contract every provider implements, push channels, deduplication |
| AI assistance | The optional model features: what is off by default, and why the vector design avoids pgvector |
| Event extraction | How an invitation and how a sentence become a calendar entry |
| JMAP | What is implemented, what is deliberately not, and the id spaces |
| Security model | Encryption at rest, the secrets file, tokens, and what a public link can reach |
Client authors should also read Client development, which is the protocol-level reference.
Conventions in these pages
- Commands are given as you would run them against a Compose deployment:
docker compose exec php php bin/console <command>. Drop the prefix if you run plMail directly. - Settings paths are written as
Settings → Calendar → Connected calendars. - A "things that bite" section at the end of a page collects the traps — the failure whose cause is not obvious from the symptom. They are there because they happened.