Skip to content
Projects

pimalaya/neverest has 323 GitHub stars and 14 forks. Star changes are not measured yet. Last checked 2026-11-09 09:08 UTC. Last push 2026-11-08.

How these counts are measured

CLI to synchronize PIM collections: mail, contact, calendar…

What you can do

  • PIM domain support: mail via IMAP and Microsoft Graph, contacts via CardDAV rfc6352, Microsoft Graph and Google People, calendar via CalDAV rfc4791, Microsoft Graph and Google Calendar, 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

Measured star growth

in 24h
Update delayed
in 7 days
Update delayed
in 15 days
Update delayed
in 1 month
Update delayed

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

README.md

Original
Logo

📫 Neverest

CLI to synchronize PIM collections: mail, contact, calendar…

Matrix Mastodon Sponsor

[!CAUTION] Neverest is v0.x: expect breaking changes between releases until it stabilises.

Table of contents

Features

  • PIM domain support: mail via IMAP and Microsoft Graph, contacts via CardDAV rfc6352, Microsoft Graph and Google People, calendar via CalDAV rfc4791, Microsoft Graph and Google Calendar, 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
  • Search index: the store's full-text index specs, kept current at every sync for the frontends to query
  • Auth support: anonymous, login, plain, oauthbearer, xoauth2, scram-sha-256 for IMAP; basic and bearer for CardDAV and CalDAV; bearer for Microsoft Graph, Google People and Google Calendar
  • TLS support: Rustls with ring or aws crypto (rustls-aws feature), Native TLS (native-tls feature)
  • Discovery support: known provider rules, PACC, Thunderbird Autoconfiguration, SRV rfc6186 and DAV rfc6764, all run in parallel
  • Interactive wizard turning an email address into one tested account covering mail, sending, contacts and calendar
  • TOML configuration with multi-account support, and JSON output via --json, described by neverest json-schema

[!TIP] Neverest is written in Rust and uses cargo features to gate backend support: CardDAV and CalDAV need dav, Microsoft Graph mail, contacts and calendars need msgraph, Google contacts gpeople, Google calendars gcal and Gmail gmail. The default feature set is declared in Cargo.toml. JMAP sources configure but have no backend yet.

The default wizard feature brings the interactive configure; leave it out of a scripted or embedded build that never prompts.

The default vendored feature builds SQLite from source; leave it out (--no-default-features --features …) to link the system one.

The default search feature builds the store's search index, which frontends such as himalaya search through and never build themselves; leave it out only where no frontend searches the store.

Installation

Pre-built binary

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

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

Nix

If you have the Flakes feature enabled:

nix profile install github:pimalaya/neverest

Or run without installing:

nix run github:pimalaya/neverest

Sources

git clone https://github.com/pimalaya/neverest
cd neverest
nix run

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 wizard discovers your servers from an email address, then offers to write the result. It sets up one account with one source per domain it found, mail (IMAP, the Gmail API or Microsoft Graph), contacts (CardDAV, the People API or Graph) and calendar (CalDAV, the Calendar API or Graph), plus an SMTP channel for sending, the offline replica most setups want; a mirror, a fan-in and anything else are written by hand against config.sample.toml.

Each domain gets the best option found without a question, and a review shows the picks in plain words, each one a change away (another option, a server URL, or leaving the domain out). Each identity then signs in once: one password for the servers of one name (imap.example.org, smtp.example.org and dav.example.org share one), typed once, tested at once, then stored in a keyring you pick under neverest-<account>-<source>, or one browser sign-in through Ortie per authorization server. Last, each source lists its collections to pick from, mail starting without junk and trash. Finding nothing asks for a server URL (imaps://, https://, carddavs://, caldavs://).

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.

Every account shares one pimdir store, at neverest/ under your state directory (`` on Linux, ~/Library/Application Support on macOS, %LOCALAPPDATA% on Windows) unless it names its own store.root. Each account is fed by one or more named sources, each a remote, and it may hold several kinds at once; its collections are grouped under its name, so a frontend reads one account of the store through its own account key (pimdir.account in himalaya, calendula and cardamum). What an account 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.

A collection one endpoint no longer lists is deleted on the other only where that endpoint sets collection.delete = true. It is off by default: a missing collection is the only sign IMAP and DAV give of a deletion, and a listing can leave out one nobody deleted. The same setting lets a frontend ask neverest to delete a collection on that server (pimdir's collection-delete intent, himalaya pimdir mailbox delete); renaming one (collection-rename) needs no setting.

Usage

Every command carries its own --help, the source of truth for its flags and syntax.

neverest init
neverest sync --dry-run
neverest sync -a <account> --include-collection INBOX
neverest check
neverest drain
neverest index
neverest conflict list
neverest conflict resolve <id> -a <account> --interactive

Every command runs over every account, one after the other, unless -a names some (it is repeatable). Each account's outcome is its own: one that fails is reported, the next one still runs, and the command exits 3. The store is locked for the whole run, so two accounts of one store never sync at once; give an account that needs its own schedule its own store.root. conflict show, conflict resolve and sync --reset act on one account, which -a names when there are several. index builds the store of the accounts it runs over, each store once. default = true on an account is ignored, with a warning.

An account is initialized once, which opens every source so credential and network errors surface up front, then adds the account to the store, creating the store when there is none. sync refuses to run an account without it, and init leaves an initialized account as it is. sync --reset -a <account> drops that account's collections before a run, rebuilding them as a first sync would, every other account of the store left as it was. It refuses while they hold a queued or parked action, which drain applies or its frontend cancels first, and while they hold other work no server has (conflicts, unpushed edits, mail a replica keeps after the server expunged it), listing that work; --reset --force drops that other work with the collections. A reset is the whole account's, so it is refused beside --dry-run, -m, -x and -s.

What himalaya, calendula and cardamum write is queued in the store, and only neverest applies it. A sync does so first, before reaching any server; drain does it alone, offline, so a frontend reads a created item back at once, the next sync pushing it.

neverest also keeps the store's search index (index.db), which the frontends query and only the store's owner builds. A sync refreshes it last, for at most store.index-timeout (60 seconds by default, "0" turning it off), and a drain for 2 seconds; a large first build spreads over several runs, each going on where the last stopped, or index builds it whole at once. The report says how far it got under index; an index failure is a warning and never changes the exit code. After a reset, the next refresh drops what the account's collections held. The index reads floating times and all-day dates in your time zone, TZ else the system's, UTC when it has no IANA name; changing it re-expands every calendar item at the next refresh.

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. A run that could not reach a source, or apply part of its work, exits 3, and the next run picks it up.

Neverest raises no desktop notification of its own. Every run warns in the log, and --json carries, in each account's report, the two numbers a notifier needs: conflicts is what this run marked, outstandingConflicts what the store holds waiting for that account. Testing the first notifies on entry, once, with no state of its own to keep:

neverest sync --json | jq -e 'select(has("event") | not) | [.accounts[].report.conflicts // [] | length] | add > 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.

Before its result, sync --json prints its progress, one line per event, each carrying event (neverest json-schema neverest-sync.events describes them), account-started announcing each account, so a frontend can show how far a long run got; the result is the last line, listing each account's report under accounts.

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, a write it would not take, a send the server refused for good, a queued action that could not be applied, a listing that left out most of a collection, or a change withheld over a body the store cannot serve. The report names which. A write that did not land is never counted, so already in sync means the run wrote nothing.

A listing that leaves out more than half of at least 20 items a collection held drops nothing: it reads as a server answering short, and the report names it under absences. Once the server is known to hold no more, sync --accept-absences drops them, for that run only.

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.

[accounts.backup]
imap.server = "imaps://imap.example.org:993"
imap.item.delete = false
imap.collection.delete = 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

Sponsoring

nlnet

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:

GitHub Ko-fi Buy Me a Coffee Liberapay thanks.dev PayPal

Explore related projects