13 KiB
Migration guide
From v1 to v2
v1 issues
- Backend abstraction was overkill. A unifying trait across IMAP/Maildir/Notmuch/Sendmail looked tidy on paper but was a maintenance tax in practice.
- The shared API restricted every protocol. Keeping a single surface across very different backends meant either lossy shortcuts or confusing behaviour, and made adding a new backend risky.
- OAuth was painful to configure and renew (token refresh, retry, keyring round-trips, vendor quirks).
- Native keyring integration was hazardous. Platform-specific bugs, silent failures, locked sessions.
- The configuration was verbose. Deserialization errors were clear, but the per-account schema kept growing.
- Composition was tangled with MML. The boundary between "message" and "template" commands was unclear, and plugging a custom composer or reader was hard.
- Native-TLS and Rustls did not coexist cleanly.
- Each command opened a fresh IMAP/SMTP session. TCP, TLS, SASL, capability negotiation, every time.
v2 changes
- Deep refactor on top of the I/O-free pattern. No async in the CLI, no backend abstraction. Both the binary and the underlying libraries are simpler.
- Thinner shared API. Just enough surface for interfaces (TUI, plugins) to drive any backend, kept deliberately small so it does not break on every release.
- Protocol-specific commands (
himalaya imap/jmap/maildir/smtp …) expose the full native capability of each protocol. - OAuth moved out to pimalaya/ortie.
- Keyring moved out to any password manager exposed as a shell command (
pass,secret-tool,gopass,security, …). - Composition and reading moved out to pimalaya/mml.
- Session reuse moved out to pimalaya/sirup, which exposes a pre-authenticated IMAP/SMTP session over a Unix socket.
A direct consequence: the v2 binary is about three times smaller than v1!
Foundations: I/O-free
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).
CLI changes
Global flags
| 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
- Renamed
folderstomailboxes. - Removed
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). - Added
--countstolistto populate per-mailbox message counts.
Envelopes
threadmoved to the protocol-specific APIs.list -f|--folder INBOXbecomeslist -m|--mailbox INBOX. The flag is optional: when omitted, the id mapped to theinboxalias under[mailbox.alias]is used.- The v1 search query grammar drops the
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 thesearchcommand instead oflist. - Default page size moves to
envelope.list.page-size(per-account, with global fallback). The-s/--page-sizeCLI flag still wins when passed. Hard fallback when neither is set: 25.
Flags
--folderbecomes-m|--mailbox <NAME>(optional, same default asenvelopes list).<id-or-flags>split into-f,--flag <FLAG>(repeatable) and a positional<message-ids>.
Messages
- Removed
delete: too protocol-specific. Use the matching protocol-specific subcommand, or combineflags addwith the per-protocol expunge / move-to-trash step. copyandmove:--folder <source>renamed--from <mailbox-id>, and positional<target>renamed--to <mailbox-id>.saverenamedadd(kept as an alias, sosavestill works).save --folder(optional) becomesadd --mailbox(mandatory).- The old
save --file <PATH>/ positional split is gone:addnow takes a singleMessageArg(a positional file path, an inline raw value, or piped stdin). - Added
add --flagto attach flags at insertion time, andadd --send(aliassave --send) to push the message through the account's send path after the append. writerenamedcompose(kept as an alias).compose,reply,forwardare no longer interactive. They build the message from CLI flags through the built-in flag composer. Interactive composition is delegated to standalone tools chained intomessages send/messages addvia a tempfile or shell process substitution. No*-withsubcommands or[message.composer.*]table remain on the himalaya side.readno longer renders human-readable text. The v2readprints message-level info. For custom rendering, piperead --rawinto 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 sendandmessages addread the raw message from a positional path, an inline raw value, or stdin (the unifiedMessageArg).exportandeditare removed.
See pimalaya/mml for a ready-to-use composer / interpreter.
Attachments
download --folderbecomes-m|--mailbox <NAME>(optional, same default asenvelopes list).--downloads-dirrenamed--dir.- Added an optional
<attachment-id>positional todownload(omit to download every attachment, preserving the v1 behaviour). - Added a
listsubcommand.
Template
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).
Configuration changes
The full configuration schema is documented in config.sample.toml. The notes below focus on what changed since v1.
Global and per-account options
- Removed
display-name,signature,signature-delim: composition left the CLI. - Only
downloads-dirremains for theattachments downloadcommand. - The
message,templateandpgptop-level entries are removed. Composition and rendering happen outside himalaya now (see the README for the recommended shell-pipeline shapes).
Table customization
- The per-type
{account,folder,envelope}.list.table.{preset,arrangement}keys collapse into a singletable.{preset,arrangement}(global / per-account).table.arrangement(dynamic,dynamic-full-width,disabled) is new. - Rename
folder.list.table.*→mailbox.list.table.*(mirrors thefolders→mailboxescommand rename). - Rename
envelope.list.table.sender-color→envelope.list.table.from-color(the column is nowFROM).
Mailbox aliases
The v1 [folder.alias] block becomes [mailbox.alias]. Two behaviour changes on top of the rename:
- Alias names are case-insensitive both on lookup and on storage, so
INBOX = "...",Inbox = "..."andinbox = "..."are equivalent entries. - The entry named
inbox(case-insensitive) is the implicit default mailbox: shared commands fall back to its id when-m/--mailboxis omitted. No separatedefault-mailboxkey.
Account-level [accounts.<name>.mailbox.alias] entries override same-named global [mailbox.alias] entries.
Secrets
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.
IMAP
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 = "user@example.com"
imap.sasl.plain.passwd.raw = "***"
# or
imap.sasl.plain.passwd.command = ["pass", "show", "example"]
# SASL LOGIN
imap.sasl.login.username = "user@example.com"
imap.sasl.login.password.raw = "***"
# SASL OAUTHBEARER (RFC 7628)
imap.sasl.oauthbearer.username = "user@example.com"
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 = "user@example.com"
imap.sasl.xoauth2.token.raw = "***"
# SASL SCRAM-SHA-256 (RFC 7677)
imap.sasl.scram-sha-256.username = "user@example.com"
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.
SMTP
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
maildir.root = "~/Mail/example"
m2dir (new)
An m2dir store: content-addressed messages with sidecar flag metadata.
m2dir.root = "~/Mail/example"
JMAP (new)
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 = "user@example.com"
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"
Gmail REST API (new)
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 = "***"
Microsoft Graph (new)
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"]
Notmuch / Sendmail
Both backends are removed. Notmuch may come back in a future release.
Suggested migration steps
- Copy config.sample.toml to a side-by-side path (for example ~/.config/himalaya/config.v2.toml) and edit it against your previous configuration.
- Run
himalaya -c ~/.config/himalaya/config.v2.toml account checkto validate the connection for each declared backend. - Once the new file passes the check, replace the v1 config.toml with it.
- If you relied on keyring / OAuth, wire a password-manager CLI (
pass,secret-tool, …) and/or pimalaya/ortie ascommand = …secrets. - If you relied on the interactive
write/reply/forward, install pimalaya/mml and chain it intohimalaya messages send/messages addvia a tempfile or>(...)process substitution (see the README for ready-madebash/zshsnippets).