12 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 pimalaya/mimosa (or any password manager exposed as a shell command).
- 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 two 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 mailboxes create,… expunge,… purge,… 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; 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>; positional<target>renamed--to <mailbox-id>.saverenamedadd(kept as an alias, sosavestill works).save --folder(optional) becomesadd --mailbox(mandatory).save <path-or-raw>split into the explicit--file <PATH>and positional<raw>.- Added
add --flagto attach flags at insertion time. write,reply,forwardare no longer interactive. They build the message from CLI flags through the built-in flag composer. The interactive variants live undercompose-with,reply-with,forward-with, which delegate to a user-defined composer declared in[message.composer.*].readno longer renders human-readable text; that responsibility moved to the reader. The v2readprints message-level info; the reader pipeline lives underread-with, backed by[message.reader.*].mailto <URI>now pipes the parsed RFC 6068 URI through a user-defined composer (same routing options ascompose-with) instead of opening the v1 interactive editor.messages sendgains--file <PATH>as a parity withmessages addfor reading the raw message from a file instead of stdin or the positional argument.exportandeditare removed.
See pimalaya/mml for a ready-to-use composer and reader.
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; plug it into himalaya as a composer/reader.
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. - Per-type table customization (
{account,folder,envelope}.list.table.*) collapsed into a singletable-presetplus atable-arrangement(dynamic,dynamic-full-width,disabled). Color customization is gone. - Composition / reading hooks live under
[message.composer.<name>]and[message.reader.<name>]; each entry sets acommandand optionallydefault = true. - The
message,templateandpgptop-level entries are removed.
Mailbox aliases
The v1 [folder.aliases] block becomes [mailbox.aliases]. 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 pimalaya/mimosa (or pass, secret-tool, gopass…) 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`.
imap.server = "example.com"
# or imap.server = "imaps://example.com:993"
# or imap.server = "imap://example.com:143" (use imap.starttls = true to upgrade)
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 = ["mimosa", "password", "read", "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"
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 = ["mimosa", "password", "read", "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"
# Required only for `messages send` over JMAP.
jmap.identity-id = "I0123abc"
jmap.drafts-mailbox-id = "M0123abc"
Notmuch / Sendmail
Both backends are removed. Notmuch may come back in a future release.
Suggested migration steps
- Copy
config.sample.tomlto 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.tomlwith it. - If you relied on keyring / OAuth, install pimalaya/mimosa and/or pimalaya/ortie and wire them as
command = …secrets. - If you relied on
write/reply/forward, install pimalaya/mml and declare it under[message.composer.*]/[message.reader.*].