The original rejection of pgvector was tested rather than reasoned, and it stands: pgvector/pgvector:pg18 means musl to glibc on an existing data volume, and 44 collation-dependent indexes — 18 of them UNIQUE — are then walked with the wrong comparator, silently, because musl records no collation version so Postgres's own mismatch check never fires. So the extension is compiled INTO the alpine image instead. libc never changes, and there is nothing to rebuild. Verified end to end rather than argued: an existing volume migrated on stock alpine, stopped, restarted on the pgvector image, then bt_index_check(heapallindexed => true) across 175 btree indexes — zero errors. Two build flags carry more weight than they look. OPTFLAGS="" because pgvector's Makefile defaults to -march=native, which bakes the build host's instruction set into vector.so and gives SIGILL mid-query on a host without it, with an empty log. with_llvm=no because a missing .bc file makes the JIT skip inlining silently, and inlining does not apply to index scans anyway. Pinned at v0.8.6, not the earliest that works. v0.8.0 does not compile against Postgres 18 at all (vacuum_delay_point gained a parameter); v0.8.1 is the first that does. But 0.8.3 fixed possible index corruption with HNSW vacuuming and 0.8.4 fixed an unrepaired HNSW graph, and message_embedding is ON DELETE CASCADE from message on a system that deletes mail in bulk — so vacuum is a hot path here and shipping the earliest that merely compiles would ship two known corruption bugs. THE HANDBOOK'S 24x WAS WRONG AND IS CORRECTED TO 1.75x. The SIMD dot product is microseconds; what dominates is that embedding is real[], so every row pays a 4KB detoast and a fresh real[] to vector conversion. 74,000 rows: 7.70s becomes 4.51s. NO INDEX IS CREATED, and that is the finding rather than an omission. An HNSW index answers ORDER BY ... LIMIT k and the search asks distance <= 0.45, which no ANN index serves. The expression form needed to build one at all is (embedding::vector(1024)), which Postgres treats as a DIFFERENT expression from the inlined function's (embedding::vector) — so the index exists, the planner ignores it, and nothing says so. And it costs 7m40s and 578MB to build at the maintenance_work_mem this stack ships, behind the one advisory lock every entrypoint waits on: an eight-minute dead stack, not a slow migration. The measured route to an index is written down in ai-assist.md rather than half taken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
- A calendar beside the mail — two-way sync with Google, Microsoft and CalDAV, drag-to-move week grid, invitations answered in place, share and booking links.
- Files where you keep them — attach from and save to Drive, Photos, OneDrive, Dropbox, Nextcloud and Immich.
- 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.






