MIGRATION.md
himalaya imap/jmap/maildir/smtp …) expose the full native capability of each protocol.pass, secret-tool, gopass, security, …).A direct consequence: the v2 binary is about three times smaller than v1!
Pimalaya has been working for the past year on an adaptation of the Sans I/O pattern for its libraries. The pattern decouples the protocol state machine from any specific I/O runtime: sync vs async, tokio vs async-std vs smol, rustls vs native-tls. The concept has been validated in pimalaya/ortie, pimalaya/cardamum and pimalaya/calendula, and is now wired into Himalaya CLI v2.
As a direct consequence, TLS is selectable at build time between native-tls and rustls (with aws-lc or ring as the crypto provider).
| v1 | v2 |
|---|---|
-o, --output {plain,json} | --json only |
--quiet / --debug / --trace | --log-level {off,error,warn,info,debug,trace} (alias --log) |
-f, --folder | -m, --mailbox |
New in v2: -b, --backend (force a specific backend for shared commands) and --log-file <PATH> (write logs straight to a file).
folders to mailboxes.add, expunge, purge, delete: these are rarely useful at the interface level (Emacs, Vim plugin, TUI). Use the protocol-specific subcommands instead (himalaya imap create, himalaya imap expunge, himalaya imap delete).--counts to list to populate per-mailbox message counts.thread moved to the protocol-specific APIs.list -f|--folder INBOX becomes list -m|--mailbox INBOX. The flag is optional: when omitted, the id mapped to the inbox alias under [mailbox.alias] is used.before <date> clause. The remaining operators (and, or, not, parens) and the sort suffix (order by date|from|to|subject [asc|desc]) are unchanged. Backends advertise the subset they accept, and unsupported clauses fail at parse time. It is now accessible from the search command instead of list.envelope.list.page-size (per-account, with global fallback). The -s/--page-size CLI flag still wins when passed. Hard fallback when neither is set: 25.--folder becomes -m|--mailbox <NAME> (optional, same default as envelopes list).<id-or-flags> split into -f, --flag <FLAG> (repeatable) and a positional <message-ids>.delete: too protocol-specific. Use the matching protocol-specific subcommand, or combine flags add with the per-protocol expunge / move-to-trash step.copy and move: --folder <source> renamed --from <mailbox-id>, and positional <target> renamed --to <mailbox-id>.save renamed add (kept as an alias, so save still works).save --folder (optional) becomes add --mailbox (mandatory).save --file <PATH> / positional split is gone: add now takes a single MessageArg (a positional file path, an inline raw value, or piped stdin).add --flag to attach flags at insertion time, and add --send (alias save --send) to push the message through the account's send path after the append.write renamed compose (kept as an alias). compose, reply, forward are no longer interactive. They build the message from CLI flags through the built-in flag composer. Interactive composition is delegated to standalone tools chained into messages send / messages add via a tempfile or shell process substitution. No *-with subcommands or [message.composer.*] table remain on the himalaya side.read no longer renders human-readable text. The v2 read prints message-level info. For custom rendering, pipe read --raw into a standalone interpreter.mailto: URI handling is no longer a himalaya subcommand. Register a small shell wrapper (e.g. mml mailto "$1" --output /tmp/draft.eml && himalaya messages send /tmp/draft.eml) as your desktop mailto handler.messages send and messages add read the raw message from a positional path, an inline raw value, or stdin (the unified MessageArg).export and edit are removed.See pimalaya/mml for a ready-to-use composer / interpreter.
download --folder becomes -m|--mailbox <NAME> (optional, same default as envelopes list).--downloads-dir renamed --dir.<attachment-id> positional to download (omit to download every attachment, preserving the v1 behaviour).list subcommand.Fully removed. The template pipeline (compose / reply / forward drafts, MML compile, MIME interpret) lives in pimalaya/mml as both a library and a CLI. Chain its CLI into messages send / messages add (see the README).
The full configuration schema is documented in config.sample.toml. The notes below focus on what changed since v1.
display-name, signature, signature-delim: composition left the CLI.downloads-dir remains for the attachments download command.message, template and pgp top-level entries are removed. Composition and rendering happen outside himalaya now (see the README for the recommended shell-pipeline shapes).{account,folder,envelope}.list.table.{preset,arrangement} keys collapse into a single table.{preset,arrangement} (global / per-account). table.arrangement (dynamic, dynamic-full-width, disabled) is new.folder.list.table.* → mailbox.list.table.* (mirrors the folders → mailboxes command rename).envelope.list.table.sender-color → envelope.list.table.from-color (the column is now FROM).The v1 [folder.alias] block becomes [mailbox.alias]. Two behaviour changes on top of the rename:
INBOX = "...", Inbox = "..." and inbox = "..." are equivalent entries.inbox (case-insensitive) is the implicit default mailbox: shared commands fall back to its id when -m/--mailbox is omitted. No separate default-mailbox key.Account-level [accounts.<name>.mailbox.alias] entries override same-named global [mailbox.alias] entries.
Every *.passwd / *.password / *.token field accepts either a raw literal ({ raw = "…" }) or a shell command ({ command = "pass show foo" } or { command = ["pass", "show", "foo"] }). Native keyring support has been removed. Use a password-manager CLI (pass, secret-tool, gopass, security, …) as the command. OAuth tokens are produced by an external broker such as pimalaya/ortie and consumed the same way.
The whole backend.type = "imap" block collapses into:
# Either a bare authority (treated as `imaps://<authority>`) or a full
# URL with `imap://` or `imaps://`. Mirrors `jmap.server`. A
# `unix:///path` scheme connects through a pre-authenticated session
# proxy such as sirup, over which no SASL is negotiated.
imap.server = "example.com"
# or imap.server = "imaps://example.com:993"
# or imap.server = "imap://example.com:143" (use imap.starttls = true to upgrade)
# or imap.server = "unix:///run/sirup/example.sock"
imap.tls.provider = "rustls" # or "native-tls"
imap.tls.rustls.crypto = "ring" # or "aws"
imap.tls.cert = "/path/to/custom/cert.pem"
imap.starttls = false
# Pick exactly one SASL mechanism. Omit the whole `imap.sasl` table to
# skip authentication entirely.
# SASL ANONYMOUS
imap.sasl.anonymous.message = "himalaya"
# SASL PLAIN
imap.sasl.plain.authcid = "[email protected]"
imap.sasl.plain.passwd.raw = "***"
# or
imap.sasl.plain.passwd.command = ["pass", "show", "example"]
# SASL LOGIN
imap.sasl.login.username = "[email protected]"
imap.sasl.login.password.raw = "***"
# SASL OAUTHBEARER (RFC 7628)
imap.sasl.oauthbearer.username = "[email protected]"
imap.sasl.oauthbearer.host = "imap.example.com"
imap.sasl.oauthbearer.port = 993
imap.sasl.oauthbearer.token.command = ["ortie", "token", "read", "example"]
# SASL XOAUTH2 (Google)
imap.sasl.xoauth2.username = "[email protected]"
imap.sasl.xoauth2.token.raw = "***"
# SASL SCRAM-SHA-256 (RFC 7677)
imap.sasl.scram-sha-256.username = "[email protected]"
imap.sasl.scram-sha-256.password.raw = "***"
The OAuth-specific section (backend.auth.type = "oauth2") is gone. Route the access token through SASL oauthbearer or xoauth2 (with a command-sourced token from a broker such as pimalaya/ortie) instead.
Same shape as IMAP, rooted at [smtp]. Bare authority defaults to smtps://. The v1 message.send.backend.type = "smtp" block becomes smtp.server, smtp.tls.*, smtp.starttls, smtp.sasl.* with the same SASL variants as IMAP.
maildir.root = "~/Mail/example"
An m2dir store: content-addressed messages with sidecar flag metadata.
m2dir.root = "~/Mail/example"
jmap.server = "fastmail.com"
# or
jmap.server = "https://api.fastmail.com/jmap/session"
jmap.tls.provider = "rustls" # or "native-tls"
jmap.tls.rustls.crypto = "ring" # or "aws"
jmap.tls.cert = "/path/to/custom/cert.pem"
# Pick exactly one of `header`, `bearer`, `basic`.
# Raw "Authorization" header value, used verbatim
jmap.auth.header.raw = "Bearer eyJhbGciOiJ..."
jmap.auth.header.command = "pass show fastmail-raw-token"
# OAuth 2.0 / API token bearer
jmap.auth.bearer.token.raw = "***"
# or
jmap.auth.bearer.token.command = ["pass", "show", "fastmail-api"]
# HTTP Basic
jmap.auth.basic.username = "[email protected]"
jmap.auth.basic.password.raw = "***"
# or
jmap.auth.basic.password.command = "pass show fastmail"
# Optional for `messages send` over JMAP: auto-discovered when omitted.
# Set them to pin a specific identity / drafts mailbox.
jmap.identity-id = "I0123abc"
jmap.drafts-mailbox-id = "M0123abc"
The native Gmail backend authenticates with a single OAuth 2.0 bearer token from a broker such as pimalaya/ortie. Labels are the mailboxes, addressed by name.
gmail.auth.token.command = ["ortie", "token", "show", "-a", "gmail"]
# or gmail.auth.token.raw = "***"
Same shape as Gmail: a single OAuth 2.0 bearer token. Sending goes through Graph, so no smtp block is needed.
msgraph.auth.token.command = ["ortie", "token", "show", "-a", "msgraph"]
Both backends are removed. Notmuch may come back in a future release.
himalaya -c ~/.config/himalaya/config.v2.toml account check to validate the connection for each declared backend.pass, secret-tool, …) and/or pimalaya/ortie as command = … secrets.write / reply / forward, install pimalaya/mml and chain it into himalaya messages send / messages add via a tempfile or >(...) process substitution (see the README for ready-made bash/zsh snippets).