pimalaya/cardamum
pimalaya/cardamum has 50 GitHub stars and 6 forks. Star changes are not measured yet. Last checked 2026-11-09 09:09 UTC. Last push 2026-11-08.
CLI to manage contacts
What you can do
- 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 and search: pimdir status shows what the local pimdir store holds for the account, and the queued writes its sync engine parked rather than applied; pimdir search and pimdir count find cards by any word of them through the store's search index.
- Discovery: an email address is enough to find a provider's server, through SRV records, .well-known and the provider configuration documents.
Measured star growth
- in 24h
- Collecting history
- in 7 days
- Collecting history
- in 15 days
- Collecting history
- in 1 month
- Collecting history
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
README.md
Original📇 Cardamum

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:
addressbookandcardwork the same whichever backend serves the account. - Protocol-specific APIs:
carddav,jmap,msgraph,gpeopleandvdireach expose what only that backend has, down to a raw request escape hatch. - Offline store report and search:
pimdir statusshows what the local pimdir store holds for the account, and the queued writes its sync engine parked rather than applied;pimdir searchandpimdir countfind cards by any word of them through the store's search index. - Discovery: an email address is enough to find a provider's server, through SRV records,
.well-knownand the provider configuration documents. - Interactive wizard:
cardamum configureturns 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, andcardamum json-schemadescribes what each one emits, for scripts and other tools. - Full standard, blocking client with TLS support:
- Rustls with ring crypto (requires
rustls-ringfeature, enabled by default) - Rustls with aws crypto (requires
rustls-awsfeature) - Native TLS (requires
native-tlsfeature)
- Rustls with ring crypto (requires
[!TIP] Each backend sits behind its own cargo feature.
carddav,jmap,msgraph,gpeople,vdirandpimdirare all enabled by default. Build with--no-default-featuresand pick the ones you need.The default
wizardfeature brings the interactiveconfigure; leave it out of a scripted or embedded build that never prompts.The default
vendoredfeature 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, then picks the best way to your contacts it discovered (your provider's own API, JMAP, then CardDAV) and shows it in plain words, the alternatives one change away. A password or an API token is typed once, tested at once, then stored in your keyring; OAuth 2.0 hands off to Ortie when it is installed. When nothing is discovered, it asks for the server URL instead (https://, carddavs:// or jmaps://, encrypted servers only).
The account is then written here, an [accounts.<name>] block appended when the file already exists.
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 = "[email protected]"
carddav.auth.basic.password.raw = "***"
addressbook.default = "card"
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/[email protected]/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 = "[email protected]"
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 = "[email protected]"
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).
cardamum pimdir search <query> searches the account's cards through the store's search index, which the sync engine builds and refreshes after each run: free text reaches names, organization, notes and phone numbers, and the pimdir query language adds terms such as email:@example.org, has:phone, has:birthday, category:friends and and, or, not. It spans every addressbook of the account unless -k names one, sorts by name unless --sort relevance, and pimdir count takes the same query. The answer is the store as of the last refresh, so a queued write is not in it, and a line under the table says what it could not see: cards whose body is not local, matched on their summary alone, or an index lagging the store. Cardamum never builds the index: a store without one fails with index-missing until the sync engine runs, and a refused query is drawn with its fault (query-refused).
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
- Chat on Matrix
- News on Mastodon or RSS
- Mail at [email protected]
Sponsoring
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
- 2022 → 2023: NGI Assure
- 2023 → 2024: NGI Zero Entrust
- 2024 → 2026: NGI Zero Core
- 2026 → 2027: NGI Zero Commons Fund
This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider sponsoring its development:
Explore related projects
codecrafters-io/build-your-own-x
Master programming by recreating your favorite technologies from scratch.
561,256 starspublic-apis/public-apis
A collective list of free APIs
497,513 starsmorluto/rea
Reverse engineer anything with agents, from app behavior down to native binaries.
445,836 starsopenclaw/openclaw
The AI that really does things. Any OS. Any Platform. The lobster way. 🦞
394,231 stars