[!IMPORTANT] This README documents Neverest v0.2.0. Refer to the v1.0.0-beta README or the v0.1.0 README for an older release you are running, and to MIGRATION.md for the upgrade path from either.
Table of contents
Features
- PIM domain support: mail via IMAP, contacts via CardDAV rfc6352, calendar via CalDAV rfc4791, one account syncing several at once
- Local pimdir store specs: the single local copy an app reads, holding every domain an account syncs
- Retention: a removed item is kept, never lost, and reclaimed on a schedule
- Relay mode: a body crossing two IMAP servers is streamed server-to-server, never stored
- Queued submission: a message a frontend enqueued leaves through its source's send channel
- Auth support: anonymous, login, plain, oauthbearer, xoauth2, scram-sha-256 for IMAP; basic and bearer for CardDAV and CalDAV
- TLS support: Rustls with ring or aws crypto (
rustls-awsfeature), Native TLS (native-tlsfeature) - Discovery support: known provider rules, PACC, Thunderbird Autoconfiguration, SRV rfc6186 and DAV rfc6764, all run in parallel
- Interactive wizard turning an email address into a tested account
- TOML configuration with multi-account support, and JSON output via
--json, described byneverest json-schema
[!TIP] Neverest is written in Rust and uses cargo features to gate backend support: contacts and calendar need
dav. The default feature set is declared in Cargo.toml. Microsoft Graph is behindmsgraph, which is not a default while it syncs mail alone. JMAP and Gmail sources configure but have no backend yet.
Installation
Pre-built binary
Neverest can be installed with the installer:
As root:
|
As a regular user:
| PREFIX=/.local
Neverest v1 is not released yet, so the installer has nothing to fetch. Until then, check out the releases GitHub workflow and look for the Artifacts section.
[!NOTE] Such binaries are built with the default cargo features. If you need specific features, please use another installation method.
Cargo
With only IMAP support:
Nix
If you have the Flakes feature enabled:
Or run without installing:
Sources
Configuration
A configuration is loaded from the first valid path among:
$XDG_CONFIG_HOME/neverest/config.toml$HOME/.config/neverest/config.toml$HOME/.neverestrc
Override the path with -c <PATH> or NEVEREST_CONFIG=<PATH>; multiple paths can be passed at once, separated by :. The first one is the base and the rest are deep-merged on top. The full field reference lives in config.sample.toml.
Run neverest with no configuration file on disk and a minimal wizard discovers a provider from an email address, tests it, then offers to write the result. It sets up one account with one backend, the offline replica most setups want; anything else is written by hand against config.sample.toml.
neverest configure runs the same flow to add another account. It generates and never edits: the account it produces is appended to the file already there as plain text, so your comments and formatting survive, and changing an account is a job for your editor.
Declining the prompt prints the account on stdout, and --json or a redirected stdout skips the prompts entirely, so neverest configure > config.toml writes the file itself.
An account is one pimdir store fed by one or more named sources, each a remote, and it may hold several kinds at once. What it does is its arity plus two flags: with no targets every source syncs into the local store, the offline replica; with them the source is copied to each target.
one-way makes the sources authoritative, so the other side's changes are overwritten rather than merged, and retain says whether the store keeps bodies or is only the ledger it has to be anyway. Sources never meet, so several cache side by side for a frontend to union at display time.
Usage
Every command carries its own --help, the source of truth for its flags and syntax.
An account is initialized once, which opens every source so credential and network errors surface up front, then creates the empty store. sync refuses to run without it, and init refuses to run over it. --reset drops the cached state before a run, rebuilding it as a first sync would.
Conflicts
A card or an event edited on both sides is merged against the base the last sync agreed on, so two people touching different fields cost nobody a decision. What no merge settles is both sides setting one field two ways: the item parks, everything else keeps syncing, and the run exits 2.
Neverest raises no desktop notification of its own. Every run warns in the log, and --json carries the two numbers a notifier needs: conflicts is what this run marked, outstandingConflicts what the store holds waiting. Testing the first notifies on entry, once, with no state of its own to keep:
| &&
A wrapper reading that report can name the item, its collection and its side, which a fixed summary and body could not.
Deciding is a command and never a run. conflict list names what is waiting, conflict show <id> prints the three bodies, and conflict resolve <id> settles one, either by taking a side with --prefer-local or --prefer-remote or by handing the bodies to the merger conflict.merger names.
A settled body has to be a body of that item, so one no parser reads and one stating another UID are both refused. A decision is refused when the remote moved while it was being made, and nothing here holds the store, so a sync stays free to run while a merger is open.
Exit 2 is wider than conflicts: the run reconciled its collections but left something waiting for a person, be it a parked conflict, a duplicate UID the other side refuses, or a write it would not take. The report names which. A write that did not land is never counted, so already in sync means the run wrote nothing.
Retention and backup
The store never truly deletes an item: when its last binding vanishes the row is retained, hidden from the sync and from listings but kept with its body. After each sync neverest purges every retained item older than store.purge-after. Unset means never purge, "0" reproduces a terminal delete, and sync --no-purge skips one run.
This is what turns a sync into a backup. Make the source read-only and leave store.purge-after unset: a remote expunge still retires the local row, but the item and its body stay in the store, restorable, and neverest never pushes a deletion back to the server.
[]
= "imaps://imap.example.org:993"
= false
= false
Duplicated identities
A collection may hold one identity twice: two messages carrying the same Message-ID, two cards the same UID. Neverest cannot tell such copies apart, so it syncs neither and reports every id involved, on every run until the collection holds the identity once. Which copy to keep is yours to decide.
Nothing is wrong with your server: RFC 5322 binds the generator of a Message-ID, not what a store may hold, and a migration commonly produces such a pair. Guessing costs mail, since propagating a delete of the copy it happened to pick removes the only copy on the other source.
Coming from Maildir
Neverest ships no Maildir converter, and a local file store is not a sync source: the pimdir store is the local replica. Keyword storage is not standardized across Maildir consumers, so a local migration would silently lose or mangle flags depending on which tool wrote the source tree.
Initialize a fresh account and resync from the authoritative server instead: flags re-converge cleanly and the store reflects the actual server state. An existing on-disk tree is brought in through io-pimdir's conversion tooling rather than synced as a source.
License
This project is licensed under either of:
Social
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
Sponsoring
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
- 2022 → 2023: NGI Assure
- 2023 → 2024: NGI Zero Entrust
- 2024 → 2026: NGI Zero Core
- 2026 → 2027: NGI Zero Commons Fund
This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider sponsoring its development:
