diff --git a/CHANGELOG.md b/CHANGELOG.md index 068141e9..58777fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Unified raw-message input across `messages add`, `messages send`, `imap message save`, `maildir message save`, `jmap email import` and `smtp message send` behind a single `MessageArg` (ported from `mml::cli::args::MessageArg`). Every command now accepts the same three forms: a positional file path, a positional inline raw message (with `\r` / `\n` literals normalized to `\r\n`), or stdin when piped. The legacy `--file ` flag on `messages add` is gone (positional path replaces it). +- Split the merged `Account` out of every client wrapper (`EmailClient`, `ImapClient`, `JmapClient`, `MaildirClient`, `M2dirClient`, `SmtpClient`). Subcommands now receive `account: &mut Account` and `client: &mut Client` as sibling arguments rather than reaching through `client.account`, which keeps account access borrow-disjoint from `&mut client` calls. + ### Fixed - Fixed compilation error when `wizard` feature was disabled ([#634]). +- Fixed `--save ` on `messages compose` / `reply` / `forward` to resolve the mailbox name through the account's alias map (`account.resolve_mailbox`) before calling the backend, so `--save Sent` honours e.g. `mailbox.alias.sent = "[Gmail]/Sent Mail"`. + +### Removed + +- Removed the `[message.composer.*]` and `[message.reader.*]` config tables together with the `messages compose-with`, `reply-with`, `forward-with`, `mailto` and `read-with` subcommands. The "stdout = MIME draft" contract was structurally incompatible with composers that spawn an interactive editor: the editor inherited the parent's piped stdout, breaking its UI. Richer composition is now wired through standalone tools chained into `messages send` / `messages add` via a tempfile or shell process substitution; see the README and [mml](https://github.com/pimalaya/mml). + ## [1.2.0] - 2026-02-19 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93032ead..990a4338 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ Himalaya CLI is the command-line front-end of the [Pimalaya](https://github.com/ - [pimalaya/stream](https://github.com/pimalaya/stream): TCP / TLS / SASL plumbing shared by all std clients. - [pimalaya/cli](https://github.com/pimalaya/cli): cross-binary CLI helpers (printer, prompt, wizard primitives, clap args, build-time env, spinner). - [pimalaya/config](https://github.com/pimalaya/config): TOML configuration loader and shell-expanded secrets. -- [pimalaya/mml](https://github.com/pimalaya/mml): MIME Meta Language composer / reader, plugged in via `[message.composer.*]` / `[message.reader.*]`. +- [pimalaya/mml](https://github.com/pimalaya/mml): MIME Meta Language composer / interpreter, chained into `messages send` / `messages add` via a tempfile or shell process substitution. - [pimalaya/sirup](https://github.com/pimalaya/sirup): session re-use over a Unix socket (pair with `imap.server` / `smtp.server` to amortize TLS handshakes). - [pimalaya/ortie](https://github.com/pimalaya/ortie): standalone OAuth 2.0 token broker (replaces v1's bundled `oauth-lib`). - [pimalaya/mimosa](https://github.com/pimalaya/mimosa): standalone secret manager (replaces v1's bundled `keyring-lib`). diff --git a/MIGRATION.md b/MIGRATION.md index 256cb70b..91852f3b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -69,13 +69,13 @@ New in v2: `-b`, `--backend` (force a specific backend for shared commands) and - `save --folder` (optional) becomes `add --mailbox` (mandatory). - `save ` split into the explicit `--file ` and positional ``. - 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 ` 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 ` as a parity with `messages add` for reading the raw message from a file instead of stdin or the positional argument. +- `write`, `reply`, `forward` are no longer interactive. They build the message from CLI flags through the built-in flag composer. Interactive composition is delegated to standalone tools chained into `messages send` / `messages add` via a tempfile or shell process substitution; no `*-with` subcommands or `[message.composer.*]` table on the himalaya side. +- `read` no longer renders human-readable text; the v2 `read` prints message-level info. For custom rendering, pipe `read --raw` into 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 send` and `messages add` read the raw message from a positional path, an inline raw value, or stdin (the unified `MessageArg`). - `export` and `edit` are removed. -See [pimalaya/mml](https://github.com/pimalaya/mml) for a ready-to-use composer and reader. +See [pimalaya/mml](https://github.com/pimalaya/mml) for a ready-to-use composer / interpreter. #### Attachments @@ -86,7 +86,7 @@ See [pimalaya/mml](https://github.com/pimalaya/mml) for a ready-to-use composer #### 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. +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; chain its CLI into `messages send` / `messages add` (see the README). ### Configuration changes @@ -96,8 +96,7 @@ The full configuration schema is documented in [config.sample.toml](./config.sam - 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.]` and `[message.reader.]`, 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. +- The `message`, `template` and `pgp` top-level entries are removed. Composition and rendering happen outside himalaya now (see the README for the recommended shell-pipeline shapes). #### Table customization @@ -221,4 +220,4 @@ Both backends are removed. Notmuch may come back in a future release. 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.*]`. +5. If you relied on the interactive `write` / `reply` / `forward`, install [pimalaya/mml](https://github.com/pimalaya/mml) and chain it into `himalaya messages send` / `messages add` via a tempfile or `>(...)` process substitution (see the README for ready-made `bash`/`zsh` snippets). diff --git a/README.md b/README.md index eb0b1056..0c6d662b 100644 --- a/README.md +++ b/README.md @@ -235,38 +235,18 @@ himalaya messages 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…), wire a user-defined composer in `[message.composer.*]` and invoke it with the `-with` variants. Each entry declares one shell command per operation (`compose` for blank drafts and `mailto`, `reply` for replies, `forward` for forwards); the source message is piped on stdin for `reply` / `forward`. For example, with [mml](https://github.com/pimalaya/mml): +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: -```toml -[message.composer.mml] -compose = "mml compose" -reply = "mml reply" -forward = "mml forward" -default = true +```sh +# Explicit tempfile, works in plain POSIX sh +mml compose /tmp/draft.eml && himalaya messages 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) ``` -``` -himalaya messages compose-with -himalaya messages reply-with -m INBOX 42 --send -himalaya messages forward-with -m INBOX 42 --send -himalaya messages mailto 'mailto:bob@example.org?subject=Hi&body=Hello' -``` - -`messages mailto ` parses an RFC 6068 `mailto:` URI (recipient list in the path, `to` / `cc` / `bcc` / `subject` / `body` query parameters), builds a draft RFC 5322 skeleton with those headers pre-filled, then pipes it on stdin to the named (or default) composer's `compose` command. The composer's output is routed through `--save` / `--send` like the other `-with` variants. Useful as a desktop `mailto:` handler. - -### Reading messages - -The built-in `messages read` command renders a message with himalaya's default formatter. For custom rendering, declare a reader in `[message.reader.*]` and call `read-with`: - -```toml -[message.reader.mml] -command = "mml read" -default = true -``` - -``` -himalaya messages read-with -m INBOX 42 -``` +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. ### Re-using sessions diff --git a/config.sample.toml b/config.sample.toml index bc814053..aa58ddfe 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -120,32 +120,6 @@ #mailbox.alias.drafts = "[Gmail]/Drafts" #mailbox.alias.trash = "[Gmail]/Trash" -# -------------------------------------------------------------------------------- -# User-defined composers and readers -# -------------------------------------------------------------------------------- - -# Composers produce a MIME draft on stdout. They are invoked by the -# `messages compose-with` / `reply-with` / `forward-with` subcommands. -# Each entry declares one shell command per operation: `compose` (for blank -# drafts and `mailto`), `reply`, and `forward`. Stdin carries the source MIME -# bytes for `reply` / `forward` and is empty for `compose`; stderr is inherited -# so the composer can prompt the user. -# -# Pick one with `compose-with `, or let himalaya use the entry flagged -# `default = true` when no name is passed. -# -# Example using https://github.com/pimalaya/mml: -#message.composer.mml.compose = ["mml", "compose"] -#message.composer.mml.reply = ["mml", "reply"] -#message.composer.mml.forward = ["mml", "forward"] -#message.composer.mml.default = true - -# Readers consume a MIME message on stdin and emit human-readable bytes on -# stdout. They are invoked by `messages read-with`. -# -#message.reader.mml.command = ["mml", "read"] -#message.reader.mml.default = true - # -------------------------------------------------------------------------------- # Account config # -------------------------------------------------------------------------------- diff --git a/src/account/context.rs b/src/account/context.rs index 04c31fa8..b57f6644 100644 --- a/src/account/context.rs +++ b/src/account/context.rs @@ -30,14 +30,13 @@ use std::{collections::HashMap, env::temp_dir, path::PathBuf}; -use anyhow::{Result, anyhow}; use comfy_table::{Color as TableColor, ContentArrangement, presets}; use crossterm::style::Color; use dirs::download_dir; use crate::config::{ - AccountConfig, AttachmentListTableConfig, ComposerConfig, Config, EnvelopeListTableConfig, - MailboxListTableConfig, ReaderConfig, TableArrangementConfig, + AccountConfig, AttachmentListTableConfig, Config, EnvelopeListTableConfig, + MailboxListTableConfig, TableArrangementConfig, }; const DEFAULT_DATETIME_FMT: &str = "%F %R%:z"; @@ -70,29 +69,16 @@ pub struct Account { /// `mailbox.alias` at the global and account levels; account /// entries overwrite same-named global entries. pub mailbox_alias: HashMap, - - /// User-defined composers. Only sourced from the global - /// [`Config`]; account-level configs do not override these. - pub composer: HashMap, - /// User-defined readers. See [`Account::composer`]. - pub reader: HashMap, } impl Account { /// Folds `other`'s set fields on top of `self`. Each `Option` /// field is taken from `other` when `Some`, otherwise from - /// `self`. The composer/reader maps are extended (entries from - /// `other` overwrite same-named entries from `self`). + /// `self`. pub fn merge(self, other: Self) -> Self { let mut mailbox_alias = self.mailbox_alias; mailbox_alias.extend(other.mailbox_alias); - let mut composer = self.composer; - composer.extend(other.composer); - - let mut reader = self.reader; - reader.extend(other.reader); - Self { downloads_dir: other.downloads_dir.or(self.downloads_dir), table_preset: other.table_preset.or(self.table_preset), @@ -118,9 +104,6 @@ impl Account { ), mailbox_alias, - - composer, - reader, } } @@ -198,40 +181,6 @@ impl Account { .map(String::as_str) } - /// Gets a composer configuration. When `name` is given, looks up - /// the corresponding entry and bails if missing. - /// When `name` is `None`, returns the entry with `default = true`, - /// or bails with a hint if no default is set. - pub fn get_composer_mut(&mut self, name: Option<&str>) -> Result<&mut ComposerConfig> { - match name { - Some(name) => self - .composer - .get_mut(name) - .ok_or(anyhow!("no composer named `{name}` in [message.composer]")), - None => self - .composer - .values_mut() - .find(|c| c.default) - .ok_or(anyhow!( - "no composer specified and no default in [message.composer.*]; \ - pass a or set `default = true` on one entry" - )), - } - } - - pub fn get_reader_mut(&mut self, name: Option<&str>) -> Result<&mut ReaderConfig> { - match name { - Some(name) => self - .reader - .get_mut(name) - .ok_or(anyhow!("no reader named `{name}` in [message.reader]")), - None => self.reader.values_mut().find(|c| c.default).ok_or(anyhow!( - "no reader specified and no default in [message.reader.*]; \ - pass a or set `default = true` on one entry" - )), - } - } - // ── envelopes list — flag glyphs ───────────────────────────────────── pub fn envelopes_list_table_unseen_char(&self) -> char { @@ -429,9 +378,6 @@ impl From for Account { attachments_list_table: config.attachment.list.table, mailbox_alias: lowercase_alias_keys(config.mailbox.alias), - - composer: config.message.composer, - reader: config.message.reader, } } } @@ -452,9 +398,6 @@ impl From for Account { attachments_list_table: config.attachment.list.table, mailbox_alias: lowercase_alias_keys(config.mailbox.alias), - - composer: HashMap::new(), - reader: HashMap::new(), } } } diff --git a/src/cli.rs b/src/cli.rs index 265d391b..f5a2216d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -170,56 +170,56 @@ impl Command { // Self::Mailbox(cmd) => { let (config, account_config) = configs()?; - let client = EmailClient::new(config, account_config, backend)?; - cmd.execute(printer, client) + let (mut account, mut client) = EmailClient::new(config, account_config, backend)?; + cmd.execute(printer, &mut account, &mut client) } Self::Envelope(cmd) => { let (config, account_config) = configs()?; - let client = EmailClient::new(config, account_config, backend)?; - cmd.execute(printer, client) + let (mut account, mut client) = EmailClient::new(config, account_config, backend)?; + cmd.execute(printer, &mut account, &mut client) } Self::Flag(cmd) => { let (config, account_config) = configs()?; - let client = EmailClient::new(config, account_config, backend)?; - cmd.execute(printer, client) + let (mut account, mut client) = EmailClient::new(config, account_config, backend)?; + cmd.execute(printer, &mut account, &mut client) } Self::Message(cmd) => { let (config, account_config) = configs()?; - let client = EmailClient::new(config, account_config, backend)?; - cmd.execute(printer, client) + let (mut account, mut client) = EmailClient::new(config, account_config, backend)?; + cmd.execute(printer, &mut account, &mut client) } Self::Attachment(cmd) => { let (config, account_config) = configs()?; - let client = EmailClient::new(config, account_config, backend)?; - cmd.execute(printer, client) + let (mut account, mut client) = EmailClient::new(config, account_config, backend)?; + cmd.execute(printer, &mut account, &mut client) } // --- Protocol-specific APIs // #[cfg(feature = "imap")] Self::Imap(cmd) => { - let client = build_imap_client(config_paths, account_name)?; - cmd.execute(printer, client) + let (mut account, mut client) = build_imap_client(config_paths, account_name)?; + cmd.execute(printer, &mut account, &mut client) } #[cfg(feature = "jmap")] Self::Jmap(cmd) => { - let client = build_jmap_client(config_paths, account_name)?; - cmd.execute(printer, client) + let (mut account, mut client) = build_jmap_client(config_paths, account_name)?; + cmd.execute(printer, &mut account, &mut client) } #[cfg(feature = "maildir")] Self::Maildir(cmd) => { - let client = build_maildir_client(config_paths, account_name)?; - cmd.execute(printer, client) + let (mut account, mut client) = build_maildir_client(config_paths, account_name)?; + cmd.execute(printer, &mut account, &mut client) } #[cfg(feature = "m2dir")] Self::M2dir(cmd) => { - let client = build_m2dir_client(config_paths, account_name)?; - cmd.execute(printer, client) + let (mut account, mut client) = build_m2dir_client(config_paths, account_name)?; + cmd.execute(printer, &mut account, &mut client) } #[cfg(feature = "smtp")] Self::Smtp(cmd) => { - let client = build_smtp_client(config_paths, account_name)?; - cmd.execute(printer, client) + let (_account, mut client) = build_smtp_client(config_paths, account_name)?; + cmd.execute(printer, &mut client) } // --- Meta diff --git a/src/config.rs b/src/config.rs index a7c5c0d5..8eaa86a3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,13 +15,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::{collections::HashMap, fs, path::Path, path::PathBuf, process::Command}; +use std::{collections::HashMap, fs, path::Path, path::PathBuf}; use anyhow::{Context, Result}; use comfy_table::ContentArrangement; use crossterm::style::Color; use pimalaya_config::{ - command, secret::Secret, toml::{TomlConfig, shell_expanded_string}, }; @@ -51,8 +50,6 @@ pub struct Config { #[serde(default)] pub mailbox: MailboxConfig, #[serde(default)] - pub message: MessageConfig, - #[serde(default)] pub attachment: AttachmentConfig, /// `account list` rendering options (global only — there is no /// per-account override for the listing of accounts). @@ -283,82 +280,6 @@ pub struct EnvelopeListTableConfig { pub size_color: Option, } -/// Message-level configuration: user-defined composers and readers. -/// -/// Composers produce a MIME draft on stdout (called by `compose-with`, -/// `reply-with`, `forward-with`). Readers consume a MIME message from -/// stdin and emit human-readable bytes on stdout (called by -/// `read-with`). Both are looked up by name; the entry flagged -/// `default = true` is used when no name is passed. -#[derive(Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct MessageConfig { - #[serde(default)] - pub composer: HashMap, - #[serde(default)] - pub reader: HashMap, -} - -/// Single composer entry under `[message.composer.]`. -/// -/// For all shell command strings defined below: -/// - The command is invoked via `sh -c`. -/// - stdin behavior varies by command as documented below. -/// - stdout is captured as the MIME draft. -/// - stderr is inherited so the composer can prompt the user. -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct ComposerConfig { - /// Command used to write a brand new message. - /// - /// This is invoked by the `compose-with` and `mailto` commands. - /// - /// - When invoked by `compose-with`, stdin is empty. - /// - When invoked by `mailto`, stdin is piped with a pre-filled RFC 5322 - /// draft skeleton built from the parsed RFC 6068 `mailto:` URI parameters - /// (such as to, cc, bcc, subject, and body). - #[serde(with = "command")] - pub compose: Command, - - /// Command used to reply to an existing message. - /// - /// This is invoked by the `reply-with` command. The original message's - /// MIME bytes are passed via stdin. - #[serde(with = "command")] - pub reply: Command, - - /// Command used to forward an existing message. - /// - /// This is invoked by the `forward-with` command. The original message's - /// MIME bytes are passed via stdin. - #[serde(with = "command")] - pub forward: Command, - - /// Marks this entry as the fallback when `compose-with` / - /// `reply-with` / `forward-with` are invoked without a name. - /// Exactly one composer should set this; if several do, the - /// first one returned by the config lookup wins. - #[serde(default)] - pub default: bool, -} - -/// Single reader entry under `[message.reader.]`. -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct ReaderConfig { - /// Shell command line invoked via `sh -c`. Stdin carries the - /// source MIME bytes; stdout is forwarded to the terminal (zero - /// bytes is fine — the reader may have spawned its own UI); - /// stderr is inherited. - #[serde(with = "command")] - pub command: Command, - - /// Marks this entry as the fallback when `read-with` is - /// invoked without a name. - #[serde(default)] - pub default: bool, -} - /// Global / per-account table rendering quirks shared across every list /// command (envelopes, mailboxes, attachments). The per-column color /// blocks live under `*.list.table.*-color` (see [`EnvelopeListTableConfig`] diff --git a/src/imap/cli.rs b/src/imap/cli.rs index 892f63d0..14d2a6f2 100644 --- a/src/imap/cli.rs +++ b/src/imap/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, envelope::cli::ImapEnvelopeCommand, flag::cli::ImapFlagCommand, id::ImapIdCommand, mailbox::cli::ImapMailboxCommand, message::cli::ImapMessageCommand, @@ -46,14 +47,19 @@ pub enum ImapCommand { } impl ImapCommand { - pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { match self { - Self::Id(cmd) => cmd.execute(printer, client), + Self::Id(cmd) => cmd.execute(printer, account, client), - Self::Envelopes(cmd) => cmd.execute(printer, client), - Self::Flags(cmd) => cmd.execute(printer, client), - Self::Mailboxes(cmd) => cmd.execute(printer, client), - Self::Messages(cmd) => cmd.execute(printer, client), + Self::Envelopes(cmd) => cmd.execute(printer, account, client), + Self::Flags(cmd) => cmd.execute(printer, account, client), + Self::Mailboxes(cmd) => cmd.execute(printer, account, client), + Self::Messages(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/imap/client.rs b/src/imap/client.rs index df406ca0..7a73dc71 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -15,12 +15,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Himalaya wrapper around [`io_imap::client::ImapClientStd`] that -//! bundles the merged [`Account`] alongside the live IMAP client. +//! Himalaya wrapper around [`io_imap::client::ImapClientStd`]. //! //! This is what every IMAP-specific subcommand receives: the dispatch //! layer (`crate::cli`) opens the session up front via -//! [`build_imap_client`] and hands the ready-to-use wrapper down. +//! [`build_imap_client`] and hands the ready-to-use wrapper down, +//! together with the merged [`Account`] as a sibling argument. use std::{ ops::{Deref, DerefMut}, @@ -40,23 +40,21 @@ use crate::{ pub struct ImapClient { inner: Inner, - pub account: Account, } impl ImapClient { - /// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL) - /// then wraps the resulting client alongside `account`. The - /// capability list reported by the connect handshake is discarded; - /// IMAP-specific subcommands that need it should call + /// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL). + /// The capability list reported by the connect handshake is + /// discarded; IMAP-specific subcommands that need it should call /// [`Inner::capability`] explicitly. - pub fn new(config: ImapConfig, account: Account) -> Result { + pub fn new(config: ImapConfig) -> Result { let mut tls: Tls = config.tls.into(); tls.rustls.alpn = vec!["imap".into()]; let sasl: Option = config.sasl.map(Sasl::try_from).transpose()?; let auto_id = resolve_auto_id_params(&config.id)?; let server = parse_imap_server(&config.server)?; let (inner, _capability) = Inner::connect(&server, &tls, config.starttls, sasl, auto_id)?; - Ok(Self { inner, account }) + Ok(Self { inner }) } } @@ -92,11 +90,13 @@ impl DerefMut for ImapClient { /// Loads the configuration, picks the active account, builds the /// merged [`Account`] then opens the IMAP session. Bails when the -/// account has no `[imap]` block. +/// account has no `[imap]` block. Returns the live client paired +/// with the merged account so subcommands receive both as sibling +/// arguments. pub fn build_imap_client( config_paths: &[PathBuf], account_name: Option<&str>, -) -> Result { +) -> Result<(Account, ImapClient)> { let mut config = load_or_wizard(config_paths)?; let (name, mut ac) = config .take_account(account_name)? @@ -106,5 +106,6 @@ pub fn build_imap_client( .take() .ok_or_else(|| anyhow!("IMAP config is missing for account `{name}`"))?; let account = Account::from(config).merge(Account::from(ac)); - ImapClient::new(imap_config, account) + let client = ImapClient::new(imap_config)?; + Ok((account, client)) } diff --git a/src/imap/envelope/cli.rs b/src/imap/envelope/cli.rs index a0281bb1..a897e69f 100644 --- a/src/imap/envelope/cli.rs +++ b/src/imap/envelope/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, envelope::{ @@ -43,12 +44,17 @@ pub enum ImapEnvelopeCommand { } impl ImapEnvelopeCommand { - pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), - Self::List(cmd) => cmd.execute(printer, client), - Self::Search(cmd) => cmd.execute(printer, client), - Self::Sort(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), + Self::List(cmd) => cmd.execute(printer, account, client), + Self::Search(cmd) => cmd.execute(printer, account, client), + Self::Sort(cmd) => cmd.execute(printer, account, client), Self::Thread(cmd) => cmd.execute(printer, client), } } diff --git a/src/imap/envelope/get.rs b/src/imap/envelope/get.rs index e8742c61..a39a14d2 100644 --- a/src/imap/envelope/get.rs +++ b/src/imap/envelope/get.rs @@ -27,6 +27,7 @@ use io_imap::types::{ use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, envelope::list::{decode_mime, format_address}, @@ -54,7 +55,12 @@ pub struct ImapEnvelopeGetCommand { } impl ImapEnvelopeGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { @@ -76,7 +82,7 @@ impl ImapEnvelopeGetCommand { }; let table = EnvelopeTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), envelope: items.into(), }; diff --git a/src/imap/envelope/list.rs b/src/imap/envelope/list.rs index a0173153..bab3d93a 100644 --- a/src/imap/envelope/list.rs +++ b/src/imap/envelope/list.rs @@ -32,6 +32,7 @@ use pimalaya_cli::printer::Printer; use rfc2047_decoder::{Decoder, RecoverStrategy}; use serde::Serialize; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, @@ -67,7 +68,12 @@ pub struct ImapEnvelopeListCommand { } impl ImapEnvelopeListCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; let exists = if self.mailbox_no_select.inner { @@ -100,13 +106,13 @@ impl ImapEnvelopeListCommand { let data = client.fetch(sequence_set, item_names, !self.sequence && has_sequence)?; let table = EnvelopesTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), colors: EnvelopeColors { - id: client.account.envelopes_list_table_id_color(), - subject: client.account.envelopes_list_table_subject_color(), - from: client.account.envelopes_list_table_from_color(), - date: client.account.envelopes_list_table_date_color(), + id: account.envelopes_list_table_id_color(), + subject: account.envelopes_list_table_subject_color(), + from: account.envelopes_list_table_from_color(), + date: account.envelopes_list_table_date_color(), }, envelopes: map_envelopes_table_entries(data), }; diff --git a/src/imap/envelope/search.rs b/src/imap/envelope/search.rs index a74ec7c1..ae73c73c 100644 --- a/src/imap/envelope/search.rs +++ b/src/imap/envelope/search.rs @@ -28,6 +28,7 @@ use io_imap::types::{ use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, @@ -75,7 +76,12 @@ pub struct ImapEnvelopeSearchCommand { } impl ImapEnvelopeSearchCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { @@ -86,9 +92,9 @@ impl ImapEnvelopeSearchCommand { let ids = client.search(criteria, !self.seq)?; let table = SearchTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), - id_color: client.account.envelopes_list_table_id_color(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), + id_color: account.envelopes_list_table_id_color(), ids: ids .into_iter() .map(|id| SearchResult { id: id.get() }) diff --git a/src/imap/envelope/sort.rs b/src/imap/envelope/sort.rs index 4b74a6b3..b121081a 100644 --- a/src/imap/envelope/sort.rs +++ b/src/imap/envelope/sort.rs @@ -27,6 +27,7 @@ use io_imap::types::{ use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, envelope::search::parse_query, mailbox::arg::MailboxNameOptionalArg, }; @@ -68,7 +69,12 @@ pub struct ImapEnvelopeSortCommand { } impl ImapEnvelopeSortCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; client.select(mailbox)?; @@ -82,7 +88,7 @@ impl ImapEnvelopeSortCommand { let ids = client.sort(sort_criteria, search_criteria, !self.seq)?; - let id_color = client.account.envelopes_list_table_id_color(); + let id_color = account.envelopes_list_table_id_color(); let table = SortResultsTable::new(ids, !self.seq, id_color); printer.out(table)?; diff --git a/src/imap/envelope/thread.rs b/src/imap/envelope/thread.rs index 519acffc..71c657ed 100644 --- a/src/imap/envelope/thread.rs +++ b/src/imap/envelope/thread.rs @@ -62,7 +62,7 @@ pub struct ImapEnvelopeThreadCommand { } impl ImapEnvelopeThreadCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { @@ -76,7 +76,7 @@ impl ImapEnvelopeThreadCommand { let all_ids = collect_thread_ids(&threads); let subjects = if !all_ids.is_empty() { - fetch_subjects(&mut client, &all_ids, !self.seq)? + fetch_subjects(client, &all_ids, !self.seq)? } else { HashMap::new() }; diff --git a/src/imap/flag/add.rs b/src/imap/flag/add.rs index a6f8c266..ab2cd83f 100644 --- a/src/imap/flag/add.rs +++ b/src/imap/flag/add.rs @@ -52,7 +52,7 @@ pub struct ImapFlagAddCommand { } impl ImapFlagAddCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/flag/cli.rs b/src/imap/flag/cli.rs index 516165be..eb3711b5 100644 --- a/src/imap/flag/cli.rs +++ b/src/imap/flag/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, flag::{ @@ -40,9 +41,14 @@ pub enum ImapFlagCommand { } impl ImapFlagCommand { - pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { match self { - Self::List(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), Self::Add(cmd) => cmd.execute(printer, client), Self::Set(cmd) => cmd.execute(printer, client), Self::Remove(cmd) => cmd.execute(printer, client), diff --git a/src/imap/flag/list.rs b/src/imap/flag/list.rs index f2f4d9a9..98b27329 100644 --- a/src/imap/flag/list.rs +++ b/src/imap/flag/list.rs @@ -24,6 +24,7 @@ use io_imap::types::flag::{Flag, FlagPerm}; use pimalaya_cli::printer::Printer; use serde::{Serialize, Serializer}; +use crate::account::context::Account; use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg}; /// List available IMAP flags for the given mailbox. @@ -38,7 +39,12 @@ pub struct ImapFlagListCommand { } impl ImapFlagListCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; let data = client.select(mailbox)?; @@ -46,8 +52,8 @@ impl ImapFlagListCommand { let permanent_flags = data.permanent_flags.unwrap_or_default(); let table = FlagsTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), flags, permanent_flags, }; diff --git a/src/imap/flag/remove.rs b/src/imap/flag/remove.rs index f36bde9b..cf4adf56 100644 --- a/src/imap/flag/remove.rs +++ b/src/imap/flag/remove.rs @@ -52,7 +52,7 @@ pub struct ImapFlagRemoveCommand { } impl ImapFlagRemoveCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/flag/set.rs b/src/imap/flag/set.rs index 8f37cee7..97224dfd 100644 --- a/src/imap/flag/set.rs +++ b/src/imap/flag/set.rs @@ -52,7 +52,7 @@ pub struct ImapFlagSetCommand { } impl ImapFlagSetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/id.rs b/src/imap/id.rs index 1307768a..d62aac30 100644 --- a/src/imap/id.rs +++ b/src/imap/id.rs @@ -27,6 +27,7 @@ use io_imap::types::{ use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::{config::ImapIdConfig, imap::client::ImapClient}; /// Get information about the IMAP server. @@ -44,7 +45,12 @@ pub struct ImapIdCommand { } impl ImapIdCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mut params: HashMap, NString<'static>> = HashMap::new(); for key in ["name", "version", "vendor", "support-url"] { let (k, v) = build_canned_pair(key)?; @@ -58,7 +64,7 @@ impl ImapIdCommand { let params = client.id(Some(params.into_iter().collect()))?; let table = ServerIdTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), server_id: params .unwrap_or_default() .into_iter() diff --git a/src/imap/mailbox/cli.rs b/src/imap/mailbox/cli.rs index 6233d1e1..ce0ff773 100644 --- a/src/imap/mailbox/cli.rs +++ b/src/imap/mailbox/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, mailbox::{ @@ -55,17 +56,22 @@ pub enum ImapMailboxCommand { } impl ImapMailboxCommand { - pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { match self { Self::Close(cmd) => cmd.execute(printer, client), Self::Create(cmd) => cmd.execute(printer, client), Self::Delete(cmd) => cmd.execute(printer, client), Self::Expunge(cmd) => cmd.execute(printer, client), - Self::List(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), Self::Purge(cmd) => cmd.execute(printer, client), Self::Rename(cmd) => cmd.execute(printer, client), Self::Select(cmd) => cmd.execute(printer, client), - Self::Status(cmd) => cmd.execute(printer, client), + Self::Status(cmd) => cmd.execute(printer, account, client), Self::Subscribe(cmd) => cmd.execute(printer, client), Self::Unselect(cmd) => cmd.execute(printer, client), Self::Unsubscribe(cmd) => cmd.execute(printer, client), diff --git a/src/imap/mailbox/close.rs b/src/imap/mailbox/close.rs index a1ef5b09..2dc5ab0f 100644 --- a/src/imap/mailbox/close.rs +++ b/src/imap/mailbox/close.rs @@ -34,7 +34,7 @@ use crate::imap::client::ImapClient; pub struct ImapMailboxCloseCommand; impl ImapMailboxCloseCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { client.close()?; printer.out(Message::new("Mailbox successfully closed")) } diff --git a/src/imap/mailbox/create.rs b/src/imap/mailbox/create.rs index 3c7b58f3..e8015a7d 100644 --- a/src/imap/mailbox/create.rs +++ b/src/imap/mailbox/create.rs @@ -32,7 +32,7 @@ pub struct ImapMailboxCreateCommand { } impl ImapMailboxCreateCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; client.create(mailbox)?; printer.out(Message::new("Mailbox successfully created")) diff --git a/src/imap/mailbox/delete.rs b/src/imap/mailbox/delete.rs index 9a776316..4a65f454 100644 --- a/src/imap/mailbox/delete.rs +++ b/src/imap/mailbox/delete.rs @@ -32,7 +32,7 @@ pub struct ImapMailboxDeleteCommand { } impl ImapMailboxDeleteCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; client.delete(mailbox)?; printer.out(Message::new("Mailbox successfully deleted")) diff --git a/src/imap/mailbox/expunge.rs b/src/imap/mailbox/expunge.rs index b67dad99..b548d994 100644 --- a/src/imap/mailbox/expunge.rs +++ b/src/imap/mailbox/expunge.rs @@ -37,7 +37,7 @@ pub struct ImapMailboxExpungeCommand { } impl ImapMailboxExpungeCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/mailbox/list.rs b/src/imap/mailbox/list.rs index 5569b709..a6c4420e 100644 --- a/src/imap/mailbox/list.rs +++ b/src/imap/mailbox/list.rs @@ -25,6 +25,7 @@ use io_imap::types::{core::QuotedChar, flag::FlagNameAttribute, mailbox::Mailbox use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::imap::client::ImapClient; /// List, search and filter mailboxes. @@ -48,7 +49,12 @@ pub struct ImapMailboxListCommand { } impl ImapMailboxListCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let reference = self.reference.try_into()?; let pattern = self.pattern.try_into()?; @@ -59,8 +65,8 @@ impl ImapMailboxListCommand { }; let table = MailboxesTable { - preset: client.account.table_preset().to_string(), - name_color: client.account.mailboxes_list_table_name_color(), + preset: account.table_preset().to_string(), + name_color: account.mailboxes_list_table_name_color(), mailboxes: mailboxes.into_iter().map(From::from).collect(), }; diff --git a/src/imap/mailbox/purge.rs b/src/imap/mailbox/purge.rs index d07e7483..44353b24 100644 --- a/src/imap/mailbox/purge.rs +++ b/src/imap/mailbox/purge.rs @@ -39,7 +39,7 @@ pub struct ImapMailboxPurgeCommand { } impl ImapMailboxPurgeCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/mailbox/rename.rs b/src/imap/mailbox/rename.rs index 94715e20..66088323 100644 --- a/src/imap/mailbox/rename.rs +++ b/src/imap/mailbox/rename.rs @@ -36,7 +36,7 @@ pub struct ImapMailboxRenameCommand { } impl ImapMailboxRenameCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let from = self.mailbox_source_name.inner.try_into()?; let to = self.mailbox_dest_name.inner.try_into()?; client.rename(from, to)?; diff --git a/src/imap/mailbox/select.rs b/src/imap/mailbox/select.rs index ed7c9f44..1fd9af6e 100644 --- a/src/imap/mailbox/select.rs +++ b/src/imap/mailbox/select.rs @@ -36,7 +36,7 @@ pub struct ImapMailboxSelectCommand { } impl ImapMailboxSelectCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; client.select(mailbox)?; printer.out(Message::new("Mailbox successfully selected")) diff --git a/src/imap/mailbox/status.rs b/src/imap/mailbox/status.rs index 10a87f22..1b1d0d7e 100644 --- a/src/imap/mailbox/status.rs +++ b/src/imap/mailbox/status.rs @@ -24,6 +24,7 @@ use io_imap::types::status::{StatusDataItem, StatusDataItemName}; use pimalaya_cli::printer::Printer; use serde::{Serialize, Serializer}; +use crate::account::context::Account; use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg}; /// Get the status of the given mailbox. @@ -37,7 +38,12 @@ pub struct ImapMailboxStatusCommand { } impl ImapMailboxStatusCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; let item_names = vec![ StatusDataItemName::Messages, @@ -50,7 +56,7 @@ impl ImapMailboxStatusCommand { let items = client.status(mailbox, item_names)?; let table = MailboxStatusTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), status: items.into(), }; diff --git a/src/imap/mailbox/subscribe.rs b/src/imap/mailbox/subscribe.rs index 2dac09e3..bc764eb9 100644 --- a/src/imap/mailbox/subscribe.rs +++ b/src/imap/mailbox/subscribe.rs @@ -32,7 +32,7 @@ pub struct ImapMailboxSubscribeCommand { } impl ImapMailboxSubscribeCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; client.subscribe(mailbox)?; printer.out(Message::new("Mailbox successfully subscribed")) diff --git a/src/imap/mailbox/unselect.rs b/src/imap/mailbox/unselect.rs index de86e7d4..4b6c5872 100644 --- a/src/imap/mailbox/unselect.rs +++ b/src/imap/mailbox/unselect.rs @@ -33,7 +33,7 @@ use crate::imap::client::ImapClient; pub struct ImapMailboxUnselectCommand; impl ImapMailboxUnselectCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { client.unselect()?; printer.out(Message::new("Mailbox successfully unselected")) } diff --git a/src/imap/mailbox/unsubscribe.rs b/src/imap/mailbox/unsubscribe.rs index 6efd024b..9876b978 100644 --- a/src/imap/mailbox/unsubscribe.rs +++ b/src/imap/mailbox/unsubscribe.rs @@ -32,7 +32,7 @@ pub struct ImapMailboxUnsubscribeCommand { } impl ImapMailboxUnsubscribeCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; client.unsubscribe(mailbox)?; printer.out(Message::new("Mailbox successfully unsubscribed")) diff --git a/src/imap/message/cli.rs b/src/imap/message/cli.rs index b87015ca..de1b66bb 100644 --- a/src/imap/message/cli.rs +++ b/src/imap/message/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::imap::{ client::ImapClient, message::{ @@ -43,12 +44,17 @@ pub enum ImapMessageCommand { } impl ImapMessageCommand { - pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { match self { Self::Save(cmd) => cmd.execute(printer, client), Self::Get(cmd) => cmd.execute(printer, client), Self::Read(cmd) => cmd.execute(printer, client), - Self::Export(cmd) => cmd.execute(printer, client), + Self::Export(cmd) => cmd.execute(printer, account, client), Self::Copy(cmd) => cmd.execute(printer, client), Self::Move(cmd) => cmd.execute(printer, client), } diff --git a/src/imap/message/copy.rs b/src/imap/message/copy.rs index 0d579abe..911a9847 100644 --- a/src/imap/message/copy.rs +++ b/src/imap/message/copy.rs @@ -48,7 +48,7 @@ pub struct ImapMessageCopyCommand { } impl ImapMessageCopyCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/message/export.rs b/src/imap/message/export.rs index c4842320..55420828 100644 --- a/src/imap/message/export.rs +++ b/src/imap/message/export.rs @@ -27,6 +27,7 @@ use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, Messag use mail_parser::{MessageParser, MimeHeaders}; use pimalaya_cli::printer::{Message, Printer}; +use crate::account::context::Account; use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameOptionalFlag}; /// Export type for message export. @@ -73,7 +74,12 @@ pub struct ImapMessageExportCommand { } impl ImapMessageExportCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut ImapClient, + ) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; client.select(mailbox)?; @@ -133,9 +139,7 @@ impl ImapMessageExportCommand { // Generate filename from subject or message-id let filename = generate_eml_filename(&message, self.id); - let dir = self - .directory - .unwrap_or_else(|| client.account.downloads_dir()); + let dir = self.directory.unwrap_or_else(|| account.downloads_dir()); if !dir.exists() { fs::create_dir_all(&dir)?; diff --git a/src/imap/message/get.rs b/src/imap/message/get.rs index 3ba23745..952c31d2 100644 --- a/src/imap/message/get.rs +++ b/src/imap/message/get.rs @@ -49,7 +49,7 @@ pub struct ImapMessageGetCommand { } impl ImapMessageGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if self.id == 0 { bail!("ID must be non-zero"); diff --git a/src/imap/message/move.rs b/src/imap/message/move.rs index 2048eb9c..1fd86a48 100644 --- a/src/imap/message/move.rs +++ b/src/imap/message/move.rs @@ -49,7 +49,7 @@ pub struct ImapMessageMoveCommand { } impl ImapMessageMoveCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/message/read.rs b/src/imap/message/read.rs index 62f4b6db..a37b1b96 100644 --- a/src/imap/message/read.rs +++ b/src/imap/message/read.rs @@ -54,7 +54,7 @@ pub struct ImapMessageReadCommand { } impl ImapMessageReadCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox = self.mailbox_name.inner.try_into()?; if !self.mailbox_no_select.inner { diff --git a/src/imap/message/save.rs b/src/imap/message/save.rs index 6baf5fe9..3087ecee 100644 --- a/src/imap/message/save.rs +++ b/src/imap/message/save.rs @@ -46,7 +46,7 @@ pub struct ImapMessageSaveCommand { } impl ImapMessageSaveCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut ImapClient) -> Result<()> { let mailbox: Mailbox<'static> = self.mailbox.inner.try_into()?; let message = self.message.parse()?; let message = Literal::try_from(message)?; diff --git a/src/jmap/cli.rs b/src/jmap/cli.rs index db3af201..8bbf40ed 100644 --- a/src/jmap/cli.rs +++ b/src/jmap/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, email::cli::JmapEmailCommand, identity::cli::JmapIdentityCommand, mailbox::cli::JmapMailboxCommand, query::JmapQueryCommand, @@ -57,15 +58,20 @@ pub enum JmapCommand { } impl JmapCommand { - pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { match self { - Self::Mailboxes(cmd) => cmd.execute(printer, client), - Self::Emails(cmd) => cmd.execute(printer, client), + Self::Mailboxes(cmd) => cmd.execute(printer, account, client), + Self::Emails(cmd) => cmd.execute(printer, account, client), - Self::Threads(cmd) => cmd.execute(printer, client), - Self::Identity(cmd) => cmd.execute(printer, client), - Self::Submission(cmd) => cmd.execute(printer, client), - Self::Vacation(cmd) => cmd.execute(printer, client), + Self::Threads(cmd) => cmd.execute(printer, account, client), + Self::Identity(cmd) => cmd.execute(printer, account, client), + Self::Submission(cmd) => cmd.execute(printer, account, client), + Self::Vacation(cmd) => cmd.execute(printer, account, client), Self::Query(cmd) => cmd.execute(printer, client), } } diff --git a/src/jmap/client.rs b/src/jmap/client.rs index 4c1bbf81..b76d62a4 100644 --- a/src/jmap/client.rs +++ b/src/jmap/client.rs @@ -43,7 +43,6 @@ use crate::{ pub struct JmapClient { inner: Inner, - pub account: Account, /// The original JMAP config block, kept around so commands like /// `email import` / `email export` can spin up their own /// auxiliary sessions (e.g. against the upload/download URL when @@ -53,9 +52,8 @@ pub struct JmapClient { impl JmapClient { /// Establishes the JMAP session (TLS, `/.well-known/jmap` - /// discovery) then wraps the resulting client alongside - /// `account`. - pub fn new(config: JmapConfig, account: Account) -> Result { + /// discovery). + pub fn new(config: JmapConfig) -> Result { let mut tls: Tls = config.tls.clone().into(); tls.rustls.alpn = vec!["http/1.1".into()]; @@ -65,11 +63,7 @@ impl JmapClient { let mut inner = Inner::connect(&url, &tls, http_auth)?; inner.session_get(&url)?; - Ok(Self { - inner, - account, - config, - }) + Ok(Self { inner, config }) } } @@ -89,11 +83,13 @@ impl DerefMut for JmapClient { /// Loads the configuration, picks the active account, builds the /// merged [`Account`] then opens the JMAP session. Bails when the -/// account has no `[jmap]` block. +/// account has no `[jmap]` block. Returns the live client paired +/// with the merged account so subcommands receive both as sibling +/// arguments. pub fn build_jmap_client( config_paths: &[PathBuf], account_name: Option<&str>, -) -> Result { +) -> Result<(Account, JmapClient)> { let mut config = load_or_wizard(config_paths)?; let (name, mut ac) = config .take_account(account_name)? @@ -103,7 +99,8 @@ pub fn build_jmap_client( .take() .ok_or_else(|| anyhow!("JMAP config is missing for account `{name}`"))?; let account = Account::from(config).merge(Account::from(ac)); - JmapClient::new(jmap_config, account) + let client = JmapClient::new(jmap_config)?; + Ok((account, client)) } /// Parses the JMAP `server` field into a [`Url`], defaulting bare diff --git a/src/jmap/email/cli.rs b/src/jmap/email/cli.rs index 3497e93a..d472a1f6 100644 --- a/src/jmap/email/cli.rs +++ b/src/jmap/email/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, email::{ @@ -47,10 +48,15 @@ pub enum JmapEmailCommand { } impl JmapEmailCommand { - pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), - Self::Query(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), + Self::Query(cmd) => cmd.execute(printer, account, client), Self::Read(cmd) => cmd.execute(printer, client), Self::Update(cmd) => cmd.execute(printer, client), Self::Delete(cmd) => cmd.execute(printer, client), diff --git a/src/jmap/email/copy.rs b/src/jmap/email/copy.rs index 8b0947b5..00510b0e 100644 --- a/src/jmap/email/copy.rs +++ b/src/jmap/email/copy.rs @@ -41,7 +41,7 @@ pub struct JmapEmailCopyCommand { } impl JmapEmailCopyCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let mailbox_ids: BTreeMap = self.mailbox_id.into_iter().map(|m| (m, true)).collect(); diff --git a/src/jmap/email/delete.rs b/src/jmap/email/delete.rs index e1da85b2..833cbf81 100644 --- a/src/jmap/email/delete.rs +++ b/src/jmap/email/delete.rs @@ -31,7 +31,7 @@ pub struct JmapEmailDestroyCommand { } impl JmapEmailDestroyCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let mut args = JmapEmailSetArgs::default(); for id in self.ids { diff --git a/src/jmap/email/export.rs b/src/jmap/email/export.rs index 7ae7f67a..789eeacb 100644 --- a/src/jmap/email/export.rs +++ b/src/jmap/email/export.rs @@ -38,7 +38,7 @@ pub struct JmapEmailExportCommand { } impl JmapEmailExportCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let properties = Some(vec![EmailProperty::Id, EmailProperty::BlobId]); let output = client.email_get(vec![self.id.clone()], properties, false, false, 0)?; diff --git a/src/jmap/email/get.rs b/src/jmap/email/get.rs index f5b13127..57552630 100644 --- a/src/jmap/email/get.rs +++ b/src/jmap/email/get.rs @@ -20,6 +20,7 @@ use clap::Parser; use log::warn; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, email::query::{EmailsChars, EmailsColors, EmailsTable}, @@ -36,7 +37,12 @@ pub struct JmapEmailGetCommand { } impl JmapEmailGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let output = client.email_get(self.ids.clone(), None, false, false, 0)?; for id in output.not_found { @@ -44,19 +50,19 @@ impl JmapEmailGetCommand { } let table = EmailsTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), colors: EmailsColors { - id: client.account.envelopes_list_table_id_color(), - flags: client.account.envelopes_list_table_flags_color(), - subject: client.account.envelopes_list_table_subject_color(), - from: client.account.envelopes_list_table_from_color(), - date: client.account.envelopes_list_table_date_color(), + id: account.envelopes_list_table_id_color(), + flags: account.envelopes_list_table_flags_color(), + subject: account.envelopes_list_table_subject_color(), + from: account.envelopes_list_table_from_color(), + date: account.envelopes_list_table_date_color(), }, chars: EmailsChars { - unseen: client.account.envelopes_list_table_unseen_char(), - flagged: client.account.envelopes_list_table_flagged_char(), - attachment: client.account.envelopes_list_table_attachment_char(), + unseen: account.envelopes_list_table_unseen_char(), + flagged: account.envelopes_list_table_flagged_char(), + attachment: account.envelopes_list_table_attachment_char(), }, emails: output.emails, }; diff --git a/src/jmap/email/import.rs b/src/jmap/email/import.rs index dcdde2b6..87be388c 100644 --- a/src/jmap/email/import.rs +++ b/src/jmap/email/import.rs @@ -64,7 +64,7 @@ pub struct JmapEmailImportCommand { } impl JmapEmailImportCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let data = self.message.parse()?.into_bytes(); let session = client.session().expect("session loaded by new_jmap_client"); diff --git a/src/jmap/email/parse.rs b/src/jmap/email/parse.rs index d62bedfa..29b6b6e3 100644 --- a/src/jmap/email/parse.rs +++ b/src/jmap/email/parse.rs @@ -35,7 +35,7 @@ pub struct JmapEmailParseCommand { } impl JmapEmailParseCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let output = client.email_parse(self.blob_ids.clone(), None)?; for id in output.not_found { diff --git a/src/jmap/email/query.rs b/src/jmap/email/query.rs index 1efb8564..ebdd9aa7 100644 --- a/src/jmap/email/query.rs +++ b/src/jmap/email/query.rs @@ -27,6 +27,7 @@ use io_jmap::{ use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::jmap::client::JmapClient; /// Query JMAP emails (Email/query + Email/get). @@ -104,7 +105,12 @@ pub struct JmapEmailQueryCommand { } impl JmapEmailQueryCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let filter = { let f = EmailFilter { in_mailbox: self.mailbox, @@ -164,19 +170,19 @@ impl JmapEmailQueryCommand { )?; let table = EmailsTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), colors: EmailsColors { - id: client.account.envelopes_list_table_id_color(), - flags: client.account.envelopes_list_table_flags_color(), - subject: client.account.envelopes_list_table_subject_color(), - from: client.account.envelopes_list_table_from_color(), - date: client.account.envelopes_list_table_date_color(), + id: account.envelopes_list_table_id_color(), + flags: account.envelopes_list_table_flags_color(), + subject: account.envelopes_list_table_subject_color(), + from: account.envelopes_list_table_from_color(), + date: account.envelopes_list_table_date_color(), }, chars: EmailsChars { - unseen: client.account.envelopes_list_table_unseen_char(), - flagged: client.account.envelopes_list_table_flagged_char(), - attachment: client.account.envelopes_list_table_attachment_char(), + unseen: account.envelopes_list_table_unseen_char(), + flagged: account.envelopes_list_table_flagged_char(), + attachment: account.envelopes_list_table_attachment_char(), }, emails: output.emails, }; diff --git a/src/jmap/email/read.rs b/src/jmap/email/read.rs index a969f51c..468176c6 100644 --- a/src/jmap/email/read.rs +++ b/src/jmap/email/read.rs @@ -38,7 +38,7 @@ pub struct JmapEmailReadCommand { } impl JmapEmailReadCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let output = client.email_get(self.ids.clone(), None, !self.html, self.html, 0)?; for id in output.not_found { diff --git a/src/jmap/email/update.rs b/src/jmap/email/update.rs index d8c9b2e9..511d8832 100644 --- a/src/jmap/email/update.rs +++ b/src/jmap/email/update.rs @@ -57,7 +57,7 @@ pub struct JmapEmailUpdateCommand { } impl JmapEmailUpdateCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let mut args = JmapEmailSetArgs::default(); for id in &self.ids { diff --git a/src/jmap/identity/cli.rs b/src/jmap/identity/cli.rs index a444c498..f2f8f8d7 100644 --- a/src/jmap/identity/cli.rs +++ b/src/jmap/identity/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, identity::{ @@ -45,9 +46,14 @@ pub enum JmapIdentityCommand { } impl JmapIdentityCommand { - pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), Self::Create(cmd) => cmd.execute(printer, client), Self::Update(cmd) => cmd.execute(printer, client), Self::Delete(cmd) => cmd.execute(printer, client), diff --git a/src/jmap/identity/create.rs b/src/jmap/identity/create.rs index 8dadc302..aca85b18 100644 --- a/src/jmap/identity/create.rs +++ b/src/jmap/identity/create.rs @@ -41,7 +41,7 @@ pub struct JmapIdentityCreateCommand { } impl JmapIdentityCreateCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let identity = IdentityCreate { name: self.name.clone(), email: self.email.clone(), diff --git a/src/jmap/identity/delete.rs b/src/jmap/identity/delete.rs index 9749e638..8ab7b1b0 100644 --- a/src/jmap/identity/delete.rs +++ b/src/jmap/identity/delete.rs @@ -31,7 +31,7 @@ pub struct JmapIdentityDeleteCommand { } impl JmapIdentityDeleteCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let mut args = JmapIdentitySetArgs::default(); for id in self.ids { diff --git a/src/jmap/identity/get.rs b/src/jmap/identity/get.rs index 6911f671..f7e88ac3 100644 --- a/src/jmap/identity/get.rs +++ b/src/jmap/identity/get.rs @@ -25,6 +25,7 @@ use log::warn; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::jmap::client::JmapClient; /// Get JMAP identities (Identity/get). @@ -39,7 +40,12 @@ pub struct JmapIdentityGetCommand { } impl JmapIdentityGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let output = client.identity_get(self.ids)?; for id in output.not_found { @@ -47,7 +53,7 @@ impl JmapIdentityGetCommand { } let table = IdentitiesTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), identities: output.identities, }; diff --git a/src/jmap/identity/update.rs b/src/jmap/identity/update.rs index 0ba4e8b0..da7ccf37 100644 --- a/src/jmap/identity/update.rs +++ b/src/jmap/identity/update.rs @@ -42,7 +42,7 @@ pub struct JmapIdentityUpdateCommand { } impl JmapIdentityUpdateCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let patch = IdentityUpdate { name: self.name, reply_to: None, diff --git a/src/jmap/mailbox/cli.rs b/src/jmap/mailbox/cli.rs index 5e2fab93..f15e5f6a 100644 --- a/src/jmap/mailbox/cli.rs +++ b/src/jmap/mailbox/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, mailbox::{ @@ -41,10 +42,15 @@ pub enum JmapMailboxCommand { } impl JmapMailboxCommand { - pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), - Self::Query(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), + Self::Query(cmd) => cmd.execute(printer, account, client), Self::Create(cmd) => cmd.execute(printer, client), Self::Update(cmd) => cmd.execute(printer, client), Self::Destroy(cmd) => cmd.execute(printer, client), diff --git a/src/jmap/mailbox/create.rs b/src/jmap/mailbox/create.rs index 1e457c4e..63348245 100644 --- a/src/jmap/mailbox/create.rs +++ b/src/jmap/mailbox/create.rs @@ -42,7 +42,7 @@ pub struct JmapMailboxCreateCommand { } impl JmapMailboxCreateCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let new_mailbox = MailboxCreate { name: Some(self.name.clone()), parent_id: self.parent_id, diff --git a/src/jmap/mailbox/destroy.rs b/src/jmap/mailbox/destroy.rs index 9097e163..df439225 100644 --- a/src/jmap/mailbox/destroy.rs +++ b/src/jmap/mailbox/destroy.rs @@ -35,7 +35,7 @@ pub struct JmapMailboxDestroyCommand { } impl JmapMailboxDestroyCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let args = JmapMailboxSetArgs { destroy: Some(self.ids.clone()), on_destroy_remove_emails: if self.purge { Some(true) } else { None }, diff --git a/src/jmap/mailbox/get.rs b/src/jmap/mailbox/get.rs index 07430421..98a07525 100644 --- a/src/jmap/mailbox/get.rs +++ b/src/jmap/mailbox/get.rs @@ -20,6 +20,7 @@ use clap::Parser; use log::warn; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, mailbox::query::{MailboxColors, MailboxesTable}, @@ -34,7 +35,12 @@ pub struct JmapMailboxGetCommand { } impl JmapMailboxGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let output = client.mailbox_get(Some(self.ids.clone()), None)?; for id in output.not_found { @@ -42,12 +48,12 @@ impl JmapMailboxGetCommand { } let table = MailboxesTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), colors: MailboxColors { - id: client.account.mailboxes_list_table_id_color(), - name: client.account.mailboxes_list_table_name_color(), - total: client.account.mailboxes_list_table_total_color(), - unread: client.account.mailboxes_list_table_unread_color(), + id: account.mailboxes_list_table_id_color(), + name: account.mailboxes_list_table_name_color(), + total: account.mailboxes_list_table_total_color(), + unread: account.mailboxes_list_table_unread_color(), }, mailboxes: output.mailboxes, }; diff --git a/src/jmap/mailbox/query.rs b/src/jmap/mailbox/query.rs index f4e5ef0a..1be89c98 100644 --- a/src/jmap/mailbox/query.rs +++ b/src/jmap/mailbox/query.rs @@ -26,6 +26,7 @@ use io_jmap::rfc8621::mailbox::{ use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::jmap::client::JmapClient; /// Query JMAP mailboxes (Mailbox/query + Mailbox/get). @@ -72,7 +73,12 @@ pub struct JmapMailboxQueryCommand { } impl JmapMailboxQueryCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let filter = { let f = MailboxFilter { parent_id: self.parent_id, @@ -105,12 +111,12 @@ impl JmapMailboxQueryCommand { )?; let table = MailboxesTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), colors: MailboxColors { - id: client.account.mailboxes_list_table_id_color(), - name: client.account.mailboxes_list_table_name_color(), - total: client.account.mailboxes_list_table_total_color(), - unread: client.account.mailboxes_list_table_unread_color(), + id: account.mailboxes_list_table_id_color(), + name: account.mailboxes_list_table_name_color(), + total: account.mailboxes_list_table_total_color(), + unread: account.mailboxes_list_table_unread_color(), }, mailboxes: output.mailboxes, }; diff --git a/src/jmap/mailbox/update.rs b/src/jmap/mailbox/update.rs index d75d84d4..06cbc365 100644 --- a/src/jmap/mailbox/update.rs +++ b/src/jmap/mailbox/update.rs @@ -57,7 +57,7 @@ pub struct JmapMailboxUpdateCommand { } impl JmapMailboxUpdateCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let is_subscribed = if self.subscribe { Some(true) } else if self.unsubscribe { diff --git a/src/jmap/query.rs b/src/jmap/query.rs index e7b186c2..4bf650eb 100644 --- a/src/jmap/query.rs +++ b/src/jmap/query.rs @@ -54,7 +54,7 @@ pub struct JmapQueryCommand { } impl JmapQueryCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let raw = if self.method_calls.is_empty() || self.method_calls.first().map(|s| s.as_str()) == Some("-") { diff --git a/src/jmap/submission/cancel.rs b/src/jmap/submission/cancel.rs index d53f3665..30e2cb8b 100644 --- a/src/jmap/submission/cancel.rs +++ b/src/jmap/submission/cancel.rs @@ -33,7 +33,7 @@ pub struct JmapSubmissionCancelCommand { } impl JmapSubmissionCancelCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let output = client.email_submission_cancel(self.ids.clone())?; if !output.not_updated.is_empty() { diff --git a/src/jmap/submission/cli.rs b/src/jmap/submission/cli.rs index 9cde94cb..bc86320b 100644 --- a/src/jmap/submission/cli.rs +++ b/src/jmap/submission/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, submission::{ @@ -43,11 +44,16 @@ pub enum JmapSubmissionCommand { } impl JmapSubmissionCommand { - pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), - Self::Query(cmd) => cmd.execute(printer, client), - Self::Create(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), + Self::Query(cmd) => cmd.execute(printer, account, client), + Self::Create(cmd) => cmd.execute(printer, account, client), Self::Cancel(cmd) => cmd.execute(printer, client), } } diff --git a/src/jmap/submission/create.rs b/src/jmap/submission/create.rs index fd6cc3cc..0492fef3 100644 --- a/src/jmap/submission/create.rs +++ b/src/jmap/submission/create.rs @@ -24,6 +24,7 @@ use io_jmap::rfc8621::email_submission::{ }; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, error::format_set_error, submission::query::SubmissionsTable, }; @@ -52,7 +53,12 @@ pub struct JmapSubmissionCreateCommand { } impl JmapSubmissionCreateCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let envelope = if let Some(mail_from_addr) = self.mail_from { let rcpt_to = self .rcpt_to @@ -91,7 +97,7 @@ impl JmapSubmissionCreateCommand { } let table = SubmissionsTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), submissions: output.created.into_values().collect(), }; diff --git a/src/jmap/submission/get.rs b/src/jmap/submission/get.rs index 11b066de..2c41fe3e 100644 --- a/src/jmap/submission/get.rs +++ b/src/jmap/submission/get.rs @@ -20,6 +20,7 @@ use clap::Parser; use log::warn; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{client::JmapClient, submission::query::SubmissionsTable}; /// Get JMAP email submissions by ID (EmailSubmission/get). @@ -31,7 +32,12 @@ pub struct JmapSubmissionGetCommand { } impl JmapSubmissionGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let output = client.email_submission_get(Some(self.ids.clone()))?; for id in output.not_found { @@ -39,7 +45,7 @@ impl JmapSubmissionGetCommand { } let table = SubmissionsTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), submissions: output.submissions, }; diff --git a/src/jmap/submission/query.rs b/src/jmap/submission/query.rs index a3a15c4b..3b848df0 100644 --- a/src/jmap/submission/query.rs +++ b/src/jmap/submission/query.rs @@ -24,6 +24,7 @@ use io_jmap::rfc8621::email_submission::{EmailSubmission, EmailSubmissionFilter, use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::jmap::client::JmapClient; /// CLI proxy for [`UndoStatus`]. @@ -69,7 +70,12 @@ pub struct JmapSubmissionQueryCommand { } impl JmapSubmissionQueryCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let filter = { let f = EmailSubmissionFilter { undo_status: self.undo_status.map(Into::into), @@ -91,7 +97,7 @@ impl JmapSubmissionQueryCommand { )?; let table = SubmissionsTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), submissions: output.submissions, }; diff --git a/src/jmap/thread/cli.rs b/src/jmap/thread/cli.rs index 11b97b4b..5602e420 100644 --- a/src/jmap/thread/cli.rs +++ b/src/jmap/thread/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{client::JmapClient, thread::get::JmapThreadGetCommand}; /// Manage JMAP threads. @@ -29,9 +30,14 @@ pub enum JmapThreadCommand { } impl JmapThreadCommand { - pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/jmap/thread/get.rs b/src/jmap/thread/get.rs index 00f01eae..d33c087e 100644 --- a/src/jmap/thread/get.rs +++ b/src/jmap/thread/get.rs @@ -25,6 +25,7 @@ use log::warn; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::jmap::client::JmapClient; /// Get JMAP threads by ID (Thread/get). @@ -38,7 +39,12 @@ pub struct JmapThreadGetCommand { } impl JmapThreadGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let output = client.thread_get(self.ids.clone())?; for id in output.not_found { @@ -46,7 +52,7 @@ impl JmapThreadGetCommand { } printer.out(ThreadsTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), threads: output.threads, }) } diff --git a/src/jmap/vacation/cli.rs b/src/jmap/vacation/cli.rs index c3206ec7..fc8c0057 100644 --- a/src/jmap/vacation/cli.rs +++ b/src/jmap/vacation/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::jmap::{ client::JmapClient, vacation::{get::JmapVacationGetCommand, set::JmapVacationSetCommand}, @@ -34,9 +35,14 @@ pub enum JmapVacationCommand { } impl JmapVacationCommand { - pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), Self::Set(cmd) => cmd.execute(printer, client), } } diff --git a/src/jmap/vacation/get.rs b/src/jmap/vacation/get.rs index 0cd2cdee..303b907e 100644 --- a/src/jmap/vacation/get.rs +++ b/src/jmap/vacation/get.rs @@ -24,6 +24,7 @@ use io_jmap::rfc8621::{capabilities::VACATION_RESPONSE, vacation_response::Vacat use pimalaya_cli::printer::{Message, Printer}; use serde::Serialize; +use crate::account::context::Account; use crate::jmap::client::JmapClient; /// Get the JMAP vacation response (VacationResponse/get). @@ -31,7 +32,12 @@ use crate::jmap::client::JmapClient; pub struct JmapVacationGetCommand; impl JmapVacationGetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut JmapClient, + ) -> Result<()> { let has_vacation = client .session() .map(|s| s.capabilities.contains_key(VACATION_RESPONSE)) @@ -46,7 +52,7 @@ impl JmapVacationGetCommand { }; let table = VacationTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), vacation, }; diff --git a/src/jmap/vacation/set.rs b/src/jmap/vacation/set.rs index 75d12552..5bc7d9a9 100644 --- a/src/jmap/vacation/set.rs +++ b/src/jmap/vacation/set.rs @@ -57,7 +57,7 @@ pub struct JmapVacationSetCommand { } impl JmapVacationSetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut JmapClient) -> Result<()> { let has_vacation = client .session() .map(|s| s.capabilities.contains_key(VACATION_RESPONSE)) diff --git a/src/m2dir/cli.rs b/src/m2dir/cli.rs index 5f8a391a..fe383162 100644 --- a/src/m2dir/cli.rs +++ b/src/m2dir/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::m2dir::{ client::M2dirClient, create::M2dirMailboxCreateCommand, delete::M2dirMailboxDeleteCommand, envelope::cli::M2dirEnvelopeCommand, flag::cli::M2dirFlagCommand, @@ -48,15 +49,20 @@ pub enum M2dirCommand { } impl M2dirCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut M2dirClient, + ) -> Result<()> { match self { Self::Create(cmd) => cmd.execute(printer, client), Self::Delete(cmd) => cmd.execute(printer, client), - Self::List(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), Self::Messages(cmd) => cmd.execute(printer, client), - Self::Flags(cmd) => cmd.execute(printer, client), - Self::Envelopes(cmd) => cmd.execute(printer, client), + Self::Flags(cmd) => cmd.execute(printer, account, client), + Self::Envelopes(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/m2dir/client.rs b/src/m2dir/client.rs index 347028a0..4205724b 100644 --- a/src/m2dir/client.rs +++ b/src/m2dir/client.rs @@ -15,8 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Himalaya wrapper around [`io_m2dir::client::M2dirClient`] bundling -//! the merged [`Account`] alongside the m2dir client. +//! Himalaya wrapper around [`io_m2dir::client::M2dirClient`]. use std::{ ops::{Deref, DerefMut}, @@ -31,15 +30,14 @@ use crate::{account::context::Account, cli::load_or_wizard, config::M2dirConfig} pub struct M2dirClient { inner: Inner, - pub account: Account, } impl M2dirClient { /// Builds an [`M2dirClient`] rooted at the configured m2store /// path. - pub fn new(config: M2dirConfig, account: Account) -> Self { + pub fn new(config: M2dirConfig) -> Self { let inner = Inner::new(config.root.to_string_lossy().into_owned()); - Self { inner, account } + Self { inner } } } @@ -59,11 +57,13 @@ impl DerefMut for M2dirClient { /// Loads the configuration, picks the active account, builds the /// merged [`Account`] then opens the m2dir client. Bails when the -/// account has no `[m2dir]` block. +/// account has no `[m2dir]` block. Returns the client paired with +/// the merged account so subcommands receive both as sibling +/// arguments. pub fn build_m2dir_client( config_paths: &[PathBuf], account_name: Option<&str>, -) -> Result { +) -> Result<(Account, M2dirClient)> { let mut config = load_or_wizard(config_paths)?; let (name, mut ac) = config .take_account(account_name)? @@ -73,5 +73,5 @@ pub fn build_m2dir_client( .take() .ok_or_else(|| anyhow!("m2dir config is missing for account `{name}`"))?; let account = Account::from(config).merge(Account::from(ac)); - Ok(M2dirClient::new(m2dir_config, account)) + Ok((account, M2dirClient::new(m2dir_config))) } diff --git a/src/m2dir/create.rs b/src/m2dir/create.rs index 902243f7..57b3c743 100644 --- a/src/m2dir/create.rs +++ b/src/m2dir/create.rs @@ -32,7 +32,7 @@ pub struct M2dirMailboxCreateCommand { } impl M2dirMailboxCreateCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { client.init_store()?; client.create_mailbox(&self.m2dir_name.inner)?; printer.out(Message::new("m2dir folder successfully created")) diff --git a/src/m2dir/delete.rs b/src/m2dir/delete.rs index dfdde34f..ebdb3503 100644 --- a/src/m2dir/delete.rs +++ b/src/m2dir/delete.rs @@ -29,7 +29,7 @@ pub struct M2dirMailboxDeleteCommand { } impl M2dirMailboxDeleteCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir_name.inner)?; client.delete_mailbox(path)?; diff --git a/src/m2dir/envelope/cli.rs b/src/m2dir/envelope/cli.rs index abe0b2aa..850f960f 100644 --- a/src/m2dir/envelope/cli.rs +++ b/src/m2dir/envelope/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::m2dir::{ client::M2dirClient, envelope::{get::M2dirEnvelopeGetCommand, list::M2dirEnvelopeListCommand}, @@ -35,10 +36,15 @@ pub enum M2dirEnvelopeCommand { } impl M2dirEnvelopeCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut M2dirClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), - Self::List(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), + Self::List(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/m2dir/envelope/get.rs b/src/m2dir/envelope/get.rs index 2766f778..210ef000 100644 --- a/src/m2dir/envelope/get.rs +++ b/src/m2dir/envelope/get.rs @@ -24,6 +24,7 @@ use mail_parser::{Header, MessageParser}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::m2dir::{ arg::{M2dirNameFlag, MessageIdArg}, client::M2dirClient, @@ -42,7 +43,12 @@ pub struct M2dirEnvelopeGetCommand { } impl M2dirEnvelopeGetCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut M2dirClient, + ) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; @@ -54,7 +60,7 @@ impl M2dirEnvelopeGetCommand { }; let table = EnvelopeTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), headers: parsed.headers(), }; diff --git a/src/m2dir/envelope/list.rs b/src/m2dir/envelope/list.rs index f2860fe2..80b841ca 100644 --- a/src/m2dir/envelope/list.rs +++ b/src/m2dir/envelope/list.rs @@ -24,6 +24,7 @@ use mail_parser::MessageParser; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::m2dir::{arg::M2dirNameFlag, client::M2dirClient}; /// List M2DIR envelopes from the given mailbox. @@ -37,7 +38,12 @@ pub struct M2dirEnvelopeListCommand { } impl M2dirEnvelopeListCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut M2dirClient, + ) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; @@ -72,13 +78,13 @@ impl M2dirEnvelopeListCommand { envelopes.sort_by(|a, b| a.date.cmp(&b.date)); let table = EnvelopesTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), colors: EnvelopeColors { - id: client.account.envelopes_list_table_id_color(), - subject: client.account.envelopes_list_table_subject_color(), - from: client.account.envelopes_list_table_from_color(), - date: client.account.envelopes_list_table_date_color(), + id: account.envelopes_list_table_id_color(), + subject: account.envelopes_list_table_subject_color(), + from: account.envelopes_list_table_from_color(), + date: account.envelopes_list_table_date_color(), }, envelopes, }; diff --git a/src/m2dir/flag/add.rs b/src/m2dir/flag/add.rs index be5fa21a..3c06e11f 100644 --- a/src/m2dir/flag/add.rs +++ b/src/m2dir/flag/add.rs @@ -42,7 +42,7 @@ pub struct M2dirFlagAddCommand { } impl M2dirFlagAddCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; diff --git a/src/m2dir/flag/cli.rs b/src/m2dir/flag/cli.rs index 826a6825..3a2541af 100644 --- a/src/m2dir/flag/cli.rs +++ b/src/m2dir/flag/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::m2dir::{ client::M2dirClient, flag::{ @@ -40,9 +41,14 @@ pub enum M2dirFlagCommand { } impl M2dirFlagCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut M2dirClient, + ) -> Result<()> { match self { - Self::List(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), Self::Add(cmd) => cmd.execute(printer, client), Self::Set(cmd) => cmd.execute(printer, client), Self::Remove(cmd) => cmd.execute(printer, client), diff --git a/src/m2dir/flag/list.rs b/src/m2dir/flag/list.rs index d4b3d52c..896c8bbb 100644 --- a/src/m2dir/flag/list.rs +++ b/src/m2dir/flag/list.rs @@ -23,6 +23,7 @@ use comfy_table::{Cell, ContentArrangement, Row, Table}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::m2dir::{ arg::{M2dirNameFlag, MessageIdArg}, client::M2dirClient, @@ -41,15 +42,20 @@ pub struct M2dirFlagListCommand { } impl M2dirFlagListCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut M2dirClient, + ) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; let flags = client.read_flags(&m2dir, &self.id.inner)?; let table = FlagsTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), flags: flags.iter().map(str::to_owned).collect(), }; diff --git a/src/m2dir/flag/remove.rs b/src/m2dir/flag/remove.rs index 81828655..7855b094 100644 --- a/src/m2dir/flag/remove.rs +++ b/src/m2dir/flag/remove.rs @@ -40,7 +40,7 @@ pub struct M2dirFlagRemoveCommand { } impl M2dirFlagRemoveCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; diff --git a/src/m2dir/flag/set.rs b/src/m2dir/flag/set.rs index 8f31570c..eb33b117 100644 --- a/src/m2dir/flag/set.rs +++ b/src/m2dir/flag/set.rs @@ -40,7 +40,7 @@ pub struct M2dirFlagSetCommand { } impl M2dirFlagSetCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; diff --git a/src/m2dir/list.rs b/src/m2dir/list.rs index d7ad1756..73c8bc4f 100644 --- a/src/m2dir/list.rs +++ b/src/m2dir/list.rs @@ -24,6 +24,7 @@ use io_m2dir::m2dir::M2dir; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::m2dir::client::M2dirClient; /// List m2dir folders found under the store root. @@ -31,12 +32,17 @@ use crate::m2dir::client::M2dirClient; pub struct M2dirMailboxListCommand; impl M2dirMailboxListCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut M2dirClient, + ) -> Result<()> { let m2dirs = client.list_mailboxes()?; let table = M2dirsTable { - preset: client.account.table_preset().to_string(), - name_color: client.account.mailboxes_list_table_name_color(), + preset: account.table_preset().to_string(), + name_color: account.mailboxes_list_table_name_color(), rows: m2dirs.into_iter().map(From::from).collect(), }; diff --git a/src/m2dir/message/cli.rs b/src/m2dir/message/cli.rs index 9c1c7109..08e4f316 100644 --- a/src/m2dir/message/cli.rs +++ b/src/m2dir/message/cli.rs @@ -40,7 +40,7 @@ pub enum M2dirMessageCommand { } impl M2dirMessageCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { match self { Self::Save(cmd) => cmd.execute(printer, client), Self::Get(cmd) => cmd.execute(printer, client), diff --git a/src/m2dir/message/export.rs b/src/m2dir/message/export.rs index 2a5577cc..1345362e 100644 --- a/src/m2dir/message/export.rs +++ b/src/m2dir/message/export.rs @@ -57,7 +57,7 @@ pub struct M2dirMessageExportCommand { } impl M2dirMessageExportCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; diff --git a/src/m2dir/message/get.rs b/src/m2dir/message/get.rs index 2e11f4ec..badb9700 100644 --- a/src/m2dir/message/get.rs +++ b/src/m2dir/message/get.rs @@ -41,7 +41,7 @@ pub struct M2dirMessageGetCommand { } impl M2dirMessageGetCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; diff --git a/src/m2dir/message/read.rs b/src/m2dir/message/read.rs index 508a041e..868d38a3 100644 --- a/src/m2dir/message/read.rs +++ b/src/m2dir/message/read.rs @@ -48,7 +48,7 @@ pub struct M2dirMessageReadCommand { } impl M2dirMessageReadCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; diff --git a/src/m2dir/message/save.rs b/src/m2dir/message/save.rs index 4fc7b530..6c50b2ec 100644 --- a/src/m2dir/message/save.rs +++ b/src/m2dir/message/save.rs @@ -50,7 +50,7 @@ pub struct M2dirMessageSaveCommand { } impl M2dirMessageSaveCommand { - pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut M2dirClient) -> Result<()> { let store = client.open_store()?; let path = store.resolve_folder_path(&self.m2dir.inner)?; let m2dir = client.open_m2dir(path)?; diff --git a/src/maildir/cli.rs b/src/maildir/cli.rs index 20131c00..9213b2e4 100644 --- a/src/maildir/cli.rs +++ b/src/maildir/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::maildir::{ client::MaildirClient, create::MaildirMailboxCreateCommand, delete::MaildirMailboxDeleteCommand, envelope::cli::MaildirEnvelopeCommand, @@ -48,16 +49,21 @@ pub enum MaildirCommand { } impl MaildirCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut MaildirClient, + ) -> Result<()> { match self { Self::Create(cmd) => cmd.execute(printer, client), Self::Rename(cmd) => cmd.execute(printer, client), Self::Delete(cmd) => cmd.execute(printer, client), - Self::List(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), Self::Messages(cmd) => cmd.execute(printer, client), - Self::Flags(cmd) => cmd.execute(printer, client), - Self::Envelopes(cmd) => cmd.execute(printer, client), + Self::Flags(cmd) => cmd.execute(printer, account, client), + Self::Envelopes(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/maildir/client.rs b/src/maildir/client.rs index f1101966..904b049e 100644 --- a/src/maildir/client.rs +++ b/src/maildir/client.rs @@ -15,12 +15,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Himalaya wrapper around [`io_maildir::client::MaildirClient`] -//! that bundles the merged [`Account`] alongside the maildir client. +//! Himalaya wrapper around [`io_maildir::client::MaildirClient`]. //! //! Built up front by the dispatch layer (`crate::cli`) via //! [`build_maildir_client`] and handed down to every maildir-specific -//! subcommand. +//! subcommand, together with the merged [`Account`] as a sibling +//! argument. use std::{ ops::{Deref, DerefMut}, @@ -35,7 +35,6 @@ use crate::{account::context::Account, cli::load_or_wizard, config::MaildirConfi pub struct MaildirClient { inner: Inner, - pub account: Account, /// Filesystem root of the configured maildir. Kept on the wrapper /// so commands can join sub-paths (per-mailbox) without needing /// the original [`MaildirConfig`]. @@ -45,14 +44,10 @@ pub struct MaildirClient { impl MaildirClient { /// Builds a [`MaildirClient`] rooted at the configured maildir /// path. - pub fn new(config: MaildirConfig, account: Account) -> Self { + pub fn new(config: MaildirConfig) -> Self { let root = config.root.clone(); let inner = Inner::new(root.to_string_lossy().into_owned()); - Self { - inner, - account, - root, - } + Self { inner, root } } /// Resolves a maildir CLI argument: tries `path` as-is first, then @@ -83,11 +78,13 @@ impl DerefMut for MaildirClient { /// Loads the configuration, picks the active account, builds the /// merged [`Account`] then opens the maildir client. Bails when the -/// account has no `[maildir]` block. +/// account has no `[maildir]` block. Returns the client paired with +/// the merged account so subcommands receive both as sibling +/// arguments. pub fn build_maildir_client( config_paths: &[PathBuf], account_name: Option<&str>, -) -> Result { +) -> Result<(Account, MaildirClient)> { let mut config = load_or_wizard(config_paths)?; let (name, mut ac) = config .take_account(account_name)? @@ -97,5 +94,5 @@ pub fn build_maildir_client( .take() .ok_or_else(|| anyhow!("Maildir config is missing for account `{name}`"))?; let account = Account::from(config).merge(Account::from(ac)); - Ok(MaildirClient::new(maildir_config, account)) + Ok((account, MaildirClient::new(maildir_config))) } diff --git a/src/maildir/create.rs b/src/maildir/create.rs index eb4aad4b..bf8306fc 100644 --- a/src/maildir/create.rs +++ b/src/maildir/create.rs @@ -32,7 +32,7 @@ pub struct MaildirMailboxCreateCommand { } impl MaildirMailboxCreateCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let path = client .root .join(&self.maildir_name.inner) diff --git a/src/maildir/delete.rs b/src/maildir/delete.rs index 3c5f6a6b..0bbf3efc 100644 --- a/src/maildir/delete.rs +++ b/src/maildir/delete.rs @@ -32,7 +32,7 @@ pub struct MaildirMailboxDeleteCommand { } impl MaildirMailboxDeleteCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let path = client .root .join(&self.maildir_path.inner) diff --git a/src/maildir/envelope/cli.rs b/src/maildir/envelope/cli.rs index 83a4dd6a..41f2df76 100644 --- a/src/maildir/envelope/cli.rs +++ b/src/maildir/envelope/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::maildir::{ client::MaildirClient, envelope::{get::MaildirEnvelopeGetCommand, list::MaildirEnvelopeListCommand}, @@ -36,10 +37,15 @@ pub enum MaildirEnvelopeCommand { } impl MaildirEnvelopeCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut MaildirClient, + ) -> Result<()> { match self { - Self::Get(cmd) => cmd.execute(printer, client), - Self::List(cmd) => cmd.execute(printer, client), + Self::Get(cmd) => cmd.execute(printer, account, client), + Self::List(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/maildir/envelope/get.rs b/src/maildir/envelope/get.rs index cdfc5712..ab28c5d1 100644 --- a/src/maildir/envelope/get.rs +++ b/src/maildir/envelope/get.rs @@ -24,6 +24,7 @@ use mail_parser::Header; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::maildir::{ arg::{MaildirPathFlag, MessageIdArg}, client::MaildirClient, @@ -43,7 +44,12 @@ pub struct MaildirEnvelopeGetCommand { } impl MaildirEnvelopeGetCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut MaildirClient, + ) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let message = client.get(maildir, &self.id.inner)?; @@ -55,7 +61,7 @@ impl MaildirEnvelopeGetCommand { }; let table = EnvelopeTable { - preset: client.account.table_preset().to_string(), + preset: account.table_preset().to_string(), headers: parsed.headers(), }; diff --git a/src/maildir/envelope/list.rs b/src/maildir/envelope/list.rs index 79ffec49..c2a1683d 100644 --- a/src/maildir/envelope/list.rs +++ b/src/maildir/envelope/list.rs @@ -23,6 +23,7 @@ use comfy_table::{Cell, Color, ContentArrangement, Row, Table}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::maildir::{arg::MaildirPathFlag, client::MaildirClient}; /// List MAILDIR envelopes from the given mailbox. @@ -37,7 +38,12 @@ pub struct MaildirEnvelopeListCommand { } impl MaildirEnvelopeListCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut MaildirClient, + ) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let entries: Vec<_> = client.list_entries(maildir)?.into_iter().collect(); @@ -76,13 +82,13 @@ impl MaildirEnvelopeListCommand { envelopes.sort_by(|a, b| a.date.cmp(&b.date)); let table = EnvelopesTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), colors: EnvelopeColors { - id: client.account.envelopes_list_table_id_color(), - subject: client.account.envelopes_list_table_subject_color(), - from: client.account.envelopes_list_table_from_color(), - date: client.account.envelopes_list_table_date_color(), + id: account.envelopes_list_table_id_color(), + subject: account.envelopes_list_table_subject_color(), + from: account.envelopes_list_table_from_color(), + date: account.envelopes_list_table_date_color(), }, envelopes, }; diff --git a/src/maildir/flag/add.rs b/src/maildir/flag/add.rs index b52fe578..e02339e9 100644 --- a/src/maildir/flag/add.rs +++ b/src/maildir/flag/add.rs @@ -43,7 +43,7 @@ pub struct MaildirFlagAddCommand { } impl MaildirFlagAddCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let flags = MaildirFlags::from_iter(self.flags.into_iter().map(Into::into)); diff --git a/src/maildir/flag/cli.rs b/src/maildir/flag/cli.rs index e04bc38a..bf357bef 100644 --- a/src/maildir/flag/cli.rs +++ b/src/maildir/flag/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::maildir::{ client::MaildirClient, flag::{ @@ -40,9 +41,14 @@ pub enum MaildirFlagCommand { } impl MaildirFlagCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut MaildirClient, + ) -> Result<()> { match self { - Self::List(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account), Self::Add(cmd) => cmd.execute(printer, client), Self::Set(cmd) => cmd.execute(printer, client), Self::Remove(cmd) => cmd.execute(printer, client), diff --git a/src/maildir/flag/list.rs b/src/maildir/flag/list.rs index 8b046a37..1b1e7b8c 100644 --- a/src/maildir/flag/list.rs +++ b/src/maildir/flag/list.rs @@ -24,7 +24,7 @@ use io_maildir::flag::MaildirFlag; use pimalaya_cli::printer::Printer; use serde::Serialize; -use crate::maildir::client::MaildirClient; +use crate::account::context::Account; /// List available MAILDIR flags for the given mailbox. /// @@ -35,10 +35,10 @@ use crate::maildir::client::MaildirClient; pub struct MaildirFlagListCommand; impl MaildirFlagListCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, account: &mut Account) -> Result<()> { let table = FlagsTable { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), flags: vec![ FlagRow::new(MaildirFlag::Passed), FlagRow::new(MaildirFlag::Replied), diff --git a/src/maildir/flag/remove.rs b/src/maildir/flag/remove.rs index 4fce223f..3e2b567e 100644 --- a/src/maildir/flag/remove.rs +++ b/src/maildir/flag/remove.rs @@ -43,7 +43,7 @@ pub struct MaildirFlagRemoveCommand { } impl MaildirFlagRemoveCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let flags = MaildirFlags::from_iter(self.flags.into_iter().map(Into::into)); diff --git a/src/maildir/flag/set.rs b/src/maildir/flag/set.rs index 94125d1c..09f1bae7 100644 --- a/src/maildir/flag/set.rs +++ b/src/maildir/flag/set.rs @@ -43,7 +43,7 @@ pub struct MaildirFlagSetCommand { } impl MaildirFlagSetCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let flags = MaildirFlags::from_iter(self.flags.into_iter().map(Into::into)); diff --git a/src/maildir/list.rs b/src/maildir/list.rs index 4c7cc00b..4f618e1a 100644 --- a/src/maildir/list.rs +++ b/src/maildir/list.rs @@ -24,6 +24,7 @@ use io_maildir::maildir::Maildir; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::maildir::client::MaildirClient; /// List, search and filter maildirs. @@ -35,12 +36,17 @@ use crate::maildir::client::MaildirClient; pub struct MaildirMailboxListCommand; impl MaildirMailboxListCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut MaildirClient, + ) -> Result<()> { let maildirs = client.list_maildirs()?; let table = MaildirsTable { - preset: client.account.table_preset().to_string(), - name_color: client.account.mailboxes_list_table_name_color(), + preset: account.table_preset().to_string(), + name_color: account.mailboxes_list_table_name_color(), rows: maildirs.into_iter().map(From::from).collect(), }; diff --git a/src/maildir/message/cli.rs b/src/maildir/message/cli.rs index 9c807015..54e57389 100644 --- a/src/maildir/message/cli.rs +++ b/src/maildir/message/cli.rs @@ -44,7 +44,7 @@ pub enum MaildirMessageCommand { } impl MaildirMessageCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { match self { Self::Save(cmd) => cmd.execute(printer, client), Self::Get(cmd) => cmd.execute(printer, client), diff --git a/src/maildir/message/copy.rs b/src/maildir/message/copy.rs index 7daf5e9d..d6a6d8ff 100644 --- a/src/maildir/message/copy.rs +++ b/src/maildir/message/copy.rs @@ -43,7 +43,7 @@ pub struct MaildirMessageCopyCommand { } impl MaildirMessageCopyCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let source = client.resolve_maildir(&self.source.inner)?; let target = client.resolve_maildir(&self.target.inner)?; diff --git a/src/maildir/message/export.rs b/src/maildir/message/export.rs index df237a1c..4542dc8d 100644 --- a/src/maildir/message/export.rs +++ b/src/maildir/message/export.rs @@ -57,7 +57,7 @@ pub struct MaildirMessageExportCommand { } impl MaildirMessageExportCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let msg = client.get(maildir, &self.id.inner)?; diff --git a/src/maildir/message/get.rs b/src/maildir/message/get.rs index 9a2976aa..ffebc4dc 100644 --- a/src/maildir/message/get.rs +++ b/src/maildir/message/get.rs @@ -41,7 +41,7 @@ pub struct MaildirMessageGetCommand { } impl MaildirMessageGetCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let msg = client.get(maildir, &self.id.inner)?; diff --git a/src/maildir/message/move.rs b/src/maildir/message/move.rs index 46167c08..454a9b7c 100644 --- a/src/maildir/message/move.rs +++ b/src/maildir/message/move.rs @@ -43,7 +43,7 @@ pub struct MaildirMessageMoveCommand { } impl MaildirMessageMoveCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let source = client.resolve_maildir(&self.source.inner)?; let target = client.resolve_maildir(&self.target.inner)?; diff --git a/src/maildir/message/read.rs b/src/maildir/message/read.rs index 4fb6f5e2..8ec128c5 100644 --- a/src/maildir/message/read.rs +++ b/src/maildir/message/read.rs @@ -48,7 +48,7 @@ pub struct MaildirMessageReadCommand { } impl MaildirMessageReadCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let message = client.get(maildir, &self.id.inner)?; diff --git a/src/maildir/message/save.rs b/src/maildir/message/save.rs index b2baa470..218edc66 100644 --- a/src/maildir/message/save.rs +++ b/src/maildir/message/save.rs @@ -56,7 +56,7 @@ pub struct MaildirMessageSaveCommand { } impl MaildirMessageSaveCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let maildir = client.resolve_maildir(&self.maildir.inner)?; let msg = self.message.parse()?; let flags = MaildirFlags::from_iter(self.flags.into_iter().map(Into::into)); diff --git a/src/maildir/rename.rs b/src/maildir/rename.rs index 0805e7be..fee95c32 100644 --- a/src/maildir/rename.rs +++ b/src/maildir/rename.rs @@ -37,7 +37,7 @@ pub struct MaildirMailboxRenameCommand { } impl MaildirMailboxRenameCommand { - pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut MaildirClient) -> Result<()> { let path = client .root .join(&self.maildir_path.inner) diff --git a/src/shared/attachments/cli.rs b/src/shared/attachments/cli.rs index 82e25d78..ccc33e4f 100644 --- a/src/shared/attachments/cli.rs +++ b/src/shared/attachments/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ attachments::{download::AttachmentDownloadCommand, list::AttachmentListCommand}, client::EmailClient, @@ -36,10 +37,15 @@ pub enum AttachmentCommand { } impl AttachmentCommand { - pub fn execute(self, printer: &mut impl Printer, client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { match self { - Self::List(cmd) => cmd.execute(printer, client), - Self::Download(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), + Self::Download(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/shared/attachments/download.rs b/src/shared/attachments/download.rs index 3d7d0528..641ba822 100644 --- a/src/shared/attachments/download.rs +++ b/src/shared/attachments/download.rs @@ -26,6 +26,7 @@ use clap::Parser; use mail_parser::{MessageParser, MimeHeaders}; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ attachments::list::{Attachment, AttachmentColors, Attachments, mime_string}, client::EmailClient, @@ -66,18 +67,20 @@ pub struct AttachmentDownloadCommand { } impl AttachmentDownloadCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let mailbox = self.mailbox.resolve(&client.account)?; + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { + let mailbox = self.mailbox.resolve(account)?; let raw = client.get_message(&mailbox, &self.message_id)?; let Some(message) = MessageParser::new().parse(&raw) else { bail!("Failed to parse RFC 5322 message"); }; - let dir = self - .dir - .clone() - .unwrap_or_else(|| client.account.downloads_dir()); + let dir = self.dir.clone().unwrap_or_else(|| account.downloads_dir()); if !dir.exists() { fs::create_dir_all(&dir)?; @@ -126,17 +129,17 @@ impl AttachmentDownloadCommand { } let attachments = Attachments { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), with_inline: written.iter().any(|a| a.inline), with_path: true, colors: AttachmentColors { - id: client.account.attachments_list_table_id_color(), - filename: client.account.attachments_list_table_filename_color(), - r#type: client.account.attachments_list_table_type_color(), - size: client.account.attachments_list_table_size_color(), - inline: client.account.attachments_list_table_inline_color(), - path: client.account.attachments_list_table_path_color(), + id: account.attachments_list_table_id_color(), + filename: account.attachments_list_table_filename_color(), + r#type: account.attachments_list_table_type_color(), + size: account.attachments_list_table_size_color(), + inline: account.attachments_list_table_inline_color(), + path: account.attachments_list_table_path_color(), }, attachments: written, }; diff --git a/src/shared/attachments/list.rs b/src/shared/attachments/list.rs index 550ca9f3..b33e0507 100644 --- a/src/shared/attachments/list.rs +++ b/src/shared/attachments/list.rs @@ -25,6 +25,7 @@ use mail_parser::{MessageParser, MessagePart, MimeHeaders}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::shared::{client::EmailClient, mailboxes::arg::MailboxArg}; /// List the attachments carried by a single message in the active @@ -53,8 +54,13 @@ pub struct AttachmentListCommand { } impl AttachmentListCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let mailbox = self.mailbox.resolve(&client.account)?; + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { + let mailbox = self.mailbox.resolve(account)?; let raw = client.get_message(&mailbox, &self.message_id)?; let Some(message) = MessageParser::new().parse(&raw) else { @@ -83,17 +89,17 @@ impl AttachmentListCommand { } let attachments = Attachments { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), with_inline: self.inline, with_path: false, colors: AttachmentColors { - id: client.account.attachments_list_table_id_color(), - filename: client.account.attachments_list_table_filename_color(), - r#type: client.account.attachments_list_table_type_color(), - size: client.account.attachments_list_table_size_color(), - inline: client.account.attachments_list_table_inline_color(), - path: client.account.attachments_list_table_path_color(), + id: account.attachments_list_table_id_color(), + filename: account.attachments_list_table_filename_color(), + r#type: account.attachments_list_table_type_color(), + size: account.attachments_list_table_size_color(), + inline: account.attachments_list_table_inline_color(), + path: account.attachments_list_table_path_color(), }, attachments, }; diff --git a/src/shared/client.rs b/src/shared/client.rs index f90ed6cb..3b85e7f5 100644 --- a/src/shared/client.rs +++ b/src/shared/client.rs @@ -18,9 +18,11 @@ //! Cross-protocol [`EmailClient`] for the shared subcommands //! (`mailboxes`, `envelopes`, `flags`, `messages`, `attachments`). //! -//! Wraps [`io_email::client::EmailClientStd`] and bundles the active -//! [`Account`] (display, identity, composer/reader registries) the -//! shared commands need alongside the I/O client. Implements +//! Wraps [`io_email::client::EmailClientStd`]. The active +//! [`Account`] is threaded as a sibling argument through every +//! `execute` chain rather than being bundled into the client; this +//! keeps account access (`resolve_mailbox`, identity, etc.) borrow- +//! disjoint from `&mut EmailClient` calls. Implements //! [`Deref`]/[`DerefMut`] onto the inner client so callers can call //! its methods directly. //! @@ -45,7 +47,6 @@ use crate::{ pub struct EmailClient { inner: EmailClientStd, - pub account: Account, } impl EmailClient { @@ -53,7 +54,7 @@ impl EmailClient { config: Config, mut account_config: AccountConfig, backend: Backend, - ) -> Result { + ) -> Result<(Account, Self)> { let mut inner = EmailClientStd::new(); let mut configured = false; @@ -156,7 +157,7 @@ impl EmailClient { let account = Account::from(config).merge(Account::from(account_config)); - Ok(Self { inner, account }) + Ok((account, Self { inner })) } } diff --git a/src/shared/envelopes/cli.rs b/src/shared/envelopes/cli.rs index a1a29f60..ef90cea3 100644 --- a/src/shared/envelopes/cli.rs +++ b/src/shared/envelopes/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, envelopes::{list::EnvelopeListCommand, search::EnvelopeSearchCommand}, @@ -38,10 +39,15 @@ pub enum EnvelopeCommand { } impl EnvelopeCommand { - pub fn execute(self, printer: &mut impl Printer, client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { match self { - Self::List(cmd) => cmd.execute(printer, client), - Self::Search(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), + Self::Search(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/shared/envelopes/list.rs b/src/shared/envelopes/list.rs index 2eee3af9..ea17925d 100644 --- a/src/shared/envelopes/list.rs +++ b/src/shared/envelopes/list.rs @@ -26,6 +26,7 @@ use io_email::{address::Address, envelope::Envelope, flag::Flag}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::shared::{client::EmailClient, mailboxes::arg::MailboxArg}; /// List envelopes for the active account, regardless of the underlying @@ -75,39 +76,44 @@ pub struct EnvelopeListCommand { } impl EnvelopeListCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { let page = Some(self.page).filter(|p| *p > 0); let page_size = self .page_size - .or(Some(client.account.envelopes_list_page_size())) + .or(Some(account.envelopes_list_page_size())) .filter(|p| *p > 0); - let mailbox = self.mailbox.resolve(&client.account)?; + let mailbox = self.mailbox.resolve(account)?; let envelopes = client.list_envelopes(&mailbox, page, page_size, self.has_attachment)?; let envelopes = Envelopes { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), max_width: self.max_width, - datetime_fmt: client.account.datetime_fmt().to_string(), - datetime_local_tz: client.account.datetime_local_tz(), + datetime_fmt: account.datetime_fmt().to_string(), + datetime_local_tz: account.datetime_local_tz(), recipient: self.recipient, with_attachment: self.has_attachment, chars: FlagChars { - unseen: client.account.envelopes_list_table_unseen_char(), - replied: client.account.envelopes_list_table_replied_char(), - flagged: client.account.envelopes_list_table_flagged_char(), - attachment: client.account.envelopes_list_table_attachment_char(), + unseen: account.envelopes_list_table_unseen_char(), + replied: account.envelopes_list_table_replied_char(), + flagged: account.envelopes_list_table_flagged_char(), + attachment: account.envelopes_list_table_attachment_char(), }, colors: EnvelopeColors { - id: client.account.envelopes_list_table_id_color(), - flags: client.account.envelopes_list_table_flags_color(), - att: client.account.envelopes_list_table_att_color(), - subject: client.account.envelopes_list_table_subject_color(), - from: client.account.envelopes_list_table_from_color(), - to: client.account.envelopes_list_table_to_color(), - date: client.account.envelopes_list_table_date_color(), - size: client.account.envelopes_list_table_size_color(), + id: account.envelopes_list_table_id_color(), + flags: account.envelopes_list_table_flags_color(), + att: account.envelopes_list_table_att_color(), + subject: account.envelopes_list_table_subject_color(), + from: account.envelopes_list_table_from_color(), + to: account.envelopes_list_table_to_color(), + date: account.envelopes_list_table_date_color(), + size: account.envelopes_list_table_size_color(), }, envelopes, }; diff --git a/src/shared/envelopes/search.rs b/src/shared/envelopes/search.rs index 4e077bcf..eec15e69 100644 --- a/src/shared/envelopes/search.rs +++ b/src/shared/envelopes/search.rs @@ -23,6 +23,7 @@ use clap::Parser; use io_email::search::{error::Error as SearchQueryError, query::SearchEmailsQuery}; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, envelopes::list::{EnvelopeColors, Envelopes, FlagChars}, @@ -81,13 +82,18 @@ pub struct EnvelopeSearchCommand { } impl EnvelopeSearchCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { let page = Some(self.page).filter(|p| *p > 0); let page_size = self .page_size - .or(Some(client.account.envelopes_list_page_size())) + .or(Some(account.envelopes_list_page_size())) .filter(|p| *p > 0); - let mailbox = self.mailbox.resolve(&client.account)?; + let mailbox = self.mailbox.resolve(account)?; let query = parse_query(self.query.as_deref()); let envelopes = client.search_envelopes( @@ -99,28 +105,28 @@ impl EnvelopeSearchCommand { )?; let envelopes = Envelopes { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), max_width: self.max_width, - datetime_fmt: client.account.datetime_fmt().to_string(), - datetime_local_tz: client.account.datetime_local_tz(), + datetime_fmt: account.datetime_fmt().to_string(), + datetime_local_tz: account.datetime_local_tz(), recipient: self.recipient, with_attachment: self.has_attachment, chars: FlagChars { - unseen: client.account.envelopes_list_table_unseen_char(), - replied: client.account.envelopes_list_table_replied_char(), - flagged: client.account.envelopes_list_table_flagged_char(), - attachment: client.account.envelopes_list_table_attachment_char(), + unseen: account.envelopes_list_table_unseen_char(), + replied: account.envelopes_list_table_replied_char(), + flagged: account.envelopes_list_table_flagged_char(), + attachment: account.envelopes_list_table_attachment_char(), }, colors: EnvelopeColors { - id: client.account.envelopes_list_table_id_color(), - flags: client.account.envelopes_list_table_flags_color(), - att: client.account.envelopes_list_table_att_color(), - subject: client.account.envelopes_list_table_subject_color(), - from: client.account.envelopes_list_table_from_color(), - to: client.account.envelopes_list_table_to_color(), - date: client.account.envelopes_list_table_date_color(), - size: client.account.envelopes_list_table_size_color(), + id: account.envelopes_list_table_id_color(), + flags: account.envelopes_list_table_flags_color(), + att: account.envelopes_list_table_att_color(), + subject: account.envelopes_list_table_subject_color(), + from: account.envelopes_list_table_from_color(), + to: account.envelopes_list_table_to_color(), + date: account.envelopes_list_table_date_color(), + size: account.envelopes_list_table_size_color(), }, envelopes, }; diff --git a/src/shared/flags/add.rs b/src/shared/flags/add.rs index 46049b9a..7ff1d386 100644 --- a/src/shared/flags/add.rs +++ b/src/shared/flags/add.rs @@ -23,6 +23,7 @@ use io_email::flag::{Flag, FlagOp}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, flags::arg::{FlagsArg, MessageIdsArg}, @@ -41,8 +42,13 @@ pub struct FlagAddCommand { } impl FlagAddCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let mailbox = self.mailbox.resolve(&client.account)?; + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { + let mailbox = self.mailbox.resolve(account)?; let ids: Vec<&str> = self.message_ids.inner.iter().map(String::as_str).collect(); let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); diff --git a/src/shared/flags/cli.rs b/src/shared/flags/cli.rs index 428dd448..eb07398e 100644 --- a/src/shared/flags/cli.rs +++ b/src/shared/flags/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, flags::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand}, @@ -36,11 +37,16 @@ pub enum FlagCommand { } impl FlagCommand { - pub fn execute(self, printer: &mut impl Printer, client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { match self { - Self::Add(cmd) => cmd.execute(printer, client), - Self::Set(cmd) => cmd.execute(printer, client), - Self::Remove(cmd) => cmd.execute(printer, client), + Self::Add(cmd) => cmd.execute(printer, account, client), + Self::Set(cmd) => cmd.execute(printer, account, client), + Self::Remove(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/shared/flags/remove.rs b/src/shared/flags/remove.rs index f8d69a31..9449f3c8 100644 --- a/src/shared/flags/remove.rs +++ b/src/shared/flags/remove.rs @@ -23,6 +23,7 @@ use io_email::flag::{Flag, FlagOp}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, flags::arg::{FlagsArg, MessageIdsArg}, @@ -41,8 +42,13 @@ pub struct FlagRemoveCommand { } impl FlagRemoveCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let mailbox = self.mailbox.resolve(&client.account)?; + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { + let mailbox = self.mailbox.resolve(account)?; let ids: Vec<&str> = self.message_ids.inner.iter().map(String::as_str).collect(); let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); diff --git a/src/shared/flags/set.rs b/src/shared/flags/set.rs index 157b7676..3c64ccb6 100644 --- a/src/shared/flags/set.rs +++ b/src/shared/flags/set.rs @@ -23,6 +23,7 @@ use io_email::flag::{Flag, FlagOp}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, flags::arg::{FlagsArg, MessageIdsArg}, @@ -41,8 +42,13 @@ pub struct FlagSetCommand { } impl FlagSetCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let mailbox = self.mailbox.resolve(&client.account)?; + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { + let mailbox = self.mailbox.resolve(account)?; let ids: Vec<&str> = self.message_ids.inner.iter().map(String::as_str).collect(); let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); diff --git a/src/shared/mailboxes/cli.rs b/src/shared/mailboxes/cli.rs index 5b144a47..a0f1263e 100644 --- a/src/shared/mailboxes/cli.rs +++ b/src/shared/mailboxes/cli.rs @@ -19,6 +19,7 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{client::EmailClient, mailboxes::list::MailboxListCommand}; /// Shared API to manage mailboxes for the active account. @@ -31,9 +32,14 @@ pub enum MailboxCommand { } impl MailboxCommand { - pub fn execute(self, printer: &mut impl Printer, client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { match self { - Self::List(cmd) => cmd.execute(printer, client), + Self::List(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/shared/mailboxes/list.rs b/src/shared/mailboxes/list.rs index c601a7e9..7530ea85 100644 --- a/src/shared/mailboxes/list.rs +++ b/src/shared/mailboxes/list.rs @@ -24,6 +24,7 @@ use io_email::mailbox::Mailbox; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::shared::client::EmailClient; /// Shared API to list mailboxes for the active account. @@ -48,19 +49,24 @@ pub struct MailboxListCommand { } impl MailboxListCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { let mailboxes = client.list_mailboxes(self.counts)?; let mailboxes = Mailboxes { - preset: client.account.table_preset().to_string(), - arrangement: client.account.table_arrangement(), + preset: account.table_preset().to_string(), + arrangement: account.table_arrangement(), max_width: self.max_width, with_counts: self.counts, colors: MailboxColors { - id: client.account.mailboxes_list_table_id_color(), - name: client.account.mailboxes_list_table_name_color(), - total: client.account.mailboxes_list_table_total_color(), - unread: client.account.mailboxes_list_table_unread_color(), + id: account.mailboxes_list_table_id_color(), + name: account.mailboxes_list_table_name_color(), + total: account.mailboxes_list_table_total_color(), + unread: account.mailboxes_list_table_unread_color(), }, mailboxes, }; diff --git a/src/shared/messages/add.rs b/src/shared/messages/add.rs index a2d4aca2..1806369a 100644 --- a/src/shared/messages/add.rs +++ b/src/shared/messages/add.rs @@ -49,7 +49,7 @@ pub struct MessageAddCommand { } impl MessageAddCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { let raw = self.message.parse()?.into_bytes(); let flags: Vec = self.flag.iter().map(Into::into).collect(); let id = client.add_message(&self.mailbox, &flags, raw)?; diff --git a/src/shared/messages/arg.rs b/src/shared/messages/arg.rs index abe65930..7153cf9c 100644 --- a/src/shared/messages/arg.rs +++ b/src/shared/messages/arg.rs @@ -48,7 +48,7 @@ use pimalaya_cli::clap::parsers::path_parser; pub struct MessageArg { /// Can be a path to a file, raw message contents or nothing if /// piped via standard input. - #[arg(name = "message-raw", value_name = "MESSAGE", trailing_var_arg = true)] + #[arg(name = "message-raw", value_name = "MESSAGE", raw = true)] pub raw: Vec, } diff --git a/src/shared/messages/cli.rs b/src/shared/messages/cli.rs index abc98e66..865e72ff 100644 --- a/src/shared/messages/cli.rs +++ b/src/shared/messages/cli.rs @@ -19,15 +19,13 @@ use anyhow::Result; use clap::Subcommand; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, messages::{ - add::MessageAddCommand, compose::MessageComposeCommand, - compose_with::MessageComposeWithCommand, copy::MessageCopyCommand, - forward::MessageForwardCommand, forward_with::MessageForwardWithCommand, - mailto::MessageMailtoCommand, mv::MessageMoveCommand, read::MessageReadCommand, - read_with::MessageReadWithCommand, reply::MessageReplyCommand, - reply_with::MessageReplyWithCommand, send::MessageSendCommand, + add::MessageAddCommand, compose::MessageComposeCommand, copy::MessageCopyCommand, + forward::MessageForwardCommand, mv::MessageMoveCommand, read::MessageReadCommand, + reply::MessageReplyCommand, send::MessageSendCommand, }, }; @@ -35,48 +33,42 @@ use crate::shared::{ /// /// A message is composed of headers (key-value properties) and a body /// (suite of MIME parts). The built-in `compose` / `reply` / `forward` -/// / `read` subcommands cover simple cases via CLI flags. For -/// non-default workflows, the `-with` variants delegate composition -/// and rendering to a user-defined command from -/// `[message.composer.*]` / `[message.reader.*]`. +/// / `read` subcommands cover simple cases via CLI flags. Richer +/// composition is delegated to standalone tools (e.g. +/// [`mml`](https://github.com/pimalaya/mml)) wired up through shell +/// pipelines into `messages send` / `messages add`. #[derive(Debug, Subcommand)] pub enum MessageCommand { #[command(visible_alias = "save")] Add(MessageAddCommand), #[command(visible_alias = "write", alias = "new")] Compose(MessageComposeCommand), - #[command(visible_alias = "write-with")] - ComposeWith(MessageComposeWithCommand), #[command(visible_alias = "cp")] Copy(MessageCopyCommand), #[command(visible_alias = "fwd")] Forward(MessageForwardCommand), - ForwardWith(MessageForwardWithCommand), - Mailto(MessageMailtoCommand), #[command(visible_alias = "mv")] Move(MessageMoveCommand), Read(MessageReadCommand), - ReadWith(MessageReadWithCommand), Reply(MessageReplyCommand), - ReplyWith(MessageReplyWithCommand), Send(MessageSendCommand), } impl MessageCommand { - pub fn execute(self, printer: &mut impl Printer, client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { match self { Self::Add(cmd) => cmd.execute(printer, client), - Self::Compose(cmd) => cmd.execute(printer, client), - Self::ComposeWith(cmd) => cmd.execute(printer, client), + Self::Compose(cmd) => cmd.execute(printer, account, client), Self::Copy(cmd) => cmd.execute(printer, client), - Self::Forward(cmd) => cmd.execute(printer, client), - Self::ForwardWith(cmd) => cmd.execute(printer, client), - Self::Mailto(cmd) => cmd.execute(printer, client), + Self::Forward(cmd) => cmd.execute(printer, account, client), Self::Move(cmd) => cmd.execute(printer, client), Self::Read(cmd) => cmd.execute(printer, client), - Self::ReadWith(cmd) => cmd.execute(printer, client), - Self::Reply(cmd) => cmd.execute(printer, client), - Self::ReplyWith(cmd) => cmd.execute(printer, client), + Self::Reply(cmd) => cmd.execute(printer, account, client), Self::Send(cmd) => cmd.execute(printer, client), } } diff --git a/src/shared/messages/compose.rs b/src/shared/messages/compose.rs index 7a4d72a4..15afc52a 100644 --- a/src/shared/messages/compose.rs +++ b/src/shared/messages/compose.rs @@ -21,6 +21,7 @@ use anyhow::Result; use clap::Parser; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, messages::{ @@ -36,8 +37,10 @@ use crate::shared::{ /// produced RFC 5322 bytes are written to stdout by default; pass /// `--save ` to append a copy, `--send` to push through the /// account's SMTP/JMAP send path, or both. For richer composition -/// (multipart MIME, MML directives, signing/encryption, TUI editing, -/// …) use `compose-with ` instead. +/// (multipart MIME, MML directives, signing/encryption, editor-driven +/// workflows), chain a standalone composer like +/// [`mml`](https://github.com/pimalaya/mml) into `messages send` / +/// `messages add` via a tempfile or bash/zsh process substitution. #[derive(Debug, Parser)] pub struct MessageComposeCommand { /// Sender address (`From` header). @@ -99,7 +102,12 @@ pub struct MessageComposeCommand { } impl MessageComposeCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { let raw = builder::build( BuilderArgs { from: self.from.as_deref(), @@ -116,6 +124,13 @@ impl MessageComposeCommand { None, )?; - output::route(printer, &mut client, raw, self.save.as_deref(), self.send) + output::route( + printer, + account, + client, + raw, + self.save.as_deref(), + self.send, + ) } } diff --git a/src/shared/messages/compose_with.rs b/src/shared/messages/compose_with.rs deleted file mode 100644 index a4767235..00000000 --- a/src/shared/messages/compose_with.rs +++ /dev/null @@ -1,76 +0,0 @@ -// This file is part of Himalaya, a CLI to manage emails. -// -// Copyright (C) 2022-2026 soywod -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use anyhow::{Result, bail}; -use clap::Parser; -use pimalaya_cli::printer::Printer; -use pimalaya_config::command::shell; - -use crate::shared::{ - client::EmailClient, - messages::{output, runner}, -}; - -/// Compose a new message by delegating to a user-defined composer. -/// -/// Looks `` up in `[message.composer.]` and runs its -/// `command` via `sh -c`. With no ``, falls back to the entry -/// flagged `default = true`. The escape hatch `--command ""` -/// lets you run an ad-hoc command without editing the config. -/// -/// The composer takes the terminal: stdin is left empty (new -/// message — no source), stderr is inherited (composer prompts/ -/// errors). The composer's stdout must be a valid RFC 5322 message, -/// which himalaya then routes through `--save` / `--send`, or to -/// stdout if neither is set. -#[derive(Debug, Parser)] -pub struct MessageComposeWithCommand { - /// Name of an entry in `[message.composer.*]`. Optional — when - /// omitted, the composer flagged `default = true` is used. - #[arg(value_name = "NAME", conflicts_with = "command")] - pub name: Option, - - /// Ad-hoc shell command, mutually exclusive with ``. - /// Useful for trying the feature before editing the config. - #[arg(long, value_name = "SHELL")] - pub command: Option, - - #[arg(long, value_name = "MAILBOX")] - pub save: Option, - - #[arg(long)] - pub send: bool, -} - -impl MessageComposeWithCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let mut command = self.command.map(|cmd| shell(&cmd)); - let command = command.as_mut().unwrap_or( - &mut client - .account - .get_composer_mut(self.name.as_deref())? - .compose, - ); - - let raw = runner::run(command, &[])?; - if raw.is_empty() { - bail!("composer `{command:?}` produced no output"); - } - - output::route(printer, &mut client, raw, self.save.as_deref(), self.send) - } -} diff --git a/src/shared/messages/copy.rs b/src/shared/messages/copy.rs index 64f82e85..6a3e0725 100644 --- a/src/shared/messages/copy.rs +++ b/src/shared/messages/copy.rs @@ -49,7 +49,7 @@ pub struct MessageCopyCommand { } impl MessageCopyCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect(); client.copy_messages(&self.from, &self.to, &ids)?; printer.out(Message::new("Message(s) successfully copied")) diff --git a/src/shared/messages/forward.rs b/src/shared/messages/forward.rs index d62f5aa4..3ab29595 100644 --- a/src/shared/messages/forward.rs +++ b/src/shared/messages/forward.rs @@ -21,6 +21,7 @@ use anyhow::Result; use clap::Parser; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, messages::{ @@ -34,7 +35,9 @@ use crate::shared::{ /// Fetches the source, pre-fills `Fwd:` on the subject and the /// `References` header, and quotes the source body. The produced /// MIME is written to stdout, or routed via `--save` / `--send`. -/// For non-default composition, use `forward-with `. +/// For richer composition, pipe `messages read ` into a +/// standalone composer (`mml forward`, etc.) and feed its output +/// back into `messages send` / `messages add`. #[derive(Debug, Parser)] pub struct MessageForwardCommand { #[arg(value_name = "ID")] @@ -101,7 +104,12 @@ pub struct MessageForwardCommand { } impl MessageForwardCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { let source = client.get_message(&self.mailbox, &self.id)?; let raw = builder::build( @@ -125,6 +133,13 @@ impl MessageForwardCommand { }), )?; - output::route(printer, &mut client, raw, self.save.as_deref(), self.send) + output::route( + printer, + account, + client, + raw, + self.save.as_deref(), + self.send, + ) } } diff --git a/src/shared/messages/forward_with.rs b/src/shared/messages/forward_with.rs deleted file mode 100644 index 721ce410..00000000 --- a/src/shared/messages/forward_with.rs +++ /dev/null @@ -1,78 +0,0 @@ -// This file is part of Himalaya, a CLI to manage emails. -// -// Copyright (C) 2022-2026 soywod -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use anyhow::{Result, bail}; -use clap::Parser; -use pimalaya_cli::printer::Printer; -use pimalaya_config::command::shell; - -use crate::shared::{ - client::EmailClient, - messages::{output, runner}, -}; - -/// Forward a message by delegating to a user-defined composer. -/// -/// Same shape as `reply-with`: fetches the source, pipes it on -/// stdin to the named (or default) composer, captures stdout as the -/// MIME draft. -#[derive(Debug, Parser)] -pub struct MessageForwardWithCommand { - #[arg(value_name = "ID")] - pub id: String, - - #[arg( - long = "mailbox", - short = 'm', - value_name = "NAME", - default_value = "Inbox" - )] - pub mailbox: String, - - #[arg(value_name = "NAME", conflicts_with = "command")] - pub name: Option, - - #[arg(long, value_name = "SHELL")] - pub command: Option, - - #[arg(long, value_name = "MAILBOX")] - pub save: Option, - - #[arg(long)] - pub send: bool, -} - -impl MessageForwardWithCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let source = client.get_message(&self.mailbox, &self.id)?; - - let mut command = self.command.map(|cmd| shell(&cmd)); - let command = command.as_mut().unwrap_or( - &mut client - .account - .get_composer_mut(self.name.as_deref())? - .forward, - ); - - let raw = runner::run(command, &source)?; - if raw.is_empty() { - bail!("composer `{command:?}` produced no output"); - } - - output::route(printer, &mut client, raw, self.save.as_deref(), self.send) - } -} diff --git a/src/shared/messages/mailto.rs b/src/shared/messages/mailto.rs deleted file mode 100644 index a0583eca..00000000 --- a/src/shared/messages/mailto.rs +++ /dev/null @@ -1,246 +0,0 @@ -// This file is part of Himalaya, a CLI to manage emails. -// -// Copyright (C) 2022-2026 soywod -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use anyhow::{Result, anyhow, bail}; -use clap::Parser; -use percent_encoding::percent_decode_str; -use pimalaya_cli::printer::Printer; -use pimalaya_config::command::shell; -use url::Url; - -use crate::shared::{ - client::EmailClient, - messages::{ - builder::{self, BuilderArgs}, - output, runner, - }, -}; - -/// Compose a new message from a `mailto:` URI, opening a user-defined -/// composer with the URI's recipient and headers prefilled. -/// -/// The URI is parsed per RFC 6068: the path is the comma-separated -/// `To:` list; supported query params are `to`, `cc`, `bcc`, `subject` -/// and `body` (any other param is ignored). The parsed fields are -/// folded into a draft RFC 5322 skeleton via the built-in MIME -/// assembler, then piped on stdin to the composer (same contract as -/// `messages compose-with` / `reply-with` / `forward-with`). The -/// composer's stdout is routed through `--save` / `--send`, or to -/// stdout if neither is set. -#[derive(Debug, Parser)] -pub struct MessageMailtoCommand { - /// `mailto:` URI as defined by RFC 6068. - #[arg(value_name = "URI")] - pub uri: String, - - /// Name of an entry in `[message.composer.*]`. Optional: when - /// omitted, the composer flagged `default = true` is used. - #[arg(value_name = "NAME", conflicts_with = "command")] - pub name: Option, - - /// Ad-hoc shell command, mutually exclusive with ``. - #[arg(long, value_name = "SHELL")] - pub command: Option, - - /// Save the produced message to the given mailbox. - #[arg(long, value_name = "MAILBOX")] - pub save: Option, - - /// Submit the produced message through the account's send backend. - #[arg(long)] - pub send: bool, -} - -impl MessageMailtoCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let fields = parse_mailto_uri(&self.uri)?; - - let to: Vec = fields.to; - let cc: Vec = fields.cc; - let bcc: Vec = fields.bcc; - - let draft = builder::build( - BuilderArgs { - from: None, - to: &to, - cc: &cc, - bcc: &bcc, - subject: fields.subject.as_deref(), - body: fields.body.as_deref(), - body_file: None, - attach: &[], - signature: None, - signature_file: None, - }, - None, - )?; - - let mut command = self.command.map(|cmd| shell(&cmd)); - let command = command.as_mut().unwrap_or( - &mut client - .account - .get_composer_mut(self.name.as_deref())? - .compose, - ); - - let raw = runner::run(command, &draft)?; - if raw.is_empty() { - bail!("composer `{command:?}` produced no output"); - } - - output::route(printer, &mut client, raw, self.save.as_deref(), self.send) - } -} - -/// Fields extracted from a `mailto:` URI. Unrecognised query params -/// are silently ignored. -struct MailtoFields { - to: Vec, - cc: Vec, - bcc: Vec, - subject: Option, - body: Option, -} - -/// Parses a `mailto:` URI per RFC 6068. -/// -/// The path carries one or more comma-separated recipient addresses -/// (percent-decoded). The query string carries the headers `to`, `cc`, -/// `bcc`, `subject`, and `body`; addresses in `to` / `cc` / `bcc` may -/// themselves be comma-separated. Any other parameter is dropped. -fn parse_mailto_uri(uri: &str) -> Result { - let url = Url::parse(uri).map_err(|err| anyhow!("invalid mailto URI `{uri}`: {err}"))?; - if url.scheme() != "mailto" { - bail!("expected `mailto:` URI, got scheme `{}`", url.scheme()); - } - - let mut to = split_addresses(url.path()); - let mut cc = Vec::new(); - let mut bcc = Vec::new(); - let mut subject = None; - let mut body = None; - - for (key, value) in url.query_pairs() { - match key.as_ref().to_ascii_lowercase().as_str() { - "to" => to.extend(split_addresses(value.as_ref())), - "cc" => cc.extend(split_addresses(value.as_ref())), - "bcc" => bcc.extend(split_addresses(value.as_ref())), - "subject" => subject = Some(value.into_owned()), - "body" => body = Some(value.into_owned()), - _ => {} - } - } - - Ok(MailtoFields { - to, - cc, - bcc, - subject, - body, - }) -} - -/// Splits a comma-separated address list, percent-decodes each entry, -/// and drops the empties. Used both for the URI path and for the `to` -/// / `cc` / `bcc` query params (query values already come decoded -/// from `query_pairs`, but the comma split applies to both shapes). -fn split_addresses(raw: &str) -> Vec { - raw.split(',') - .filter_map(|part| { - let decoded = percent_decode_str(part).decode_utf8_lossy().into_owned(); - let trimmed = decoded.trim().to_owned(); - if trimmed.is_empty() { - None - } else { - Some(trimmed) - } - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn single_recipient() { - let f = parse_mailto_uri("mailto:bob@example.org").unwrap(); - assert_eq!(f.to, vec!["bob@example.org"]); - assert!(f.cc.is_empty()); - assert!(f.bcc.is_empty()); - assert!(f.subject.is_none()); - assert!(f.body.is_none()); - } - - #[test] - fn comma_separated_path() { - let f = parse_mailto_uri("mailto:a@x.org,b@x.org").unwrap(); - assert_eq!(f.to, vec!["a@x.org", "b@x.org"]); - } - - #[test] - fn percent_decoded_path() { - let f = parse_mailto_uri("mailto:bob%40example.org").unwrap(); - assert_eq!(f.to, vec!["bob@example.org"]); - } - - #[test] - fn subject_and_body_via_query() { - let f = parse_mailto_uri( - "mailto:bob@example.org?subject=Hello%20World&body=Hi%20Bob%2C%0AHow%20are%20you%3F", - ) - .unwrap(); - assert_eq!(f.subject.as_deref(), Some("Hello World")); - assert_eq!(f.body.as_deref(), Some("Hi Bob,\nHow are you?")); - } - - #[test] - fn cc_and_bcc_lists() { - let f = parse_mailto_uri( - "mailto:bob@example.org?cc=carol@example.org,dave@example.org&bcc=eve@example.org", - ) - .unwrap(); - assert_eq!(f.cc, vec!["carol@example.org", "dave@example.org"]); - assert_eq!(f.bcc, vec!["eve@example.org"]); - } - - #[test] - fn empty_path_with_to_param() { - let f = parse_mailto_uri("mailto:?to=bob@example.org&subject=Hi").unwrap(); - assert_eq!(f.to, vec!["bob@example.org"]); - assert_eq!(f.subject.as_deref(), Some("Hi")); - } - - #[test] - fn case_insensitive_query_keys() { - let f = parse_mailto_uri("mailto:bob@example.org?Subject=Hi&BODY=Yo").unwrap(); - assert_eq!(f.subject.as_deref(), Some("Hi")); - assert_eq!(f.body.as_deref(), Some("Yo")); - } - - #[test] - fn unknown_params_are_ignored() { - let f = parse_mailto_uri("mailto:bob@example.org?foo=bar&subject=Hi").unwrap(); - assert_eq!(f.subject.as_deref(), Some("Hi")); - assert!(f.body.is_none()); - } - - #[test] - fn non_mailto_scheme_is_rejected() { - assert!(parse_mailto_uri("https://example.org").is_err()); - } -} diff --git a/src/shared/messages/mod.rs b/src/shared/messages/mod.rs index 1f6c725e..24e8b600 100644 --- a/src/shared/messages/mod.rs +++ b/src/shared/messages/mod.rs @@ -20,16 +20,10 @@ pub mod arg; pub mod builder; pub mod cli; pub mod compose; -pub mod compose_with; pub mod copy; pub mod forward; -pub mod forward_with; -pub mod mailto; pub mod mv; pub mod output; pub mod read; -pub mod read_with; pub mod reply; -pub mod reply_with; -pub mod runner; pub mod send; diff --git a/src/shared/messages/mv.rs b/src/shared/messages/mv.rs index 60626368..d6aa71e1 100644 --- a/src/shared/messages/mv.rs +++ b/src/shared/messages/mv.rs @@ -49,7 +49,7 @@ pub struct MessageMoveCommand { } impl MessageMoveCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect(); client.move_messages(&self.from, &self.to, &ids)?; printer.out(Message::new("Message(s) successfully moved")) diff --git a/src/shared/messages/output.rs b/src/shared/messages/output.rs index 3a90fc13..510ed524 100644 --- a/src/shared/messages/output.rs +++ b/src/shared/messages/output.rs @@ -15,27 +15,35 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Post-composer routing: where the produced MIME bytes go. +//! Post-build routing: where the produced MIME bytes go. //! -//! Used by `compose` / `reply` / `forward` (and their `-with` -//! variants). The same `--save ` / `--send` flags can combine: -//! `--save Sent --send` sends the message *and* appends a copy to the -//! `Sent` mailbox. With neither flag, the raw bytes are written to -//! stdout — same shape as a manual `mml compile > out.eml`. +//! Used by the built-in flag composers `compose` / `reply` / +//! `forward`. The same `--save ` / `--send` flags can combine: +//! `--save Sent --send` sends the message and appends a copy to the +//! `Sent` mailbox. The mailbox name is resolved through +//! [`Account::resolve_mailbox`] before the backend call so user +//! aliases (`mailbox.alias.sent = "[Gmail]/Sent Mail"`) apply. With +//! neither flag, the raw bytes are written to stdout: same shape as +//! a manual `mml compile > out.eml`. +//! +//! [`Account::resolve_mailbox`]: crate::account::context::Account::resolve_mailbox use std::io::{Write, stdout}; use anyhow::Result; +use io_email::flag::{Flag, IanaFlag}; use pimalaya_cli::printer::{Message, Printer}; -use crate::shared::client::EmailClient; +use crate::{account::context::Account, shared::client::EmailClient}; /// Routes `raw` through the requested combination of side-effects. -/// `save` writes a copy to the named mailbox before sending; `send` -/// pushes the message through the configured SMTP / JMAP send path. -/// With neither set, dumps `raw` to stdout and returns. +/// `save` writes a copy to the named mailbox (resolved through the +/// account's alias map) before sending; `send` pushes the message +/// through the configured SMTP / JMAP send path. With neither set, +/// dumps `raw` to stdout and returns. pub fn route( printer: &mut impl Printer, + account: &Account, client: &mut EmailClient, raw: Vec, save: Option<&str>, @@ -47,14 +55,21 @@ pub fn route( return Ok(()); } - if let Some(mailbox) = save { - client.add_message(mailbox, &[], raw.clone())?; + if let Some(name) = save { + let mailbox = account.resolve_mailbox(name); + client.add_message(mailbox, &[Flag::from_iana(IanaFlag::Seen)], raw.clone())?; } if send { client.send_message(raw)?; - return printer.out(Message::new("Message successfully sent")); } - printer.out(Message::new("Message saved")) + let msg = match (save.is_some(), send) { + (true, true) => "Message successfully saved and sent", + (false, true) => "Message successfully saved", + (true, false) => "Message successfully sent", + (false, false) => "Nothing done with this message", + }; + + printer.out(Message::new(msg)) } diff --git a/src/shared/messages/read.rs b/src/shared/messages/read.rs index 1a8a641a..ea6e5a88 100644 --- a/src/shared/messages/read.rs +++ b/src/shared/messages/read.rs @@ -33,8 +33,8 @@ use crate::shared::client::EmailClient; /// Fetches the message and renders headers + text bodies. Pass /// `--raw` to dump the original RFC 5322 bytes to stdout instead, /// or `--json` to emit the parsed message as JSON. For a custom -/// pretty-printer (mml interpret, w3m, your own viewer, …) use -/// `read-with `. +/// pretty-printer (`mml interpret`, w3m, your own viewer), pipe the +/// `--raw` output into the renderer of your choice. #[derive(Debug, Parser)] pub struct MessageReadCommand { /// Identifier of the message (IMAP UID, JMAP email id, or Maildir @@ -59,7 +59,7 @@ pub struct MessageReadCommand { } impl MessageReadCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { if self.raw && printer.is_json() { bail!("`--raw` and `--json` cannot be combined"); } diff --git a/src/shared/messages/read_with.rs b/src/shared/messages/read_with.rs deleted file mode 100644 index 621463ad..00000000 --- a/src/shared/messages/read_with.rs +++ /dev/null @@ -1,76 +0,0 @@ -// This file is part of Himalaya, a CLI to manage emails. -// -// Copyright (C) 2022-2026 soywod -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use std::io::{Write, stdout}; - -use anyhow::Result; -use clap::Parser; -use pimalaya_cli::printer::Printer; -use pimalaya_config::command::shell; - -use crate::shared::{client::EmailClient, messages::runner}; - -/// Read a message by delegating to a user-defined reader. -/// -/// Fetches the source and pipes it on stdin to the named (or -/// default) reader. The reader's stdout is forwarded to the -/// terminal — zero bytes is fine (the reader may have spawned its -/// own UI), non-empty bytes are written as-is. -#[derive(Debug, Parser)] -pub struct MessageReadWithCommand { - /// Identifier of the message. - #[arg(value_name = "ID")] - pub id: String, - - /// Mailbox the message lives in. Ignored for JMAP. - #[arg( - long = "mailbox", - short = 'm', - value_name = "NAME", - default_value = "Inbox" - )] - pub mailbox: String, - - /// Name of an entry in `[message.reader.*]`. Optional — when - /// omitted, the reader flagged `default = true` is used. - #[arg(value_name = "NAME", conflicts_with = "command")] - pub name: Option, - - /// Ad-hoc shell command, mutually exclusive with ``. - #[arg(long, value_name = "SHELL")] - pub command: Option, -} - -impl MessageReadWithCommand { - pub fn execute(self, _printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let source = client.get_message(&self.mailbox, &self.id)?; - - let mut command = self.command.map(|cmd| shell(&cmd)); - let command = command - .as_mut() - .unwrap_or(&mut client.account.get_reader_mut(self.name.as_deref())?.command); - - let bytes = runner::run(command, &source)?; - - if !bytes.is_empty() { - let mut out = stdout().lock(); - out.write_all(&bytes)?; - } - - Ok(()) - } -} diff --git a/src/shared/messages/reply.rs b/src/shared/messages/reply.rs index 64def89c..755bc95f 100644 --- a/src/shared/messages/reply.rs +++ b/src/shared/messages/reply.rs @@ -21,6 +21,7 @@ use anyhow::Result; use clap::Parser; use pimalaya_cli::printer::Printer; +use crate::account::context::Account; use crate::shared::{ client::EmailClient, messages::{ @@ -35,7 +36,9 @@ use crate::shared::{ /// and the `Re:` subject, optionally derives recipients from /// `Reply-To`/`From`, and quotes the source text body. The produced /// MIME is written to stdout, or routed via `--save` / `--send`. -/// For non-default composition, use `reply-with `. +/// For richer composition, pipe `messages read ` into a +/// standalone composer (`mml reply`, etc.) and feed its output back +/// into `messages send` / `messages add`. #[derive(Debug, Parser)] pub struct MessageReplyCommand { /// Identifier of the source message (IMAP UID, JMAP id, Maildir @@ -112,7 +115,12 @@ pub struct MessageReplyCommand { } impl MessageReplyCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { let source = client.get_message(&self.mailbox, &self.id)?; let raw = builder::build( @@ -136,6 +144,13 @@ impl MessageReplyCommand { }), )?; - output::route(printer, &mut client, raw, self.save.as_deref(), self.send) + output::route( + printer, + account, + client, + raw, + self.save.as_deref(), + self.send, + ) } } diff --git a/src/shared/messages/reply_with.rs b/src/shared/messages/reply_with.rs deleted file mode 100644 index b4c9b1bb..00000000 --- a/src/shared/messages/reply_with.rs +++ /dev/null @@ -1,80 +0,0 @@ -// This file is part of Himalaya, a CLI to manage emails. -// -// Copyright (C) 2022-2026 soywod -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use anyhow::{Result, bail}; -use clap::Parser; -use pimalaya_cli::printer::Printer; -use pimalaya_config::command::shell; - -use crate::shared::{ - client::EmailClient, - messages::{output, runner}, -}; - -/// Reply to a message by delegating to a user-defined composer. -/// -/// Fetches the source message, then runs the named (or default) -/// composer with the source MIME piped on stdin. The composer must -/// consume stdin first if it wants user interaction — TUI composers -/// can re-open `/dev/tty` once stdin is drained (vim/less/fzf all do -/// this). The produced MIME is routed through `--save` / `--send`, -/// or stdout if neither is set. -#[derive(Debug, Parser)] -pub struct MessageReplyWithCommand { - /// Identifier of the source message. - #[arg(value_name = "ID")] - pub id: String, - - /// Mailbox the source message lives in. Ignored for JMAP. - #[arg( - long = "mailbox", - short = 'm', - value_name = "NAME", - default_value = "Inbox" - )] - pub mailbox: String, - - #[arg(value_name = "NAME", conflicts_with = "command")] - pub name: Option, - - #[arg(long, value_name = "SHELL")] - pub command: Option, - - #[arg(long, value_name = "MAILBOX")] - pub save: Option, - - #[arg(long)] - pub send: bool, -} - -impl MessageReplyWithCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { - let source = client.get_message(&self.mailbox, &self.id)?; - - let mut command = self.command.map(|cmd| shell(&cmd)); - let command = command - .as_mut() - .unwrap_or(&mut client.account.get_composer_mut(self.name.as_deref())?.reply); - - let raw = runner::run(command, &source)?; - if raw.is_empty() { - bail!("composer `{command:?}` produced no output"); - } - - output::route(printer, &mut client, raw, self.save.as_deref(), self.send) - } -} diff --git a/src/shared/messages/runner.rs b/src/shared/messages/runner.rs deleted file mode 100644 index 4a15360f..00000000 --- a/src/shared/messages/runner.rs +++ /dev/null @@ -1,69 +0,0 @@ -// This file is part of Himalaya, a CLI to manage emails. -// -// Copyright (C) 2022-2026 soywod -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! Spawns user-defined composer and reader commands. -//! -//! A composer/reader is just a shell command line invoked via -//! `sh -c`. Himalaya pipes source MIME bytes (empty for new messages) -//! into the child's stdin, captures stdout (the produced MIME draft -//! or the interpreted text), and inherits stderr so the spawned -//! command can prompt the user or print errors directly to the -//! terminal. TUI composers that need interactive input can re-open -//! `/dev/tty` once they've consumed stdin — standard Unix practice. - -use std::{ - io::Write, - process::{Command, Stdio}, -}; - -use anyhow::{Result, anyhow, bail}; - -/// Spawns `command`, writes `stdin_bytes` to its -/// stdin, and returns the captured stdout bytes. -/// Stderr is inherited. -/// Bails on a non-zero exit status. -pub fn run(command: &mut Command, stdin_bytes: &[u8]) -> Result> { - let mut child = command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .spawn() - .map_err(|err| anyhow!("spawn `{command:?}`: {err}"))?; - - if let Some(mut stdin) = child.stdin.take() { - stdin - .write_all(stdin_bytes) - .map_err(|err| anyhow!("write stdin to `{command:?}`: {err}"))?; - } - - let output = child - .wait_with_output() - .map_err(|err| anyhow!("wait `{command:?}`: {err}"))?; - - if !output.status.success() { - bail!( - "`{command:?}` exited with status {}", - output - .status - .code() - .map(|c| c.to_string()) - .unwrap_or_else(|| "?".to_string()) - ); - } - - Ok(output.stdout) -} diff --git a/src/shared/messages/send.rs b/src/shared/messages/send.rs index 116fc45a..df6cd5ae 100644 --- a/src/shared/messages/send.rs +++ b/src/shared/messages/send.rs @@ -37,7 +37,7 @@ pub struct MessageSendCommand { } impl MessageSendCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { let raw = self.message.parse()?.into_bytes(); client.send_message(raw)?; printer.out(Message::new("Message successfully sent")) diff --git a/src/smtp/cli.rs b/src/smtp/cli.rs index d47abf12..75437ccc 100644 --- a/src/smtp/cli.rs +++ b/src/smtp/cli.rs @@ -35,7 +35,7 @@ pub enum SmtpCommand { } impl SmtpCommand { - pub fn execute(self, printer: &mut impl Printer, client: SmtpClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut SmtpClient) -> Result<()> { match self { Self::Messages(cmd) => cmd.execute(printer, client), } diff --git a/src/smtp/client.rs b/src/smtp/client.rs index 351b7d68..0a26e2fa 100644 --- a/src/smtp/client.rs +++ b/src/smtp/client.rs @@ -15,12 +15,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Himalaya wrapper around [`io_smtp::client::SmtpClientStd`] that -//! bundles the merged [`Account`] alongside the live SMTP client. +//! Himalaya wrapper around [`io_smtp::client::SmtpClientStd`]. //! //! Built up front by the dispatch layer (`crate::cli`) via //! [`build_smtp_client`] and handed down to every SMTP-specific -//! subcommand. SMTP send is stateless after auth, so no session +//! subcommand, together with the merged [`Account`] as a sibling +//! argument. SMTP send is stateless after auth, so no session //! context needs to follow the stream. use std::{ @@ -39,21 +39,19 @@ use crate::{account::context::Account, cli::load_or_wizard, config::SmtpConfig}; pub struct SmtpClient { inner: Inner, - #[allow(dead_code)] - pub account: Account, } impl SmtpClient { /// Opens the SMTP connection (TCP/TLS/STARTTLS, greeting, EHLO, - /// SASL) then wraps the resulting client alongside `account`. - pub fn new(config: SmtpConfig, account: Account) -> Result { + /// SASL). + pub fn new(config: SmtpConfig) -> Result { let mut tls: Tls = config.tls.into(); tls.rustls.alpn = vec!["smtp".into()]; let sasl: Option = config.sasl.map(Sasl::try_from).transpose()?; let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); let server = parse_smtp_server(&config.server)?; let inner = Inner::connect(&server, &tls, config.starttls, domain, sasl)?; - Ok(Self { inner, account }) + Ok(Self { inner }) } } @@ -89,11 +87,13 @@ impl DerefMut for SmtpClient { /// Loads the configuration, picks the active account, builds the /// merged [`Account`] then opens the SMTP session. Bails when the -/// account has no `[smtp]` block. +/// account has no `[smtp]` block. Returns the live client paired +/// with the merged account so subcommands receive both as sibling +/// arguments. pub fn build_smtp_client( config_paths: &[PathBuf], account_name: Option<&str>, -) -> Result { +) -> Result<(Account, SmtpClient)> { let mut config = load_or_wizard(config_paths)?; let (name, mut ac) = config .take_account(account_name)? @@ -103,5 +103,6 @@ pub fn build_smtp_client( .take() .ok_or_else(|| anyhow!("SMTP config is missing for account `{name}`"))?; let account = Account::from(config).merge(Account::from(ac)); - SmtpClient::new(smtp_config, account) + let client = SmtpClient::new(smtp_config)?; + Ok((account, client)) } diff --git a/src/smtp/message/cli.rs b/src/smtp/message/cli.rs index 1b71b1d7..78c5c317 100644 --- a/src/smtp/message/cli.rs +++ b/src/smtp/message/cli.rs @@ -32,7 +32,7 @@ pub enum SmtpMessageCommand { } impl SmtpMessageCommand { - pub fn execute(self, printer: &mut impl Printer, client: SmtpClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut SmtpClient) -> Result<()> { match self { Self::Send(cmd) => cmd.execute(printer, client), } diff --git a/src/smtp/message/send.rs b/src/smtp/message/send.rs index fa248b93..73b70d47 100644 --- a/src/smtp/message/send.rs +++ b/src/smtp/message/send.rs @@ -41,7 +41,7 @@ pub struct SmtpMessageSendCommand { } impl SmtpMessageSendCommand { - pub fn execute(self, printer: &mut impl Printer, mut client: SmtpClient) -> Result<()> { + pub fn execute(self, printer: &mut impl Printer, client: &mut SmtpClient) -> Result<()> { let message = self.message.parse()?; let (reverse_path, forward_paths) = into_smtp_msg(message.as_bytes())?; client.send(reverse_path, forward_paths, message.into_bytes())?; diff --git a/src/wizard/discover.rs b/src/wizard/discover.rs index 34fdf4d3..24b246c2 100644 --- a/src/wizard/discover.rs +++ b/src/wizard/discover.rs @@ -116,7 +116,6 @@ pub fn run_or_exit(target: &Path) -> Result { table: Default::default(), envelope: Default::default(), mailbox: Default::default(), - message: Default::default(), attachment: Default::default(), account: Default::default(), accounts: HashMap::from([(account_name, account)]),