Seven columns used to divide whatever width there was, so a phone — or a narrow docked pane — gave each of them about fifty pixels, and a block showed its colour and little else. A column has a floor now: below the width where seven of them fit the grid pans sideways under an hour gutter that stays pinned, and four days are legible where none were. The time-grid shell's own header costed that fix as disproportionate once — "three scroll containers kept in step by JavaScript" — and it is not, because the three grids already share one scroller; all that was left was making each one's first cell sticky. A BLOCK IS TWO ROWS NOW. The time on one line with the calendar's colour as a bar down the left, the title under it, which is a whole title in a 96px column where one line was four characters of each. Day view keeps the single line and is told so rather than measured for it: a day column is the whole grid and is never the width that makes stacking worth doing. The agenda's date rail is gone. A day is a heading with its events under it, and the times sit in a lane that is a grid track rather than a fixed guess — sized against every time in the view, so it is 43px where an "all day" needs it and 33px where nothing does. The month gives up its words entirely below 5.5rem of cell and draws one bar per entry, which is what a phone can honestly say in forty pixels, and "N more" is gone along with a count that a variable cell height could never make true; the date is the link into the day instead. The grid opens on now rather than on a fixed 07:00, and a chip too small for its own title opens to it while the pointer rests on it. Width only: growing the height made the chip inside stop following its parent, and a click aimed at the middle of a Day-view block landed under the button and opened nothing. Two bugs fell out of the work. An all-day entry sorted below the morning east of UTC, because its start is a floating midnight in UTC while a timed one's is an instant — the time grid never showed it, since it puts all-day entries in a band of their own and never asks. And a grid block had never had a background at all: an inline colour was overriding the surface class written beside it, so the only paint it carried was a tenth of a calendar colour over whatever happened to be behind it. Opening on now has one consequence worth naming: a fixture at 10:00 is on screen all morning and scrolled above the viewport by the afternoon, and `boundingBox()` answers for an off-screen element with coordinates that land under the pinned day headings. calendar-timegrid's drag helpers scroll the block into view before measuring it now; without that the suite passed for hours and then did not, and the failure read as "dragging is broken". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.4 KiB
plMail
Self-hosted mail with a calendar beside it. Connect the mailboxes you already have — IMAP, Gmail, Outlook — and read them side by side in one inbox, on a server you own.
📖 Read the handbook →
Installing on Docker, Linux, WSL2, macOS or a NAS · every feature in detail · connecting Gmail and
Outlook, with the exact permissions to tick · how the internals work.
Also on the wiki and in docs/ — all three
are built from the same files.
What it is
A mail client you host yourself. It connects to the mailboxes you already have — IMAP, Gmail, Outlook — syncs them into your own PostgreSQL database, and gives them one inbox, one search box and one set of labels in the browser.
It is not a mail server: it doesn't receive mail from the internet or host your domain. Because everything is synced locally, search runs against your database, and your mail stays readable when a provider doesn't.
Highlights
- Every account in one place — IMAP, Gmail and Outlook side by side, sending aliases included. Credentials are encrypted before they touch the database.
- New mail arrives on its own — IMAP IDLE, Gmail and Outlook push, browser notifications.
- Search that means it — full text plus
from:,subject:,label:,has:,before:and friends, completed as you type. - Filters as a tree — any/all/none, nested, restated in plain English and counted against real mail before you save. Apply one to mail that arrived before it existed.
- Filed where you drop it — drag a conversation onto a folder, a label or one of the inbox tabs, several at once if they are ticked. A folder moves it and a label just adds itself, so dropping on "Receipts" does not take the mail out of your inbox. Only the targets that will accept it light up, and a tab you choose by hand stays chosen when the next message in the thread arrives.
- A calendar beside the mail — two-way sync with Google, Microsoft and CalDAV, a drag-to-move week grid that pans rather than squeezes on a phone, invitations answered in place, share and booking links.
- Files where you keep them — attach from and save to Drive, Photos, OneDrive, Dropbox, Nextcloud, Immich and Paperless-ngx.
- A language model, if you want one — point plMail at an Ollama box on your own network and it can search by meaning, draft replies, summarise a thread and sort mail into tabs. A long thread is summarised from as much of it as fits, and says so; ask for the whole conversation and it runs as a job with a context window to match. What it has been told about how you write is editable in the composer, for one attempt at a time. Off by default, each of the four switched on separately, and nothing ever leaves your network.
- You decide what sorts your mail — header rules that answer the same way every time, or the model reading each message; and on Gmail, whether either of them is allowed to disagree with Google. Change your mind and the mailbox re-sorts itself in the background. Every message's Details panel shows what all three said and which one won, and a Report button tells your administrator when it got it wrong.
- It tells you when something breaks — a sign-in that has expired, a calendar that stopped syncing, push that lapsed, work the queue gave up on. Each with what it means for your mail and the smallest thing that fixes it. A Google sign-in dying every week gets named for what it is — Google expires tokens for an app still in Testing — and warned about before the next one.
- Yours — two-factor auth, per-app JMAP passwords, 41 themes, English, German and Pirate.
A look around
Screenshots use demo data — none of the accounts, senders or messages shown are real.
Install
Images are published for linux/amd64 and linux/arm64. Nothing to fill in first: the secrets are
generated on first start, and the public address is asked for on the setup screen.
Docker
git clone https://github.com/karatektus/pl_mail.git
cd pl_mail
docker compose up -d
Open https://localhost and create your account.
TrueNAS SCALE
- Create a dataset for plMail — say
yourPool/yourDataSet/plmail. - Open
truenas.compose.yamland change one line to that path:path_data: &path_data /mnt/yourPool/yourDataSet/plmail - Apps → Discover → ⋮ → Install via YAML, paste the file, install.
Reach it on port 30080 and put a reverse proxy in front of it. Everything else in that file works
as it is; each setting explains itself in place.
Then
Add an IMAP mailbox in Settings → Mail accounts and you're done. Gmail and Outlook need OAuth credentials from Google or Microsoft first — the handbook walks through both, including the optional Pub/Sub topic for instant Gmail delivery.
Back up two things, separately: the PostgreSQL data and
var/secrets/generated.env. The database holds your mail; the key inside that file unlocks the credentials in it. Keeping them apart is the point — and losing the key means the stored mailbox passwords are gone.
Contributing
Bug reports and pull requests welcome — see CONTRIBUTING.md for the dev setup, the test suites and the console commands.
License
AGPL-3.0.






