Files
himalaya/MIGRATION.md
T
Clément DOUIN 1f6cf06166 chore: clean
2026-05-26 19:40:35 +02:00

225 lines
12 KiB
Markdown

# 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](https://github.com/pimalaya/ortie).
- **Keyring moved out** to [pimalaya/mimosa](https://github.com/pimalaya/mimosa) (or any password manager exposed as a shell command).
- **Composition and reading moved out** to [pimalaya/mml](https://github.com/pimalaya/mml).
- **Session reuse moved out** to [pimalaya/sirup](https://github.com/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](https://sans-io.readthedocs.io/) 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](https://github.com/pimalaya/ortie), [pimalaya/cardamum](https://github.com/pimalaya/cardamum) and [pimalaya/calendula](https://github.com/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 `folders` to `mailboxes`.
- 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 `--counts` to `list` to populate per-mailbox message counts.
#### Envelopes
- `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.
- 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 the `search` command instead of `list`.
- Default page size moves to `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.
#### Flags
- `--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>`.
#### Messages
- Removed `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>`; 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 <path-or-raw>` split into the explicit `--file <PATH>` and positional `<raw>`.
- Added `add --flag` to attach flags at insertion time.
- `write`, `reply`, `forward` are no longer interactive. They build the message from CLI flags through the built-in flag composer. The interactive variants live under `compose-with`, `reply-with`, `forward-with`, which delegate to a user-defined composer declared in `[message.composer.*]`.
- `read` no longer renders human-readable text; that responsibility moved to the reader. The v2 `read` prints message-level info; the reader pipeline lives under `read-with`, backed by `[message.reader.*]`.
- `mailto <URI>` now pipes the parsed RFC 6068 URI through a user-defined composer (same routing options as `compose-with`) instead of opening the v1 interactive editor.
- `messages send` gains `--file <PATH>` as a parity with `messages add` for reading the raw message from a file instead of stdin or the positional argument.
- `export` and `edit` are removed.
See [pimalaya/mml](https://github.com/pimalaya/mml) for a ready-to-use composer and reader.
#### Attachments
- `download --folder` becomes `-m|--mailbox <NAME>` (optional, same default as `envelopes list`).
- `--downloads-dir` renamed `--dir`.
- Added an optional `<attachment-id>` positional to `download` (omit to download every attachment, preserving the v1 behaviour).
- Added a `list` subcommand.
#### Template
Fully removed. The template pipeline (compose / reply / forward drafts, MML compile, MIME interpret) lives in [pimalaya/mml](https://github.com/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](./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-dir` remains for the `attachments download` command.
- Composition / reading hooks live under `[message.composer.<name>]` and `[message.reader.<name>]`, each optionally flagged `default = true`. A composer entry sets one shell command per operation (`compose`, `reply`, `forward`); a reader entry sets a single `command`.
- The `message`, `template` and `pgp` top-level entries are removed.
#### Table customization
- The per-type `{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.
- Rename `folder.list.table.*``mailbox.list.table.*` (mirrors the `folders``mailboxes` command rename).
- Rename `envelope.list.table.sender-color``envelope.list.table.from-color` (the column is now `FROM`).
#### 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 = "..."` and `inbox = "..."` are equivalent entries.
- The entry named `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.
#### 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](https://github.com/pimalaya/mimosa) (or `pass`, `secret-tool`, `gopass`…) as the command. OAuth tokens are produced by an external broker such as [pimalaya/ortie](https://github.com/pimalaya/ortie) and consumed the same way.
#### IMAP
The whole `backend.type = "imap"` block collapses into:
```toml
# 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](https://github.com/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
```toml
maildir.root = "~/Mail/example"
```
#### JMAP (new)
```toml
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
1. Copy [`config.sample.toml`](./config.sample.toml) to a side-by-side path (for example `~/.config/himalaya/config.v2.toml`) and edit it against your previous configuration.
2. Run `himalaya -c ~/.config/himalaya/config.v2.toml account check` to validate the connection for each declared backend.
3. Once the new file passes the check, replace the v1 `config.toml` with it.
4. If you relied on keyring / OAuth, install [pimalaya/mimosa](https://github.com/pimalaya/mimosa) and/or [pimalaya/ortie](https://github.com/pimalaya/ortie) and wire them as `command = …` secrets.
5. If you relied on `write` / `reply` / `forward`, install [pimalaya/mml](https://github.com/pimalaya/mml) and declare it under `[message.composer.*]` / `[message.reader.*]`.