Skip to content
pimalayaPublic

About

CLI to manage contacts

Topics

Resources

Contributing

Security policy

Stars

50 stars

Watchers

2 watching

Forks

Repository files navigation

📇 Cardamum crates.io Matrix Mastodon Sponsor

CLI to manage contacts, written in Rust

Caution

Cardamum is v0.x: expect breaking changes between releases until it stabilises.

Table of contents

Features

  • Shared API: addressbook and card work the same whichever backend serves the account.
  • Protocol-specific APIs: carddav, jmap, msgraph, gpeople and vdir each expose what only that backend has, down to a raw request escape hatch.
  • Offline store report: pimdir status shows what the local pimdir store holds for the account, and the queued writes its sync engine parked rather than applied.
  • Discovery: an email address is enough to find a provider's server, through SRV records, .well-known and the provider configuration documents.
  • Interactive wizard: cardamum configure turns an email address into a tested account.
  • Multi-account: one TOML file, one block per account, several files deep-merged when you want secrets apart.
  • JSON output: every command switches to JSON with --json, and cardamum json-schema describes what each one emits, for scripts and other tools.
  • Full standard, blocking client with TLS support:
    • Rustls with ring crypto (requires rustls-ring feature, enabled by default)
    • Rustls with aws crypto (requires rustls-aws feature)
    • Native TLS (requires native-tls feature)

Tip

Each backend sits behind its own cargo feature. carddav, jmap, msgraph, gpeople, vdir and pimdir are all enabled by default. Build with --no-default-features and pick the ones you need.

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 and OpenSSL from source; leave it out (--no-default-features --features …) to link the system ones.

Installation

Pre-built binary

As root:

curl -sSL https://raw.githubusercontent.com/pimalaya/cardamum/master/install.sh | sudo sh

As a regular user:

curl -sSL https://raw.githubusercontent.com/pimalaya/cardamum/master/install.sh | PREFIX=~/.local sh

These commands install the latest binary from the GitHub releases section.

For a more up-to-date version, check the releases workflow and look for the Artifacts section: those are built from master, with the default cargo features.

Cargo

cargo install --locked --git https://github.com/pimalaya/cardamum.git

With CardDAV and vdir support only:

cargo install --locked --git https://github.com/pimalaya/cardamum.git \
  --no-default-features \
  --features carddav,vdir,rustls-ring

Nix

If you have the Flakes feature enabled:

nix profile install github:pimalaya/cardamum

Or run without installing:

nix run github:pimalaya/cardamum

Sources

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

Configuration

The configuration is loaded from the first existing path among:

  • $XDG_CONFIG_HOME/cardamum/config.toml
  • $HOME/.config/cardamum/config.toml
  • $HOME/.cardamumrc

Override the path with cardamum -c <PATH> or CARDAMUM_CONFIG=<PATH>, :-separated to deep-merge several files on top of the first. The full field reference lives in config.sample.toml.

cardamum configure generates an account, and a bare cardamum offers it when it finds no configuration. It asks one question, taking an email address, a server URL or a local folder path, discovers what it can, tests the account, then writes it here.

An [accounts.<name>] block is appended when the file already exists. What discovery cannot find it does not ask for: a self-hosted server publishing no discovery record is written by hand.

Secrets come from an OS keyring, a token broker such as Ortie, any command or a raw value. Cardamum reads them but never issues or refreshes them, so a provider on OAuth 2.0 needs a broker. Ready-made blocks for common providers follow.

Apple

Apple exposes contacts via CardDAV, but you cannot use your regular password. You need to generate an app-specific password (required once two-factor authentication is on):

[accounts.example]
carddav.discover = "icloud.com"
carddav.server = "https://contacts.icloud.com/"
# The home URL is usually of this shape:
#carddav.home = "https://contacts.icloud.com/<id>/principal/"

carddav.auth.basic.username = "example@icloud.com"
carddav.auth.basic.password.raw = "***"

addressbook.default = "card"

Google

Use the gpeople backend, which speaks the People API directly and is richer than Google's CardDAV bridge. Contact groups map to addressbooks, the myContacts group listing as Contacts.

Both routes need OAuth 2.0, and an access token expires within the hour, so point the token at a broker rather than pasting one in:

[accounts.example]
gpeople.auth.token.command = ["ortie", "token", "show"]
addressbook.default = "myContacts"

Google models contacts as JSON and exposes no vCard representation, so cardamum synthesizes the document you read and re-projects what you write.

Properties with a well-defined vCard slot are authoritative in both directions, and everything else is stashed verbatim on the server so it round-trips instead of being dropped on the next write.

CardDAV works too, with a bearer token and the address book home addressed by hand, its discovery entry point being off-spec:

[accounts.example]
carddav.home = "https://www.googleapis.com/carddav/v1/principals/example@gmail.com/lists"
carddav.auth.bearer.token.command = ["ortie", "token", "show"]
addressbook.default = "default"

The two routes need different scopes, which are not interchangeable: auth/carddav authorizes CardDAV, auth/contacts the People API.

Microsoft

Microsoft offers no CardDAV, only the Graph API, which takes an OAuth 2.0 bearer token and nothing else. Contact folders map to addressbooks, the default folder listing under the contacts id:

[accounts.example]
msgraph.auth.token.command = ["ortie", "token", "show"]
addressbook.default = "contacts"

As with Google, contacts are JSON: the vCard is synthesized on read and re-projected on write, with the unmapped properties stashed server-side so they survive.

Fastmail

Standard CardDAV with the mailbox address and its app password:

[accounts.example]
carddav.discover = "fastmail.com"
carddav.server = "https://carddav.fastmail.com/dav/"
# The home URL is usually of this shape:
#carddav.home = "https://carddav.fastmail.com/dav/addressbooks/user/<email>/"

carddav.auth.basic.username = "example@fastmail.com"
carddav.auth.basic.password.raw = "***"

addressbook.default = "Default"

Fastmail also serves JMAP, bearer-token only, the token coming from the Fastmail settings. It forbids creating, updating and destroying an address book, so the addressbook write commands fail there by design:

[accounts.example]
jmap.server = "fastmail.com"
jmap.auth.bearer.token.raw = "***"

Posteo

Standard CardDAV with the mailbox address and its password:

[accounts.example]
carddav.discover = "posteo.de"
carddav.server = "https://posteo.de:8843/"
# The home URL is usually of this shape:
#carddav.home = "https://posteo.de:8843/addressbooks/<username>/"

carddav.auth.basic.username = "example@posteo.net"
carddav.auth.basic.password.raw = "***"

addressbook.default = "default"

Proton

Not supported: Proton exposes no contacts API, neither CardDAV nor through Proton Bridge, which proxies mail only. Contacts are reachable only from Proton's own web and mobile apps.

Local addressbooks

No server is involved, so nothing needs discovering. Point cardamum at a directory and it works offline.

A vdir home is one directory per addressbook, holding one .vcf file per card. This is what vdirsyncer writes, and what most local tools read:

[accounts.local]
vdir.home-dir = "~/.local/share/vdirsyncer/contacts"
addressbook.default = "personal"

A pimdir store is the offline cache a sync engine fills: a SQLite index plus content-addressed bodies, shared with the other Pimalaya clients reading the same store.

Cardamum reads it and stages card writes onto its queue, leaving the store to its owner, the sync, so a listing never waits on a sync in flight and never keeps one out.

It is a cache, not a server, so the store must already exist, addressbooks come from the sync and the collection verbs refuse here:

[accounts.cached]
pimdir.root = "~/.local/state/neverest/example"
# Required by a store grouping several accounts; unset reads the only one.
#pimdir.account = "personal"

A card that is listed but not downloaded reads as "body not fetched" until a sync hydrates it. Every write is queued and reaches the server on the next sync, and the commands say so. A queued update or deletion shows in the listing straight away; a queued create is not listed until the sync applies it, and card create reports its link id (its UID), not an id the other card commands take: the card gets one when the sync applies it. An addressbook no source syncs refuses writes, since nothing would carry them to a server. A create whose UID the addressbook already holds, synced or queued, is refused (item-duplicate under --json): update that card instead. An update made from the card it read (card update with no vCard source, or --if-match) is not applied over a server change the sync brings in first: the sync engine parks it instead. cardamum pimdir status lists every parked write of the account, with its queue row and the reason, beside how much of each addressbook is downloaded and how many creates are queued. A store of another pimdir format version than the one cardamum builds against is refused (store-version): one an earlier neverest wrote is reinstalled through neverest, its queued writes synced first, and a later one asks for a cardamum upgrade. A store another user can read is refused too (store-exposed).

Usage

Run cardamum --help for the full command tree, and cardamum <command> --help for any subcommand's arguments and its JSON output shape (printed when the global --json flag is set).

A few real command lines:

cardamum configure
cardamum addressbook list
cardamum card list --addressbook personal
cardamum card read --addressbook personal <CARD-ID>
cardamum card create --addressbook personal ada.vcf
cardamum -a work -b carddav card list
cardamum carddav report sync personal
cardamum jmap contact-card changes <SINCE-STATE>
cardamum gpeople connection list --sync-token <TOKEN>
cardamum msgraph contacts delta --folder contacts

Logs go to stderr, so they can be redirected to a file while the command output stays on stdout:

cardamum card list --log-level debug 2>/tmp/cardamum.log

Use --log-file <PATH> to append them to a file directly. When --log-level is omitted the RUST_LOG environment variable is consulted, and RUST_BACKTRACE=1 adds the full error backtrace.

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

About

CLI to manage contacts

Topics

Resources

Contributing

Security policy

Stars

50 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Contributors

Languages