# ================================================================================ # `neverest` configuration file. # # Loaded from the first valid path among: # - $XDG_CONFIG_HOME/neverest/config.toml # - $HOME/.config/neverest/config.toml # - $HOME/.neverestrc # # Override with `neverest -c ` or `NEVEREST_CONFIG=`. Multiple paths # can be passed at once, separated by `:`; the first one is the base and the # rest are deep-merged on top of it. # # Bare `neverest` with no configuration file on disk offers the wizard, which # writes a one-account, one-source configuration in the flat spelling below. # Everything else on this page is written by hand. # # An account is one pimdir STORE, fed by one or more named SOURCES, each a # remote. A backend written directly under the account (`imap.server = …`) is # sugar for a source named after its protocol; the `sources` table at the bottom # is what you reach for when you need two of one protocol. # # WHAT AN ACCOUNT DOES is its arity plus two flags, so there is no mode to name: # # sources targets one-way behaviour # ------- ------- ------- ------------------------------------------------ # 1 1 false two-way mirror between the two remotes # 1 1 true source overwrites target # 1 N true source overwrites each target (false is refused) # N 0 false each source merges two-way with the local store # N 0 true sources overwrite the store, local edits dropped # # Anything else is refused at load, naming the nearest legal shape. With no # target the local store is the destination, the offline replica everything # below is written for. # # Sources never meet: an item one holds crosses to the targets, never to another # source, so several sit side by side in one store for a frontend to union. # ================================================================================ [accounts.example] # Use this account when `-a/--account` is not passed. default = true # Make the sources authoritative: their side wins and the other's change is # DISCARDED rather than merged, so nothing is reported as a conflict. Turning it # on over an account that synced both ways is refused once; `neverest sync # --accept-mode` says you meant it. #one-way = false # Whether the store keeps bodies and is readable by a frontend, rather than only # the ledger of item spines and checkpoints it has to be in every mode. Unset # takes the destination's answer: true with no targets, false with targets. Set # it with targets to migrate AND keep a local copy. #retain = true # -------------------------------------------------------------------------------- # Per-source overview # -------------------------------------------------------------------------------- # # Each source picks exactly one backend: `imap.*`, `jmap.*`, `gmail.*`, # `msgraph.*`, `carddav.*` or `caldav.*`. Its filter, permissions and pool size # live inside that backend table: # - `collection.filter`: which collections this source syncs # - `collection.{create,delete}`, `flag.update`, `item.update`: default true # - `item.{create,delete}`: both required when the block is declared # - `pool-size`: default 8 (IMAP), 4 (JMAP, Gmail, Graph, DAV) # # `item.update` gates in-place body edits, so it is inert for mail and # meaningful for CardDAV and CalDAV. # Which collections this source syncs. Pick exactly one variant; omit for `all`. imap.collection.filter = "all" #imap.collection.filter.include = ["INBOX", "Sent"] #imap.collection.filter.exclude = ["[Gmail]/All Mail", "Trash"] # -------------------------------------------------------------------------------- # IMAP config # https://www.iana.org/go/rfc9051 # -------------------------------------------------------------------------------- # Bare authority (implicit `imaps://`), or a full `imap://` / `imaps://` URL. imap.server = "imap.fastmail.com" #imap.server = "imap.example.com:143" #imap.server = "imap://example.com:143" #imap.server = "imaps://example.com:993" # TLS and crypto providers, defaulting to the first available at runtime. #imap.tls.provider = "rustls" #imap.tls.provider = "native-tls" #imap.tls.rustls.crypto = "ring" #imap.tls.rustls.crypto = "aws" # Extra root certificate, PEM-encoded. The path is shell-expanded. #imap.tls.cert = "/path/to/custom/cert.pem" # Upgrade a cleartext `imap://` connection. #imap.starttls = false # Offered during the TLS handshake (rustls only). Defaults to ["imap"], `[]` # skips ALPN. #imap.alpn = ["imap"] #imap.alpn = [] # Pick exactly one SASL mechanism. Omit the whole `sasl` table to skip # authentication (no `AUTHENTICATE` command sent). # ANONYMOUS: https://datatracker.ietf.org/doc/html/rfc4505 #imap.sasl.anonymous.message = "neverest" # PLAIN: https://datatracker.ietf.org/doc/html/rfc4616 imap.sasl.plain.username = "pimalaya@fastmail.org" imap.sasl.plain.password.command = ["pass", "show", "pimalaya/fastmail-imap-pop-smtp"] #imap.sasl.plain.password.command = "pass show example" # LOGIN: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00 #imap.sasl.login.username = "user@example.com" #imap.sasl.login.password.raw = "***" # OAUTHBEARER (GS2 host and port derived from the server URL): # https://datatracker.ietf.org/doc/html/rfc7628 #imap.sasl.oauthbearer.username = "user@example.com" #imap.sasl.oauthbearer.token.raw = "***" #imap.sasl.oauthbearer.token.command = ["ortie", "token", "read", "example"] # XOAUTH2: https://developers.google.com/gmail/imap/xoauth2-protocol #imap.sasl.xoauth2.username = "user@example.com" #imap.sasl.xoauth2.token.raw = "***" # SCRAM-SHA-256: https://datatracker.ietf.org/doc/html/rfc7677 #imap.sasl.scram-sha-256.username = "user@example.com" #imap.sasl.scram-sha-256.password.raw = "***" # Keep the server read-mostly, so a buggy sync cannot destroy its state. imap.collection.create = false imap.collection.delete = false imap.item.create = true imap.item.delete = false # Narrow the pool when the server's `LIMIT` advertises less than 8. #imap.pool-size = 8 # -------------------------------------------------------------------------------- # JMAP config (alternative to the IMAP block above) # https://www.iana.org/go/rfc8620 # https://www.iana.org/go/rfc8621 # -------------------------------------------------------------------------------- # # This block parses, but no JMAP backend is compiled in yet, so a side # declaring it is refused when the sync opens it. # Bare authority (discovered through `GET /.well-known/jmap`) or session URL. #jmap.server = "fastmail.com" #jmap.server = "https://api.fastmail.com/jmap/session" # TLS mirrors the imap.tls block above. ALPN defaults to ["http/1.1"]. #jmap.tls.provider = "rustls" #jmap.tls.rustls.crypto = "ring" #jmap.tls.cert = "/path/to/custom/cert.pem" #jmap.alpn = ["http/1.1"] #jmap.alpn = [] # Pick exactly one of `header`, `bearer`, `basic`. #jmap.auth.header.raw = "Bearer eyJhbGciOiJ..." #jmap.auth.header.command = "pass show fastmail-token" #jmap.auth.bearer.token.raw = "***" #jmap.auth.bearer.token.command = ["ortie", "access-token", "read", "fastmail-api"] #jmap.auth.basic.username = "user@example.com" #jmap.auth.basic.password.raw = "***" #jmap.auth.basic.password.command = "pass show fastmail" #jmap.pool-size = 4 # -------------------------------------------------------------------------------- # Gmail config (alternative to the IMAP / JMAP blocks above) # https://developers.google.com/gmail/api # -------------------------------------------------------------------------------- # # This block parses, but no Gmail backend is compiled in yet, so a side # declaring it is refused when the sync opens it. # Labels are exposed as collections; the API host is fixed, so only the owner, # TLS and the credential are configurable. #gmail.user-id = "me" #gmail.tls.provider = "rustls" #gmail.tls.rustls.crypto = "ring" #gmail.tls.cert = "/path/to/custom/cert.pem" #gmail.alpn = ["http/1.1"] #gmail.alpn = [] # Bearer tokens only. Refreshing one is the command's job, typically ortie. #gmail.auth.token.raw = "***" #gmail.auth.token.command = ["ortie", "access-token", "read", "gmail"] #gmail.item.create = true #gmail.item.delete = false #gmail.pool-size = 4 # -------------------------------------------------------------------------------- # Microsoft Graph config (alternative to the blocks above) # https://learn.microsoft.com/en-us/graph/api/resources/mail-api-overview # -------------------------------------------------------------------------------- # # NOT in a released binary: `msgraph` is off by default while Graph carries mail, # contacts and calendar and this backend syncs only mail. This block parses in # every build, and a source declaring it is refused when the sync opens it unless # you built with `--features msgraph`. # Mail folders are exposed as collections; the API host is fixed, so only the # owner, TLS and the credential are configurable. #msgraph.user-id = "me" #msgraph.tls.provider = "rustls" #msgraph.tls.rustls.crypto = "ring" #msgraph.tls.cert = "/path/to/custom/cert.pem" #msgraph.alpn = ["http/1.1"] #msgraph.alpn = [] # Bearer tokens only: neverest runs no OAuth flow. The command runs once a run, # typically ortie serving a cached, auto-refreshed token. #msgraph.auth.token.raw = "***" #msgraph.auth.token.command = ["ortie", "-a", "msgraph", "token", "show", "--auto-refresh"] # Graph pushes flags and deletes only; appends and moves are rejected # whatever the permissions say. #msgraph.item.create = true #msgraph.item.delete = false #msgraph.pool-size = 4 # -------------------------------------------------------------------------------- # CardDAV config (contacts, not mail) # https://www.iana.org/go/rfc6352 # -------------------------------------------------------------------------------- # A CardDAV side syncs **contacts**, so it pairs with another contacts side or # with the store alone, never with a mailbox. Requires the `dav` cargo feature, # in the default set, which covers CalDAV too. The URL is the entry point only: # the principal and home set are discovered, each address book a collection. # # Bare authority (implicit `https://`), or a full `http://` / `https://` URL. #carddav.server = "dav.example.org" #carddav.server = "dav.example.org:8443" #carddav.server = "https://dav.example.org/dav/" # Pick exactly one of `basic` (the common case) or `bearer`. #carddav.auth.basic.username = "user" #carddav.auth.basic.password.command = ["pass", "show", "example/dav"] #carddav.auth.bearer.token.command = ["ortie", "-a", "dav", "token", "show", "-r"] # TLS mirrors the imap.tls block above. ALPN defaults to ["http/1.1"]. #carddav.tls.provider = "rustls" #carddav.alpn = ["http/1.1"] # Cards are mutable, unlike mail: `item.update` gates in-place edits, and a # conditional write reports a card edited on both sides as a conflict. #carddav.item.update = true #carddav.item.create = true #carddav.item.delete = false #carddav.pool-size = 4 # -------------------------------------------------------------------------------- # CalDAV config (calendar, not mail) # https://www.iana.org/go/rfc4791 # -------------------------------------------------------------------------------- # A CalDAV side syncs **calendar**, on exactly the terms the CardDAV block above # describes: same fields, same `dav` cargo feature, same adapter underneath. # # The item is the calendar object **resource**, not the component: RFC 4791 # keeps every component sharing a `UID` in one resource, so a recurring series # and its overrides are one item and an override is a body edit. # # Bare authority (implicit `https://`), or a full `http://` / `https://` URL. #caldav.server = "dav.example.org" #caldav.server = "dav.example.org:8443" #caldav.server = "https://dav.example.org/dav/" # Pick exactly one of `basic` (the common case) or `bearer`. #caldav.auth.basic.username = "user" #caldav.auth.basic.password.command = ["pass", "show", "example/dav"] #caldav.auth.bearer.token.command = ["ortie", "-a", "dav", "token", "show", "-r"] # TLS mirrors the imap.tls block above. ALPN defaults to ["http/1.1"]. #caldav.tls.provider = "rustls" #caldav.alpn = ["http/1.1"] # Calendar resources are mutable, like cards: `item.update` gates in-place # edits, and a conditional write reports an event edited on both sides as a # conflict. #caldav.item.update = true #caldav.item.create = true #caldav.item.delete = false #caldav.pool-size = 4 # -------------------------------------------------------------------------------- # Conflict options # -------------------------------------------------------------------------------- # Every run three-way merges a conflicted card or event against the base the # last sync agreed on, and clears the conflict when nothing collided. Both # sides setting one field two ways parks the item, and the run exits 2. # # Neverest raises no notification of its own: `--json` carries `conflicts`, # what this run marked, and `outstandingConflicts`, what the store holds. # Testing the first notifies on entry, once, with no state to keep: # # neverest sync --json | jq -e '.conflicts | length > 0' >/dev/null \ # && notify-send "neverest" "an item needs a decision" # The interactive merger `conflict resolve --interactive` hands a collision # to. UNSET by default, and never reached from a sync. # # The four paths are appended git-mergetool style: base, the two sides, then # the path to write. A command naming {base}, {local}, {remote} or {output} is # substituted instead, which is what a tool taking its output as a flag needs. #conflict.merger = "tcard merge {base} {local} {remote} --output {output}" #conflict.merger = "tcal merge {base} {local} {remote} --output {output}" # # A tool taking the four in order needs nothing but its own name: #conflict.merger = "my-merge-tool" # # Taken only on a zero exit with the output written, and only when it is a # body of that item: one no parser reads and one stating another UID are # refused. A decision is refused when the store saw a newer remote revision. # -------------------------------------------------------------------------------- # Store options # -------------------------------------------------------------------------------- # Where pimdir.db and `objects/` live. Defaults to neverest// under # the platform's state location: $XDG_STATE_HOME on Linux and the BSDs, # ~/Library/Application Support on macOS, %LOCALAPPDATA% on Windows. #store.root = "~/.local/state/neverest/example" # What the store keeps is `retain`, at the top of this account. Dropping it from # true to false does NOT delete the bodies already stored: they stay, # unreferenced, until an explicit `pimdir gc` or `neverest sync --reset`. # # CAUTION: with `retain = true` and a target, the store is a backup rather than a # cache, and `neverest sync --reset` destroys it along with the rest. # The store never truly deletes: an item whose last binding vanishes is retained, # hidden but kept with its body. This is how long before a sync reclaims it: an # integer plus `s`, `m`, `h`, `d` or `w`. UNSET never purges, `"0"` purges at # once, `sync --no-purge` skips one run, `pimdir` restores retained items. #store.purge-after = "90d" # BACKUP RECIPE: a read-only source plus no purge, so a remote expunge cannot # lose anything. The item stays in the store, restorable, until you purge on # purpose: # # imap.item.delete = false # imap.collection.delete = false # # store.purge-after left unset # -------------------------------------------------------------------------------- # Submission send channel (SMTP) # -------------------------------------------------------------------------------- # The server a source's queued `submit` intents are sent through at the start of # every sync. A frontend enqueues one through the store's action queue; there is # no reserved Outbox collection. A source either sends by itself (Graph, through # sendMail) or needs this, and AT MOST ONE per account may declare it. # # An SMTP 4xx leaves the intent pending for the next run, a 5xx parks it. # Sending is at-least-once, so `Message-ID` deduplication is the provider's job. # # The block mirrors the `imap` one above field for field: a bare authority # (implicit `smtps://`) or a full URL, the same `tls` table, one `sasl` # mechanism. #smtp.server = "smtp.fastmail.com" #smtp.server = "smtps://smtp.fastmail.com:465" #smtp.server = "smtp://smtp.example.com:587" #smtp.starttls = true # TLS mirrors the imap.tls block above. #smtp.tls.provider = "rustls" #smtp.tls.rustls.crypto = "ring" #smtp.tls.cert = "/path/to/custom/cert.pem" # Offered during the TLS handshake (rustls only). Defaults to ["smtp"], `[]` # skips ALPN. #smtp.alpn = ["smtp"] #smtp.alpn = [] # Pick exactly one SASL mechanism, spelled as under `imap.sasl`. Omit the whole # `sasl` table for an unauthenticated relay, which stops after EHLO and sends no # AUTH at all. #smtp.sasl.anonymous.message = "neverest" #smtp.sasl.plain.username = "pimalaya@fastmail.org" #smtp.sasl.plain.password.command = ["pass", "show", "pimalaya/fastmail-imap-pop-smtp"] #smtp.sasl.login.username = "user@example.com" #smtp.sasl.login.password.raw = "***" #smtp.sasl.oauthbearer.username = "user@example.com" #smtp.sasl.oauthbearer.token.command = ["ortie", "token", "read", "example"] #smtp.sasl.xoauth2.username = "user@example.com" #smtp.sasl.xoauth2.token.raw = "***" #smtp.sasl.scram-sha-256.username = "user@example.com" #smtp.sasl.scram-sha-256.password.raw = "***" # -------------------------------------------------------------------------------- # Named sources (the explicit form) # -------------------------------------------------------------------------------- # # Everything above is the sugar: `imap.server` is `sources.imap.imap.server`, # the source taking its protocol as its name. The two spellings are the same # configuration, source id included. # # Reach for the explicit table when you need two sources of one protocol. The # map key is the source name, which is the pimdir source id every binding it # owns is recorded under: RENAMING ONE ORPHANS THEM. `targets` is the same map. # # MIGRATE, one way: the source is the truth and the target is made to match it. # Anything changed on the target alone is overwritten on the next run. # # [accounts.migrate] # one-way = true # sources.old.imap.server = "imap.old-provider.com" # sources.old.imap.sasl.plain.username = "user@old-provider.com" # sources.old.imap.sasl.plain.password.command = "pass show old" # targets.new.imap.server = "imap.new-provider.com" # targets.new.imap.sasl.plain.username = "user@new-provider.com" # targets.new.imap.sasl.plain.password.command = "pass show new" # # MIRROR, two ways: drop `one-way` and the same pair keeps each other in step. # Two authoritative remotes can genuinely disagree, so a divergence is reported # as a conflict rather than resolved. # # SIDE BY SIDE: several sources and no target, caching into one store without # ever writing to each other. This is the default, so it is what you get by # saying nothing. # # [accounts.personal] # sources.fastmail.imap.server = "imap.fastmail.com" # sources.fastmail.imap.sasl.plain.username = "user@fastmail.com" # sources.fastmail.imap.sasl.plain.password.command = "pass show fastmail" # sources.gmail.imap.server = "imap.gmail.com" # sources.gmail.imap.sasl.xoauth2.username = "user@gmail.com" # sources.gmail.imap.sasl.xoauth2.token.command = ["ortie", "token", "read", "gmail"] # # `neverest sync --source ` narrows a run to the named sources.