<div align="center">
<img src="./logo.svg" alt="Logo" width="128" height="128" />
<h1>📫 Neverest</h1>
<p>CLI to synchronize PIM collections: mail, contact, calendar…</p>
<p>
<a href="https://matrix.to/#/#pimalaya:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/chat-%23pimalaya-blue?style=flat&logo=matrix&logoColor=white"/></a>
<a href="https://fosstodon.org/@pimalaya"><img alt="Mastodon" src="https://img.shields.io/badge/news-%40pimalaya-blue?style=flat&logo=mastodon&logoColor=white"/></a>
<a href="https://pimalaya.org/sponsor/"><img alt="Sponsor" src="https://img.shields.io/badge/sponsor-pink?style=flat&logo=github-sponsors&logoColor=white"/></a>
</p>
</div>
> [!IMPORTANT]
> This README documents Neverest v0.2.0. Refer to the [v1.0.0-beta README](https://github.com/pimalaya/neverest/blob/v1.0.0-beta/README.md) or the [v0.1.0 README](https://github.com/pimalaya/neverest/blob/v0.1.0/README.md) for an older release you are running, and to [MIGRATION.md](./MIGRATION.md) for the upgrade path from either.
## Table of contents
- [Features](#features)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
- [AI policy](https://github.com/pimalaya/.github/blob/master/AI_POLICY.md)
- [License](#license)
- [Social](#social)
- [Contributing](./CONTRIBUTING.md)
- [Sponsoring](#sponsoring)
## Features
- **PIM domain** support: **mail** via IMAP, **contacts** via CardDAV <sup>[rfc6352](https://www.iana.org/go/rfc6352)</sup>, **calendar** via CalDAV <sup>[rfc4791](https://www.iana.org/go/rfc4791)</sup>, one account syncing several at once
- **Local pimdir store** <sup>[specs](https://github.com/pimalaya/pimdir)</sup>: 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](https://crates.io/crates/rustls) with ring or aws crypto (`rustls-aws` feature), [Native TLS](https://crates.io/crates/native-tls) (`native-tls` feature)
- **Discovery** support: known provider rules, PACC, Thunderbird Autoconfiguration, SRV <sup>[rfc6186](https://datatracker.ietf.org/doc/html/rfc6186)</sup> and DAV <sup>[rfc6764](https://datatracker.ietf.org/doc/html/rfc6764)</sup>, 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 by `neverest json-schema`
> [!TIP]
> Neverest is written in [Rust](https://www.rust-lang.org/) and uses [cargo features](https://doc.rust-lang.org/cargo/reference/features.html) to gate backend support: contacts and calendar need `dav`. The default feature set is declared in [Cargo.toml](./Cargo.toml). Microsoft Graph is behind `msgraph`, 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:*
```sh
curl -sSL https://raw.githubusercontent.com/pimalaya/neverest/master/install.sh | sudo sh
```
*As a regular user:*
```sh
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](https://github.com/pimalaya/neverest/actions/workflows/releases.yml) 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
```sh
cargo install --locked --git https://github.com/pimalaya/neverest.git
```
With only IMAP support:
```sh
cargo install --locked --git https://github.com/pimalaya/neverest.git \
--no-default-features \
--features imap,smtp,rustls-ring
```
### Nix
If you have the [Flakes](https://nixos.wiki/wiki/Flakes) feature enabled:
```sh
nix profile install github:pimalaya/neverest
```
Or run without installing:
```sh
nix run github:pimalaya/neverest
```
### Sources
```sh
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](./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](./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.
```sh
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.
### 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:
```sh
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.
### 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.
```toml
[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:
- [MIT license](LICENSE-MIT)
- [Apache License, Version 2.0](LICENSE-APACHE)
## Social
- Chat on [Matrix](https://matrix.to/#/#pimalaya:matrix.org)
- News on [Mastodon](https://fosstodon.org/@pimalaya) or [RSS](https://fosstodon.org/@pimalaya.rss)
- Mail at [pimalaya.org@posteo.net](mailto:pimalaya.org@posteo.net)
## Sponsoring
[](https://nlnet.nl/)
Special thanks to the [NLnet foundation](https://nlnet.nl/) and the [European Commission](https://www.ngi.eu/) that have been financially supporting the project for years:
- 2022 → 2023: [NGI Assure](https://nlnet.nl/project/Himalaya/)
- 2023 → 2024: [NGI Zero Entrust](https://nlnet.nl/project/Pimalaya/)
- 2024 → 2026: [NGI Zero Core](https://nlnet.nl/project/Pimalaya-PIM/)
- 2026 → 2027: [NGI Zero Commons Fund](https://nlnet.nl/project/Pimalaya-pimdir/)
This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider [sponsoring](https://pimalaya.org/sponsor/) its development:
[](https://github.com/sponsors/soywod)
[](https://ko-fi.com/pimalaya)
[](https://www.buymeacoffee.com/pimalaya)
[](https://liberapay.com/pimalaya)
[](https://thanks.dev/u/gh/soywod)
[](https://www.paypal.com/paypalme/soywod)