CLI to synchronize PIM collections: mail, contact, calendar…
See the code[!CAUTION] Neverest is
v0.x: expect breaking changes between releases until it stabilises.
rustls-aws feature), Native TLS (native-tls feature)--json, described by neverest 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.
Neverest can be installed with the installer:
As root:
curl -sSL https://raw.githubusercontent.com/pimalaya/neverest/master/install.sh | sudo sh
As a regular user:
curl -sSL https://raw.githubusercontent.com/pimalaya/neverest/master/install.sh | PREFIX=~/.local sh
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 install --locked --git https://github.com/pimalaya/neverest.git
With only IMAP support:
cargo install --locked --git https://github.com/pimalaya/neverest.git \
--no-default-features \
--features imap,smtp,rustls-ring
If you have the Flakes feature enabled:
nix profile install github:pimalaya/neverest
Or run without installing:
nix run github:pimalaya/neverest
git clone https://github.com/pimalaya/neverest
cd neverest
nix run
A configuration is loaded from the first valid path among:
$XDG_CONFIG_HOME/neverest/config.toml$HOME/.config/neverest/config.toml$HOME/.neverestrcOverride 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.
Every command carries its own --help, the source of truth for its flags and syntax.
neverest init -a <account>
neverest sync -a <account> --dry-run
neverest sync -a <account> --include-collection INBOX
neverest check -a <account>
neverest conflict list -a <account>
neverest conflict resolve <id> -a <account> --interactive
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.
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:
neverest sync --json | jq -e '.conflicts | length > 0' >/dev/null \
&& notify-send "neverest" "an item needs a decision"
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.
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.
[accounts.backup]
imap.server = "imaps://imap.example.org:993"
imap.item.delete = false
imap.collection.delete = false
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.
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.
This project is licensed under either of:
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider sponsoring its development:
Rust
98.0%
Shell
1.4%
CLI to synchronize PIM collections: mail, contact, calendar…
See the code[!CAUTION] Neverest is
v0.x: expect breaking changes between releases until it stabilises.
rustls-aws feature), Native TLS (native-tls feature)--json, described by neverest 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.
Neverest can be installed with the installer:
As root:
curl -sSL https://raw.githubusercontent.com/pimalaya/neverest/master/install.sh | sudo sh
As a regular user:
curl -sSL https://raw.githubusercontent.com/pimalaya/neverest/master/install.sh | PREFIX=~/.local sh
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 install --locked --git https://github.com/pimalaya/neverest.git
With only IMAP support:
cargo install --locked --git https://github.com/pimalaya/neverest.git \
--no-default-features \
--features imap,smtp,rustls-ring
If you have the Flakes feature enabled:
nix profile install github:pimalaya/neverest
Or run without installing:
nix run github:pimalaya/neverest
git clone https://github.com/pimalaya/neverest
cd neverest
nix run
A configuration is loaded from the first valid path among:
$XDG_CONFIG_HOME/neverest/config.toml$HOME/.config/neverest/config.toml$HOME/.neverestrcOverride 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.
Every command carries its own --help, the source of truth for its flags and syntax.
neverest init -a <account>
neverest sync -a <account> --dry-run
neverest sync -a <account> --include-collection INBOX
neverest check -a <account>
neverest conflict list -a <account>
neverest conflict resolve <id> -a <account> --interactive
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.
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:
neverest sync --json | jq -e '.conflicts | length > 0' >/dev/null \
&& notify-send "neverest" "an item needs a decision"
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.
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.
[accounts.backup]
imap.server = "imaps://imap.example.org:993"
imap.item.delete = false
imap.collection.delete = false
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.
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.
This project is licensed under either of:
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider sponsoring its development:
Rust
98.0%
Shell
1.4%