docs: fix shared-API command names + plural hidden aliases

* docs: fix shared-API command names, document search, draft and read semantics

The shared commands are singular since 3a1a981b but the README examples
still used the plural forms. Also document the query DSL limits (SORT
capability required, see #698), the save-as-draft recipe, and the
missing Reading messages section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(cli): accept plural shared commands as hidden aliases

Naming rule from #701: singular public API with hidden plural aliases,
plural struct fields (the mailbox.alias config key is now backed by an
`aliases` field, `aliases` kept as config alternative).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Refs: #701 #703
This commit is contained in:
Andrew Furman
2026-06-11 06:27:41 -04:00
committed by GitHub
parent 48cb0d3e7b
commit 4f99cb87d8
4 changed files with 51 additions and 32 deletions
+40 -21
View File
@@ -192,17 +192,19 @@ Accounts can be (re)configured later with `himalaya account configure <name>`. T
Backend-agnostic commands operate on the account's first configured backend, or the one selected with `-b/--backend`:
```
himalaya mailboxes list
himalaya envelopes list -m INBOX --page 2
himalaya envelopes search from alice and after 2026-01-01 order by date desc
himalaya flags add -m INBOX --flag seen 1:3,5
himalaya messages copy --from INBOX --to Archives 42
himalaya attachments download -m INBOX 42
himalaya mailbox list
himalaya envelope list -m INBOX --page 2
himalaya envelope search from alice and after 2026-01-01 order by date desc
himalaya flag add -m INBOX --flag seen 1:3,5
himalaya message copy --from INBOX --to Archives 42
himalaya attachment download -m INBOX 42
```
When the `inbox` alias is configured under `[mailbox.alias]`, `-m/--mailbox` becomes optional: shared commands fall back to that id. With `[mailbox.alias] inbox = "INBOX"`, the calls above shorten to `envelopes list --page 2`, `flags add --flag seen 1:3,5`, etc.
When the `inbox` alias is configured under `[mailbox.alias]`, `-m/--mailbox` becomes optional: shared commands fall back to that id. With `[mailbox.alias] inbox = "INBOX"`, the calls above shorten to `envelope list --page 2`, `flag add --flag seen 1:3,5`, etc.
`envelopes list` is plain pagination, ordered by date descending. To filter or sort, use `envelopes search` with a trailing query covering `date`, `after`, `from`, `to`, `subject`, `body`, `flag` conditions (combined with `and`, `or`, `not`, grouped with parens) and an `order by date|from|to|subject [asc|desc]` sort chain. Date clauses target the `Date:` header (sent-at) on every backend.
`envelope list` is plain pagination, ordered by date descending. To filter or sort, use `envelope search` with a trailing query covering `date`, `after`, `from`, `to`, `subject`, `body`, `flag` conditions (combined with `and`, `or`, `not`, grouped with parens) and an `order by date|from|to|subject [asc|desc]` sort chain. Date clauses target the `Date:` header (sent-at) on every backend. The full grammar lives in `himalaya envelope search --help`, which is the source of truth for the query DSL.
The query DSL is himalaya's own and compiles to each backend's native search: provider-specific operators (Gmail's `in:`/`label:` syntax, `X-GM-RAW`, …) are not supported. On IMAP the search currently runs server-side as `UID SORT`, so it requires the `SORT` capability — servers without it (notably Gmail) reject the command for now (see [#698](https://github.com/pimalaya/himalaya/issues/698)).
The shared surface is a strict least-common-denominator subset across IMAP, JMAP and Maildir. Operations that do not generalize (mailbox roles, attribute flags, JMAP-specific queries…) live under the protocol-specific subcommands.
@@ -211,9 +213,9 @@ The shared surface is a strict least-common-denominator subset across IMAP, JMAP
Each backend exposes its full native API under its own subgroup:
```
himalaya imap mailboxes select INBOX
himalaya imap mailboxes status INBOX
himalaya imap mailboxes subscribe INBOX
himalaya imap mailbox select INBOX
himalaya imap mailbox status INBOX
himalaya imap mailbox subscribe INBOX
himalaya jmap mailboxes query --role drafts
himalaya jmap identity get
@@ -229,25 +231,42 @@ The `-b/--backend` flag is only consumed by the shared commands; protocol subcom
### Composing messages
The built-in `messages compose` / `reply` / `forward` commands cover simple cases via CLI flags:
The built-in `message compose` / `reply` / `forward` commands cover simple cases via CLI flags:
```
himalaya messages compose --from me@example.org --to you@example.org \
himalaya message compose --from me@example.org --to you@example.org \
--subject "Hello" --body "Hi!" --send
```
For richer composition (multipart MIME, MML directives, signing/encryption, editor-driven workflows), chain a standalone composer such as [mml](https://github.com/pimalaya/mml) into `messages send` / `messages add` through a tempfile or bash/zsh process substitution:
For richer composition (multipart MIME, MML directives, signing/encryption, editor-driven workflows), chain a standalone composer such as [mml](https://github.com/pimalaya/mml) into `message send` / `message add` through a tempfile or bash/zsh process substitution:
```sh
# Explicit tempfile, works in plain POSIX sh
mml compose /tmp/draft.eml && himalaya messages send /tmp/draft.eml
mml compose /tmp/draft.eml && himalaya message send /tmp/draft.eml
# Bash / zsh process substitution, single command, no tempfile
mml compose >(himalaya messages send)
himalaya messages read 42 | mml reply >(himalaya messages send)
mml compose >(himalaya message send)
himalaya message read 42 | mml reply >(himalaya message send)
```
The path-arg or process-substitution forms keep the composer's stdout connected to the terminal, so any `$EDITOR` it spawns sees a real tty. The bare-pipe form (`mml compose | himalaya messages send`) hangs because the editor inherits a pipe on its stdout.
The path-arg or process-substitution forms keep the composer's stdout connected to the terminal, so any `$EDITOR` it spawns sees a real tty. The bare-pipe form (`mml compose | himalaya message send`) hangs because the editor inherits a pipe on its stdout.
A prepared RFC 5322 file can also be staged as a draft instead of sent right away — handy for "compose, review in another client, then send" workflows:
```sh
himalaya message add -m drafts --flag draft < message.eml # save as draft
himalaya message send --save sent < message.eml # send + keep a copy
```
Both `-m`/`--save` values are resolved through the account's `[mailbox.alias]` map.
### Reading messages
`himalaya message read <ID>` renders headers and text bodies; `--raw` dumps the original RFC 5322 bytes; `--json` emits the parsed message. A few behaviours worth knowing, especially when scripting:
- Reading is side-effect-free: messages are fetched with `BODY.PEEK`, so `message read` never sets `\Seen`. Mark explicitly with `flag add --flag seen <ID>`.
- Ids are per-mailbox (IMAP UID, JMAP email id or Maildir filename id): the same message gets a new id when copied or moved. The `message-id` field exposed in `--json` envelope output is the stable cross-mailbox key.
- Every command accepts `--json`; envelope listings serialize as `{"envelopes": [{"id", "message-id", "flags": [{"raw", "iana"}], "subject", "from": [{"name", "email"}], "to", "date", "size", "has-attachment"}]}`.
### Re-using sessions
@@ -314,7 +333,7 @@ Himalaya CLI is one of several front-ends to the Pimalaya libraries:
Use `--log-level <level>` (alias `--log`) where `<level>` is one of `off`, `error`, `warn`, `info`, `debug`, `trace`:
```
himalaya --log trace mailboxes list
himalaya --log trace mailbox list
```
The `RUST_LOG` environment variable is consulted when `--log` is not passed, and supports per-target filters (see the [`env_logger` documentation](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)). `RUST_BACKTRACE=1` enables full error backtraces.
@@ -322,13 +341,13 @@ Himalaya CLI is one of several front-ends to the Pimalaya libraries:
Logs are written to `stderr`, so they can be redirected easily to a file:
```
himalaya --log trace mailboxes list 2>/tmp/himalaya.log
himalaya --log trace mailbox list 2>/tmp/himalaya.log
```
You can also send logs straight to a file via `--log-file <path>`:
```
himalaya --log trace --log-file /tmp/himalaya.log mailboxes list
himalaya --log trace --log-file /tmp/himalaya.log mailbox list
```
</details>
+4 -4
View File
@@ -360,7 +360,7 @@ impl From<Config> for Account {
mailboxes_list_table: config.mailbox.list.table,
attachments_list_table: config.attachment.list.table,
mailbox_alias: lowercase_alias_keys(config.mailbox.alias),
mailbox_alias: lowercase_alias_keys(config.mailbox.aliases),
}
}
}
@@ -380,7 +380,7 @@ impl From<AccountConfig> for Account {
mailboxes_list_table: config.mailbox.list.table,
attachments_list_table: config.attachment.list.table,
mailbox_alias: lowercase_alias_keys(config.mailbox.alias),
mailbox_alias: lowercase_alias_keys(config.mailbox.aliases),
}
}
}
@@ -391,13 +391,13 @@ mod tests {
use crate::config::MailboxConfig;
fn account_with_aliases(pairs: &[(&str, &str)]) -> Account {
let alias = pairs
let aliases = pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
let config = Config {
mailbox: MailboxConfig {
alias,
aliases,
..MailboxConfig::default()
},
..Config::default()
+5 -5
View File
@@ -71,15 +71,15 @@ pub struct Cli {
pub enum Command {
// --- Shared API
//
#[command(subcommand, visible_alias = "mbox")]
#[command(subcommand, visible_alias = "mbox", alias = "mailboxes")]
Mailbox(MailboxCommand),
#[command(subcommand)]
#[command(subcommand, alias = "envelopes")]
Envelope(EnvelopeCommand),
#[command(subcommand)]
#[command(subcommand, alias = "flags")]
Flag(FlagCommand),
#[command(subcommand, visible_alias = "msg")]
#[command(subcommand, visible_alias = "msg", alias = "messages")]
Message(MessageCommand),
#[command(subcommand)]
#[command(subcommand, alias = "attachments")]
Attachment(AttachmentCommand),
// --- Protocol-specific APIs
+2 -2
View File
@@ -133,8 +133,8 @@ pub struct EnvelopeConfig {
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct MailboxConfig {
#[serde(default, alias = "aliases")]
pub alias: HashMap<String, String>,
#[serde(default, rename = "alias", alias = "aliases")]
pub aliases: HashMap<String, String>,
#[serde(default)]
pub list: MailboxListConfig,