diff --git a/CHANGELOG.md b/CHANGELOG.md index 58777fdc..6cb84492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Brought the `m2dir` backend to feature parity with `maildir` at the CLI level: `m2dir messages {save, get, read, export}`, `m2dir flags {list, add, set, remove}`, `m2dir envelopes {get, list}`. Flags are free-form UTF-8 strings persisted in the `.meta/.flags` metadata file. Still missing relative to `maildir`: mailbox `rename`, message `copy` and `move` (need io-m2dir lib support first). +- Added `--save ` to `messages send`, mirroring the existing flag on `messages compose` / `reply` / `forward`. Sends the message and appends a copy of it to the named mailbox. The mailbox name is resolved through the account's `[mailbox.alias]` map. + +- Added `--send` to `messages add` (alias `messages save`), mirroring `messages send --save`. Appends the message to the mandatory `--mailbox` first and then pushes it through the account's send path. Success line now reads "Message {id} successfully added and sent" when `--send` is set. + ### Changed - 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). @@ -25,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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"`. +- Extended mailbox-alias resolution across every shared `messages` subcommand that takes a mailbox name: `add -m`, `read -m`, `reply -m`, `forward -m`, `copy --from/--to`, `move --from/--to`. Previously the value was passed verbatim to the backend; now each goes through `account.resolve_mailbox`. The shared `mailboxes` / `envelopes` / `flags` / `attachments` commands already resolved through `MailboxArg`; this brings the `messages` group in line. + +- Fixed the success-message dispatch in `handler::route`: `(save, send)` cases `(true, false)` and `(false, true)` were swapped, printing "saved" after a pure send and "sent" after a pure save. + ### 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). diff --git a/src/shared/messages/add.rs b/src/shared/messages/add.rs index 1806369a..8a3909db 100644 --- a/src/shared/messages/add.rs +++ b/src/shared/messages/add.rs @@ -23,20 +23,33 @@ use io_email::flag::Flag; use pimalaya_cli::printer::Printer; use serde::Serialize; -use crate::shared::{client::EmailClient, flags::arg::FlagArg, messages::arg::MessageArg}; +use crate::account::context::Account; +use crate::shared::{ + client::EmailClient, + flags::arg::FlagArg, + messages::{ + arg::MessageArg, + handler::{self, Outcome}, + }, +}; /// Add a raw RFC 5322 message to a mailbox. /// /// The message can be passed as a positional file path, an inline raw /// string, or piped via stdin (see [`MessageArg`] for resolution -/// order). IMAP appends via `APPEND` (RFC 3501); JMAP uploads the -/// blob and imports it via `Email/import` (the destination mailbox -/// is resolved from `--mailbox` by exact-match name); Maildir writes -/// a new file under the target maildir's `cur/` subdir using the -/// standard tmp-then-rename delivery protocol. +/// order). The destination is resolved through the account's +/// `[mailbox.alias]` map before the backend call. Pass `--send` to +/// also push the message through the account's send path after the +/// append (mirrors `messages send --save `). +/// +/// IMAP appends via `APPEND` (RFC 3501); JMAP uploads the blob and +/// imports it via `Email/import` (the destination mailbox is +/// resolved by exact-match name); Maildir writes a new file under +/// the target maildir's `cur/` subdir using the standard +/// tmp-then-rename delivery protocol. #[derive(Debug, Parser)] pub struct MessageAddCommand { - /// Destination mailbox name or path. Mandatory. + /// Destination mailbox name or alias. Mandatory. #[arg(long = "mailbox", short = 'm', value_name = "NAME")] pub mailbox: String, @@ -44,26 +57,41 @@ pub struct MessageAddCommand { #[arg(long = "flag", short = 'f', value_name = "FLAG", num_args = 0..)] pub flag: Vec, + /// Send the message after appending it. Combines with the + /// mandatory `--mailbox` to save-then-send. + #[arg(long)] + pub send: bool, + #[command(flatten)] pub message: MessageArg, } impl MessageAddCommand { - pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + 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)?; - printer.out(MessageAddOutput { id }) + let outcome = handler::apply(account, client, raw, &flags, Some(&self.mailbox), self.send)?; + let Outcome::Saved { id, sent } = outcome else { + unreachable!("--mailbox is mandatory; handler::apply always reports Saved"); + }; + printer.out(MessageAddOutput { id, sent }) } } #[derive(Serialize)] struct MessageAddOutput { id: String, + sent: bool, } impl fmt::Display for MessageAddOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Message {} successfully added", self.id) + let suffix = if self.sent { " and sent" } else { "" }; + write!(f, "Message {} successfully added{suffix}", self.id) } } diff --git a/src/shared/messages/cli.rs b/src/shared/messages/cli.rs index 865e72ff..f7d8b721 100644 --- a/src/shared/messages/cli.rs +++ b/src/shared/messages/cli.rs @@ -62,14 +62,14 @@ impl MessageCommand { client: &mut EmailClient, ) -> Result<()> { match self { - Self::Add(cmd) => cmd.execute(printer, client), + Self::Add(cmd) => cmd.execute(printer, account, client), Self::Compose(cmd) => cmd.execute(printer, account, client), - Self::Copy(cmd) => cmd.execute(printer, client), + Self::Copy(cmd) => cmd.execute(printer, account, client), Self::Forward(cmd) => cmd.execute(printer, account, client), - Self::Move(cmd) => cmd.execute(printer, client), - Self::Read(cmd) => cmd.execute(printer, client), + Self::Move(cmd) => cmd.execute(printer, account, client), + Self::Read(cmd) => cmd.execute(printer, account, client), Self::Reply(cmd) => cmd.execute(printer, account, client), - Self::Send(cmd) => cmd.execute(printer, client), + Self::Send(cmd) => cmd.execute(printer, account, client), } } } diff --git a/src/shared/messages/compose.rs b/src/shared/messages/compose.rs index 15afc52a..04b001ad 100644 --- a/src/shared/messages/compose.rs +++ b/src/shared/messages/compose.rs @@ -26,7 +26,7 @@ use crate::shared::{ client::EmailClient, messages::{ builder::{self, BuilderArgs}, - output, + handler, }, }; @@ -124,7 +124,7 @@ impl MessageComposeCommand { None, )?; - output::route( + handler::route( printer, account, client, diff --git a/src/shared/messages/copy.rs b/src/shared/messages/copy.rs index 6a3e0725..2723f4d9 100644 --- a/src/shared/messages/copy.rs +++ b/src/shared/messages/copy.rs @@ -19,21 +19,24 @@ use anyhow::Result; use clap::Parser; use pimalaya_cli::printer::{Message, Printer}; +use crate::account::context::Account; use crate::shared::{client::EmailClient, flags::arg::MessageIdsArg}; /// Copy message(s) from one mailbox to another within the active /// account. /// -/// IMAP uses `UID COPY` (RFC 3501); JMAP uses `Email/set` patches that -/// add the destination to each email's `mailboxIds`; Maildir copies -/// the underlying file. Cross-account / cross-backend copy is out of +/// Both `--from` and `--to` are resolved through the account's +/// `[mailbox.alias]` map before the backend call. IMAP uses +/// `UID COPY` (RFC 3501); JMAP uses `Email/set` patches that add the +/// destination to each email's `mailboxIds`; Maildir copies the +/// underlying file. Cross-account / cross-backend copy is out of /// scope. #[derive(Debug, Parser)] pub struct MessageCopyCommand { #[command(flatten)] pub ids: MessageIdsArg, - /// Source mailbox name or path (IMAP/Maildir). For JMAP this is + /// Source mailbox name or alias (IMAP/Maildir). For JMAP this is /// resolved by exact-match name against `Mailbox/get`. #[arg( long = "from", @@ -43,15 +46,22 @@ pub struct MessageCopyCommand { )] pub from: String, - /// Destination mailbox name or path. Mandatory. + /// Destination mailbox name or alias. Mandatory. #[arg(long = "to", short = 't', value_name = "NAME")] pub to: String, } impl MessageCopyCommand { - pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { + let from = account.resolve_mailbox(&self.from).to_owned(); + let to = account.resolve_mailbox(&self.to).to_owned(); let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect(); - client.copy_messages(&self.from, &self.to, &ids)?; + client.copy_messages(&from, &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 3ab29595..33eacbca 100644 --- a/src/shared/messages/forward.rs +++ b/src/shared/messages/forward.rs @@ -26,7 +26,7 @@ use crate::shared::{ client::EmailClient, messages::{ builder::{self, BuilderArgs, PostingStyle, SourceArgs, SourceMode}, - output, + handler, }, }; @@ -110,7 +110,8 @@ impl MessageForwardCommand { account: &mut Account, client: &mut EmailClient, ) -> Result<()> { - let source = client.get_message(&self.mailbox, &self.id)?; + let mailbox = account.resolve_mailbox(&self.mailbox).to_owned(); + let source = client.get_message(&mailbox, &self.id)?; let raw = builder::build( BuilderArgs { @@ -133,7 +134,7 @@ impl MessageForwardCommand { }), )?; - output::route( + handler::route( printer, account, client, diff --git a/src/shared/messages/handler.rs b/src/shared/messages/handler.rs new file mode 100644 index 00000000..e9c742ea --- /dev/null +++ b/src/shared/messages/handler.rs @@ -0,0 +1,115 @@ +// 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 . + +//! Post-build routing: where the produced MIME bytes go. +//! +//! [`apply`] performs the requested side-effects (stdout dump, +//! save-to-mailbox, send, or save-then-send) and returns an +//! [`Outcome`] describing what happened. [`route`] is a thin wrapper +//! that prints a generic "Message successfully X" line based on the +//! outcome, used by the built-in flag composers +//! (`compose` / `reply` / `forward`) and by `messages send`. +//! +//! Callers that need a richer success line (e.g. `messages add` +//! reporting the appended backend id) call [`apply`] directly and +//! render their own output from the [`Outcome`]. +//! +//! [`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::{account::context::Account, shared::client::EmailClient}; + +/// What [`apply`] actually did with `raw`. +pub enum Outcome { + /// Neither `save` nor `send`: bytes were written to stdout. + Stdout, + /// Saved to a mailbox; `id` is the backend-assigned id of the + /// new message, `sent` is `true` when `send` was also requested. + Saved { id: String, sent: bool }, + /// Sent only (no save). The send path returns no id. + Sent, +} + +/// Performs the requested combination of side-effects without +/// printing anything. `save` writes a copy to the named mailbox +/// (resolved through the account's alias map) with `flags` attached; +/// `send` pushes the message through the configured SMTP / JMAP send +/// path. With neither set, dumps `raw` to stdout. +pub fn apply( + account: &Account, + client: &mut EmailClient, + raw: Vec, + flags: &[Flag], + save: Option<&str>, + send: bool, +) -> Result { + if !send && save.is_none() { + let mut out = stdout().lock(); + out.write_all(&raw)?; + return Ok(Outcome::Stdout); + } + + let saved_id = match save { + Some(name) => { + let mailbox = account.resolve_mailbox(name); + Some(client.add_message(mailbox, flags, raw.clone())?) + } + None => None, + }; + + if send { + client.send_message(raw)?; + } + + Ok(match saved_id { + Some(id) => Outcome::Saved { id, sent: send }, + None => Outcome::Sent, + }) +} + +/// Generic wrapper over [`apply`]: hard-codes `\Seen` as the saved +/// flag and prints a "Message successfully X" line. Used by the +/// built-in flag composers and by `messages send`. +pub fn route( + printer: &mut impl Printer, + account: &Account, + client: &mut EmailClient, + raw: Vec, + save: Option<&str>, + send: bool, +) -> Result<()> { + let outcome = apply( + account, + client, + raw, + &[Flag::from_iana(IanaFlag::Seen)], + save, + send, + )?; + let msg = match outcome { + Outcome::Stdout => return Ok(()), + Outcome::Saved { sent: true, .. } => "Message successfully saved and sent", + Outcome::Saved { sent: false, .. } => "Message successfully saved", + Outcome::Sent => "Message successfully sent", + }; + printer.out(Message::new(msg)) +} diff --git a/src/shared/messages/mod.rs b/src/shared/messages/mod.rs index 24e8b600..1e9bd13a 100644 --- a/src/shared/messages/mod.rs +++ b/src/shared/messages/mod.rs @@ -22,8 +22,8 @@ pub mod cli; pub mod compose; pub mod copy; pub mod forward; +pub mod handler; pub mod mv; -pub mod output; pub mod read; pub mod reply; pub mod send; diff --git a/src/shared/messages/mv.rs b/src/shared/messages/mv.rs index d6aa71e1..6651df5f 100644 --- a/src/shared/messages/mv.rs +++ b/src/shared/messages/mv.rs @@ -19,21 +19,24 @@ use anyhow::Result; use clap::Parser; use pimalaya_cli::printer::{Message, Printer}; +use crate::account::context::Account; use crate::shared::{client::EmailClient, flags::arg::MessageIdsArg}; /// Move message(s) from one mailbox to another within the active /// account. /// -/// IMAP uses `UID MOVE` (RFC 6851); JMAP uses `Email/set` patches that -/// remove the source and add the destination from each email's -/// `mailboxIds`; Maildir renames the underlying file. Cross-account / -/// cross-backend move is out of scope. +/// Both `--from` and `--to` are resolved through the account's +/// `[mailbox.alias]` map before the backend call. IMAP uses +/// `UID MOVE` (RFC 6851); JMAP uses `Email/set` patches that remove +/// the source and add the destination from each email's +/// `mailboxIds`; Maildir renames the underlying file. Cross-account +/// / cross-backend move is out of scope. #[derive(Debug, Parser)] pub struct MessageMoveCommand { #[command(flatten)] pub ids: MessageIdsArg, - /// Source mailbox name or path (IMAP/Maildir). For JMAP this is + /// Source mailbox name or alias (IMAP/Maildir). For JMAP this is /// resolved by exact-match name against `Mailbox/get`. #[arg( long = "from", @@ -43,15 +46,22 @@ pub struct MessageMoveCommand { )] pub from: String, - /// Destination mailbox name or path. Mandatory. + /// Destination mailbox name or alias. Mandatory. #[arg(long = "to", short = 't', value_name = "NAME")] pub to: String, } impl MessageMoveCommand { - pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { + let from = account.resolve_mailbox(&self.from).to_owned(); + let to = account.resolve_mailbox(&self.to).to_owned(); let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect(); - client.move_messages(&self.from, &self.to, &ids)?; + client.move_messages(&from, &to, &ids)?; printer.out(Message::new("Message(s) successfully moved")) } } diff --git a/src/shared/messages/output.rs b/src/shared/messages/output.rs deleted file mode 100644 index 510ed524..00000000 --- a/src/shared/messages/output.rs +++ /dev/null @@ -1,75 +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 . - -//! Post-build routing: where the produced MIME bytes go. -//! -//! 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::{account::context::Account, shared::client::EmailClient}; - -/// Routes `raw` through the requested combination of side-effects. -/// `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>, - send: bool, -) -> Result<()> { - if !send && save.is_none() { - let mut out = stdout().lock(); - out.write_all(&raw)?; - return Ok(()); - } - - 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)?; - } - - 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 ea6e5a88..816c97b1 100644 --- a/src/shared/messages/read.rs +++ b/src/shared/messages/read.rs @@ -26,6 +26,7 @@ use mail_parser::{Message, MessageParser}; use pimalaya_cli::printer::Printer; use serde::Serialize; +use crate::account::context::Account; use crate::shared::client::EmailClient; /// Read a message from the active account (built-in flag reader). @@ -42,8 +43,8 @@ pub struct MessageReadCommand { #[arg(value_name = "ID")] pub id: String, - /// Mailbox name or path (IMAP mailbox / Maildir path). Ignored for - /// JMAP, which addresses messages by id directly. + /// Mailbox name or alias (IMAP mailbox / Maildir path). Ignored + /// for JMAP, which addresses messages by id directly. #[arg( long = "mailbox", short = 'm', @@ -59,12 +60,18 @@ pub struct MessageReadCommand { } impl MessageReadCommand { - pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { if self.raw && printer.is_json() { bail!("`--raw` and `--json` cannot be combined"); } - let raw = client.get_message(&self.mailbox, &self.id)?; + let mailbox = account.resolve_mailbox(&self.mailbox).to_owned(); + let raw = client.get_message(&mailbox, &self.id)?; if self.raw { let mut out = stdout().lock(); diff --git a/src/shared/messages/reply.rs b/src/shared/messages/reply.rs index 755bc95f..e1fd90cb 100644 --- a/src/shared/messages/reply.rs +++ b/src/shared/messages/reply.rs @@ -26,7 +26,7 @@ use crate::shared::{ client::EmailClient, messages::{ builder::{self, BuilderArgs, PostingStyle, SourceArgs, SourceMode}, - output, + handler, }, }; @@ -121,7 +121,8 @@ impl MessageReplyCommand { account: &mut Account, client: &mut EmailClient, ) -> Result<()> { - let source = client.get_message(&self.mailbox, &self.id)?; + let mailbox = account.resolve_mailbox(&self.mailbox).to_owned(); + let source = client.get_message(&mailbox, &self.id)?; let raw = builder::build( BuilderArgs { @@ -144,7 +145,7 @@ impl MessageReplyCommand { }), )?; - output::route( + handler::route( printer, account, client, diff --git a/src/shared/messages/send.rs b/src/shared/messages/send.rs index df6cd5ae..f54ec3d7 100644 --- a/src/shared/messages/send.rs +++ b/src/shared/messages/send.rs @@ -17,9 +17,13 @@ use anyhow::Result; use clap::Parser; -use pimalaya_cli::printer::{Message, Printer}; +use pimalaya_cli::printer::Printer; -use crate::shared::{client::EmailClient, messages::arg::MessageArg}; +use crate::account::context::Account; +use crate::shared::{ + client::EmailClient, + messages::{arg::MessageArg, handler}, +}; /// Send a message via the active account. /// @@ -29,17 +33,27 @@ use crate::shared::{client::EmailClient, messages::arg::MessageArg}; /// /// The message can be passed as a positional file path, an inline /// raw string, or piped via stdin (see [`MessageArg`] for resolution -/// order). +/// order). Pass `--save ` to also append a copy of the +/// sent message to a mailbox; the mailbox name is resolved through +/// the account's `[mailbox.alias]` map before the backend call. #[derive(Debug, Parser)] pub struct MessageSendCommand { + /// Append a copy of the sent message to this mailbox. + #[arg(long, value_name = "MAILBOX")] + pub save: Option, + #[command(flatten)] pub message: MessageArg, } impl MessageSendCommand { - pub fn execute(self, printer: &mut impl Printer, client: &mut EmailClient) -> Result<()> { + pub fn execute( + self, + printer: &mut impl Printer, + account: &mut Account, + client: &mut EmailClient, + ) -> Result<()> { let raw = self.message.parse()?.into_bytes(); - client.send_message(raw)?; - printer.out(Message::new("Message successfully sent")) + handler::route(printer, account, client, raw, self.save.as_deref(), true) } }