Composers and readers did not work as expected. It is just not possible for himalaya to spawn a command that spawns $EDITOR, piping and redirection cannot satisfy all the needs. Either the $EDITOR does not spawn (hangs over), either himalaya does not collect any output from edition. The simplest way is to use an intermediate temp file, or use process substitution. For eg., using mml: mml compose >(himalaya message send) You can also write into a file then feed himalaya with it.
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 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 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. Interactive composition is delegated to standalone tools chained intomessages send/messages addvia a tempfile or shell process substitution; no*-withsubcommands or[message.composer.*]table 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" /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.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 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).