Technical Documentation: pimalaya/himalaya
βΉοΈ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference
Overview (`README.md`)
Table of contents
- Features
- Installation
- Configuration
- Usage
- Interfaces
- FAQ
- AI policy
- License
- Social
- Contributing
- Sponsoring
Features
- Shared API for
mailboxes,envelopes,flags,messagesandattachments - Protocol-specific APIs exposing each backend's full surface
- IMAP, SMTP, ManageSieve, JMAP, Gmail (REST API), Microsoft Graph (Outlook / Microsoft 365) support
- Maildir specs, m2dir specs, mbox specs, pimdir specs support
- Simple auth support for IMAP, SMTP and ManageSieve (anonymous, login, plain, oauthbearer, xoauth2, scram-sha-256)
- HTTP auth support for JMAP: basic, bearer
- TLS support:
- Rustls with ring crypto
- Rustls with aws crypto (requires
rustls-awsfeature) - Native TLS (requires
native-tlsfeature)
- Discovery support:
- SOCKS5, HTTP proxy support, per account or via
$ALL_PROXYand$HTTPS_PROXY - TOML configuration with multi-account support
- JSON output via
--json
[!TIP]
Himalaya is written in Rust and uses cargo features to gate backend support. The default feature set is declared in Cargo.toml.The default
wizardfeature brings the interactiveconfigure; leave it out of a scripted or embedded build that never prompts.The default
vendoredfeature builds SQLite from source; leave it out (--no-default-features --features β¦) to link the system one.
Installation
Pre-built binary
Himalaya can be installed with the installer:
As root:
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | sudo shAs a regular user:
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local shThese commands install the latest binary from the GitHub releases section.
For a more up-to-date version than the latest release, check out the releases GitHub workflow and look for the Artifacts section. These pre-built binaries are built from the master branch.
[!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/himalaya.gitWith only IMAP+SMTP support:
cargo install --locked --git https://github.com/pimalaya/himalaya.git \
--no-default-features \
--features imap,smtp,rustls-ringArch Linux
From the community repository:
pacman -S himalayaOr the user repository:
git clone https://aur.archlinux.org/himalaya-git.git
cd himalaya-git
makepkg -iscOr with yay:
yay -S himalaya-gitHomebrew
brew install himalaya[!NOTE]
Cargo features are not compatible with brew. If you need a different feature set, please use another installation method.
Scoop
scoop install himalayaFedora Linux/CentOS/RHEL
From the COPR repo:
dnf copr enable atim/himalaya
dnf install himalayaNix
If you have the Flakes feature enabled:
nix profile install github:pimalaya/himalayaOr run without installing:
nix run github:pimalaya/himalayaSources
Requires Rust 1.89 or later:
git clone https://github.com/pimalaya/himalaya
cd himalaya
cargo build --release --lockedThe binary lands at target/release/himalaya.
Configuration
Run himalaya. With no configuration file on disk, the wizard asks for an email address and discovers the services reachable from it, probing PACC, Thunderbird Autoconfiguration, RFC 6186 SRV and RFC 8620 JMAP resolution in parallel.
Picking one prompts the authentication it advertises, tests the connection, then writes the account to disk.
A persistent configuration is loaded from the first valid path among:
$XDG_CONFIG_HOME/himalaya/config.toml$HOME/.config/himalaya/config.toml$HOME/.himalayarc
These are the same paths the himalaya-tui TUI looks at: one TOML file backs both binaries. CLI-only fields and TUI-only sections coexist without errors. See config.sample.toml for a documented template.
Override the path with -c ; multiple paths can be passed at once, separated by :. The first one is the base and the rest are deep-merged on top.
To add or reconfigure an account, run the bare himalaya wizard again and redirect its printed TOML into (or merge it with) your config file. There is no in-place edit subcommand; himalaya account list and himalaya account check inspect and validate the accounts already declared.
Proton Mail
Proton does not expose IMAP/SMTP directly: run Proton Bridge, which synchronizes mail locally and serves it on a local IMAP/SMTP endpoint. The password is the one generated by the Bridge, not your Proton account password.
[accounts.proton]
imap.server = "imap://127.0.0.1:1143"
imap.sasl.plain.username = "[email protected]"
imap.sasl.plain.password.command = "pass show proton-bridge"
smtp.server = "smtp://127.0.0.1:1025"
smtp.sasl.plain.username = "[email protected]"
smtp.sasl.plain.password.command = "pass show proton-bridge"To keep TLS on the local link, export the certificate generated by the Bridge and enable STARTTLS:
imap.starttls = true
imap.tls.cert = "/path/to/exported/cert.pem"
smtp.starttls = true
smtp.tls.cert = "/path/to/exported/cert.pem"Fastmail
Fastmail needs an app password for IMAP/SMTP, or an API token for its native JMAP endpoint.
[accounts.fastmail]
imap.server = "imaps://imap.fastmail.com"
imap.sasl.plain.username = "[email protected]"
imap.sasl.plain.password.command = "pass show fastmail"
smtp.server = "smtps://smtp.fastmail.com"
smtp.sasl.plain.username = "[email protected]"
smtp.sasl.plain.password.command = "pass show fastmail"To use JMAP instead, replace the imap/smtp blocks with a single jmap one:
jmap.server = "https://api.fastmail.com/jmap/session"
jmap.auth.bearer.token.command = "pass show fastmail"Gmail
Gmail rejects the account password over SASL PLAIN: generate an app password (requires 2-step verification) and feed it through password.command or password.raw.
[accounts.gmail]
imap.server = "imaps://imap.gmail.com:993"
imap.sasl.plain.username = "[email protected]"
imap.sasl.plain.password.command = "pass show gmail"
smtp.server = "smtps://smtp.gmail.com:465"
smtp.sasl.plain.username = "[email protected]"
smtp.sasl.plain.password.command = "pass show gmail"
mailbox.alias.inbox = "INBOX"
mailbox.alias.sent = "[Gmail]/Sent Mail"
mailbox.alias.drafts = "[Gmail]/Drafts"
mailbox.alias.trash = "[Gmail]/Trash"
mailbox.alias.archive = "[Gmail]/All Mail"Every Gmail label shows up as a top-level IMAP mailbox, and the special ones live under the [Gmail]/ prefix, so quote them in the shell or reach them through an alias.
[Gmail]/All Mail is the archive holding every message, so aliasing it puts "search everything" one flag away: himalaya envelope search -m archive ....
To use Gmail's native REST API instead of IMAP/SMTP, replace the blocks above with a single OAuth 2.0 bearer token from a helper such as ortie:
gmail.auth.token.command = ["ortie", "token", "show", "-a", "gmail"]Labels become mailboxes here too, but they carry the API's opaque label ids; address them by name (-m Himalaya-Test) and himalaya resolves the id.
Outlook
Microsoft has retired basic authentication: use OAuth 2.0 via oauthbearer or xoauth2, with the access token supplied by an external helper such as ortie.
[accounts.outlook]
imap.server = "imaps://outlook.office365.com:993"
imap.sasl.xoauth2.username = "[email protected]"
imap.sasl.xoauth2.token.command = ["ortie", "token", "show", "-a", "outlook"]
smtp.server = "smtp://smtp-mail.outlook.com:587"
smtp.starttls = true
smtp.sasl.xoauth2.username = "[email protected]"
smtp.sasl.xoauth2.token.command = ["ortie", "token", "show", "-a", "outlook"]To use the native Microsoft Graph API instead of IMAP/SMTP, replace the blocks above with a single OAuth 2.0 bearer token (sending goes through Graph too, so no SMTP is needed):
msgraph.auth.token.command = ["ortie", "token", "show", "-a", "msgraph"]Mail folders carry Graph's opaque folder ids; address them by name (-m Archive) or by a well-known name (-m inbox) and himalaya resolves the id.
Posteo
Standard IMAP and SMTP with your regular account password: no app password required.
[accounts.posteo]
imap.server = "imaps://posteo.de"
imap.sasl.plain.username = "[email protected]"
imap.sasl.plain.password.command = "pass show posteo"
smtp.server = "smtps://posteo.de"
smtp.sasl.plain.username = "[email protected]"
smtp.sasl.plain.password.command = "pass show posteo"iCloud Mail
Per the iCloud Mail support page, the IMAP login is the name of your address, johnappleseed rather than [email protected], while the SMTP login is the full address.
A dedicated app-specific password is required.
[accounts.icloud]
imap.server = "imaps://imap.mail.me.com:993"
imap.sasl.plain.username = "johnappleseed"
imap.sasl.plain.password.command = "pass show icloud"
smtp.server = "smtp://smtp.mail.me.com:587"
smtp.starttls = true
smtp.sasl.plain.username = "[email protected]"
smtp.sasl.plain.password.command = "pass show icloud"
mailbox.alias.sent = "Sent Messages"Usage
Every command carries its own --help, the source of truth for its flags and syntax. The snippets below are a taste of the surface.
Shared API
Backend-agnostic commands run on the account's first configured backend, or the one picked with -b/--backend. -m/--mailbox takes a mailbox id, name, alias or role (inbox, sent, drafts, trash...), and defaults to the inbox.
himalaya mailbox list
himalaya envelope list --page 2
himalaya envelope search from alice and after 2026-01-01 order by date desc
himalaya flag add --flag seen 1 2 3 5
himalaya message read 42
himalaya message copy --from INBOX --to Archives 42
himalaya attachment download 42envelope search uses himalaya's own cross-backend query DSL; its grammar lives in himalaya envelope search --help.
Protocol-specific APIs
Each backend also exposes its full native API under its own subgroup, always against that backend (-b/--backend is ignored here):
himalaya imap raw 'a1 SEARCH FROM "[email protected]"\r\n'
himalaya jmap mailbox query --role drafts
himalaya gmail messages list -q "from:alice is:unread"
himalaya msgraph mail-folders list
himalaya smtp send -f [email protected] -t [email protected] < message.eml
himalaya sieve list
himalaya sieve check --script-file filters.sieve
himalaya sieve put main --script-file filters.sieve
himalaya sieve activate mainManageSieve uses the optional sieve account block. RFC 5804 registers one port, 4190, and reaches TLS on it through STARTTLS, so a bare server address takes sieve:// with the upgrade on where the IMAP and SMTP ones take their implicit-TLS scheme.
sieves:// is accepted too, for the deployments listening for a handshake straight away. A password or a bearer token is refused on an unencrypted connection unless sieve.allow-cleartext-auth says otherwise.
capability, list, get, put, check, activate, deactivate, rename, delete and raw are available, and put and check accept a script file, inline text or stdin.
Composing messages
message compose / reply / forward cover simple cases through flags. For rich MIME (MML directives, signing, encryption, an editor-driven flow), chain a composer such as mml into message send / message add, or stage a prepared file as a draft:
himalaya message compose --to [email protected] --subject Hello --body Hi --send
mml compose >(himalaya message send)
himalaya message add -m drafts --flag draft < message.emlSee himalaya message send --help and the mml docs for the composer chaining.
Re-using sessions
Each invocation opens a fresh TCP+TLS+SASL session. To amortize the handshake, pair himalaya with sirup: it serves a pre-authenticated IMAP/SMTP session over a Unix socket that imap.server / smtp.server can point at.
Interfaces
Himalaya CLI is one of several front-ends to the Pimalaya libraries:
- pimalaya/himalaya-tui: official TUI (in active development)
- pimalaya/himalaya-vim: Vim plugin
- dantecatalfamo/himalaya-emacs: Emacs plugin
- jns/himalaya: Raycast extension
- openclaw/openclaw: OpenClaw SKILL
- parisni/dfzf: dfzf integration
- kezhenxu94/himalaya-fzf: interactive fzf front-end
FAQ
How different is it from aerc, mutt or alpine?
Aerc, mutt and alpine can be categorized as Terminal User Interfaces (TUI). When the program is executed, your terminal is locked into an event loop and you interact with your emails using keybinds.
Himalaya is a Command-Line Interface (CLI). There is no event loop: you interact with your emails using shell commands, in a stateless way.
A dedicated TUI (himalaya-tui) is in active development on top of the same Pimalaya libraries, and is definitely closer to aerc, mutt and alpine.
How are secrets resolved?
Every *.passwd / *.password / *.token field accepts either a raw literal or a shell command that prints the secret on stdout. The raw form is convenient for testing but should not be used in production:
imap.sasl.plain.passwd.raw = "***"
imap.sasl.plain.passwd.command = "pass show example"
imap.sasl.plain.passwd.command = ["pass", "show", "example"] Native keyring support was removed in v2. Use a third-party keyring CLI (pass, secret-tool, gopassβ¦) as the command.
How is OAuth 2.0 handled?
v2 ships no OAuth flow. Use ortie, or any other token broker, to obtain an access token, then plug it in as a command printing that token on stdout.
For JMAP, point jmap.auth.bearer.token.command at the broker. For IMAP and SMTP, route the bearer through a SASL mechanism taking a command-sourced password.
How does the wizard discover IMAP/SMTP/JMAP configs?
The wizard probes several discovery mechanisms in parallel on the email address domain, merges their results and keeps the most secure endpoint per service:
- PACC draft-ietf-mailmaint-pacc-02: well-known JSON, digest-verified against the
_ua-auto-configTXT record. - Thunderbird Autoconfiguration: ISP main / well-known / ISPDB lookups, then MX-based retry, then the
mailconf=TXT redirect. - RFC 6186 SRV:
_imap._tcp,_imaps._tcp,_submission._tcplookups assembled into a single report. - RFC 8620 JMAP: the SRV record and the
/.well-known/jmapsession resource.
A final WWW-Authenticate probe refines the advertised auth schemes, and a detected Google or Microsoft account short-circuits to its dedicated configurations. See io-pim-discovery for the full chain.
How to debug Himalaya?
Use --log-level <level> (alias --log) where <level> is one of off, error, warn, info, debug, trace:
himalaya --log trace mailbox list The RUST_LOG environment variable is consulted when --log is not passed, and supports per-target filters (see the env_logger documentation). RUST_BACKTRACE=1 enables full error backtraces.
Logs are written to stderr, so they can be redirected easily to a file:
himalaya --log trace mailbox list 2>/tmp/himalaya.log You can also send logs straight to a file via --log-file :
himalaya --log trace --log-file /tmp/himalaya.log mailbox listHow to disable color output?
Set NO_COLOR=1 in your environment.
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:
Overview (`cairn/spec/testing/README.md`)
cairn: spec capability: testing status: current
Testing reports
Real-world test reports for Himalaya CLI v2, one per backend/provider,
plus the provider-agnostic composer. Each was produced by exercising
every command variant against a live account (or a throwaway local
store), following the golden rule: operate only inside fake mailboxes
created for the run, never on existing data.
Method: provider-test-plan.md.
Every report pairs a shared run (the cross-protocol mailbox /envelope / flag / message / attachment commands) with a
specific run (the raw himalaya β¦ surface), except where a
single combined report sufficed.
Network backends
| Backend | Provider | Shared | Specific |
|---|---|---|---|
| IMAP + SMTP | Fastmail | imap-smtp-fastmail.md | imap-smtp-specific-fastmail.md |
| IMAP + SMTP | Posteo | imap-smtp-posteo.md | (combined) |
| IMAP | Gmail (Google) | imap-google.md | (combined) |
| IMAP | iCloud | imap-icloud.md | (combined; IMAP-only account) |
| JMAP | Fastmail | jmap-fastmail.md | jmap-specific-fastmail.md |
| Gmail REST | gmail.md | gmail-specific.md | |
| Microsoft Graph | Outlook | msgraph.md | msgraph-specific.md |
Local backends
| Backend | Shared | Specific |
|---|---|---|
| Maildir | maildir.md | maildir-specific.md |
| m2dir | m2dir.md | m2dir-specific.md |
Provider-agnostic
| Area | Report |
|---|---|
compose / reply / forward (unit-tested, no provider) |
compose-reply-forward.md |
Status
Every bug surfaced by these runs was fixed in-tree (mailbox nameβid
resolution across JMAP/Gmail/Graph/Maildir/m2dir, copy/move counts,
CRLF/empty-body handling, the Maildir/m2dir double-join and variadic-flag
issues, the has_prefix reply/forward subject bug, β¦). The only
library-level fixes were in io-imap (untagged MOVE COPYUID; and the
fallback Date:-sort fix below) and io-gmail (empty 2xx body parsing),
all tracked for release; everything else was himalaya-side.
The iCloud run (imap-icloud.md) surfaced two sort
issues: iCloud's server SORT is a no-op (advertised but ignores key +
REVERSE β a provider bug; workaround imap.sort.fallback = true), and the
io-imap client-side sort fallback ordered the date key by the rawDate: header string lexically instead of chronologically. The latter is
fixed in io-imap (cmp_fetch_items now parses the header viachrono::DateTime::parse_from_rfc2822), verified against iCloud.
Everything else on iCloud passed.
Not yet tested: Proton Bridge; SMTP on iCloud (that account is IMAP-only).
ManageSieve protocol coverage lives in io-managesieve since 2026-08-25: its
scripted server asserts the exact bytes every command sends and answers
greeting capability lines, quoted and literal script names, GETSCRIPT
literals, PUTSCRIPT warnings, activation, renaming, deletion, and logout.
This repository keeps only the tests for the sieve config block and its
server-URL scheme table.
The 2026-08-24 Dovecot Pigeonhole smoke test against [email protected] passed
STARTTLS, PLAIN authentication, CAPABILITY, LISTSCRIPTS, andaccount check --backend sieve. The initial list was empty because the
existing .dovecot.sieve was a regular file outside the personal store. In
an explicitly authorized migration test, Himalaya uploaded the same rules asmain and activated it; ManageSieve then listed main as active anddovecot.orig as the preserved inactive copy. GETSCRIPT verified both
contents. No script was deleted.
The same migration path was verified on 2026-08-24 for [email protected] onmail1.anycast.io. Its pre-existing regular .dovecot.sieve contained the
invoice forwarding rule. Himalaya uploaded it as main and activated it;
Dovecot converted .dovecot.sieve into its managed sieve/main.sieve
symlink and preserved the previous file as inactive dovecot.orig. Himalaya
and doveadm returned the same script contents. No manual symlink or direct
filesystem edit was used.
--- METRICS ---
- Files Extracted: 2
- Estimated Token Budget: ~6220 tokens
- Canonical Reference: https://codewiki.google/github.com/pimalaya/himalaya