fix: raw message arg (imported from mml)

This commit is contained in:
Clément DOUIN
2026-05-31 20:06:36 +02:00
parent aff4fddfb4
commit a28f95f9db
9 changed files with 151 additions and 174 deletions
+4
View File
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Restored the RFC 2971 `ID`-after-auth quirk under the new shape `imap.id.{auto, fields}`. Set `imap.id.auto = true` to chain an `ID` exchange straight after IMAP authentication (required by mail.qq.com, fastmail). `imap.id.fields` is a `{ name = bool, … }` map: missing keys are not transmitted, `false` sends `NIL`, `true` sends himalaya's canned value for the well-known keys (`name`, `version`, `vendor`, `support-url`) or `NIL` (with a warning) for any other key. Replaces the v1.2.0 `imap.extensions.id.send-after-auth` flag dropped during the v2 migration.
### 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 <PATH>` flag on `messages add` is gone (positional path replaces it).
### Fixed
- Fixed compilation error when `wizard` feature was disabled ([#634]).
+10 -22
View File
@@ -15,8 +15,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::io::{BufRead, IsTerminal, stdin};
use anyhow::Result;
use clap::Parser;
use io_imap::types::{
@@ -24,12 +22,16 @@ use io_imap::types::{
};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
use crate::{
imap::{client::ImapClient, mailbox::arg::MailboxNameArg},
shared::messages::arg::MessageArg,
};
/// Save a message to a mailbox.
///
/// This command appends a message to the specified mailbox. The
/// message is read from stdin in RFC 5322 format (raw email).
/// Appends a message to the specified mailbox. The message can be
/// passed as a positional file path, an inline raw string, or piped
/// via stdin (see [`MessageArg`] for resolution order).
#[derive(Debug, Parser)]
pub struct ImapMessageSaveCommand {
#[command(flatten)]
@@ -39,28 +41,14 @@ pub struct ImapMessageSaveCommand {
#[arg(short, long, num_args = 0..)]
pub flag: Vec<String>,
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message", value_name = "MESSAGE")]
pub message: Vec<String>,
#[command(flatten)]
pub message: MessageArg,
}
impl ImapMessageSaveCommand {
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox: Mailbox<'static> = self.mailbox.inner.try_into()?;
let message = if !self.message.is_empty() || stdin().is_terminal() || printer.is_json() {
self.message
.join(" ")
.replace('\r', "")
.replace('\n', "\r\n")
} else {
stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let message = self.message.parse()?;
let message = Literal::try_from(message)?;
let message = LiteralOrLiteral8::Literal(message);
+14 -23
View File
@@ -15,10 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
collections::BTreeMap,
io::{BufRead, IsTerminal, stdin},
};
use std::collections::BTreeMap;
use anyhow::{Result, bail};
use clap::Parser;
@@ -30,15 +27,20 @@ use pimalaya_cli::printer::{Message, Printer};
use pimalaya_stream::tls::Tls;
use url::Url;
use crate::jmap::{
client::{JmapClient, jmap_http_auth},
error::format_set_error,
use crate::{
jmap::{
client::{JmapClient, jmap_http_auth},
error::format_set_error,
},
shared::messages::arg::MessageArg,
};
/// Import an RFC 5322 message into a mailbox (upload + Email/import).
///
/// Reads the raw message from stdin or as trailing arguments. Use
/// `--upload-only` to stop after the upload and print the blobId.
/// The message can be passed as a positional file path, an inline
/// raw string, or piped via stdin (see [`MessageArg`] for resolution
/// order). Use `--upload-only` to stop after the upload and print
/// the blobId.
#[derive(Debug, Parser)]
pub struct JmapEmailImportCommand {
/// Mailbox ID(s) to place the imported email in.
@@ -57,24 +59,13 @@ pub struct JmapEmailImportCommand {
#[arg(long)]
pub upload_only: bool,
/// The raw RFC 5322 message (headers + body). Read from stdin if omitted.
#[arg(trailing_var_arg = true)]
#[arg(name = "message", value_name = "MESSAGE")]
pub message: Vec<String>,
#[command(flatten)]
pub message: MessageArg,
}
impl JmapEmailImportCommand {
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let data: Vec<u8> = if stdin().is_terminal() || printer.is_json() {
self.message
.join(" ")
.replace('\r', "")
.replace('\n', "\r\n")
.into_bytes()
} else {
let lines: Vec<String> = stdin().lock().lines().map_while(Result::ok).collect();
lines.join("\r\n").into_bytes()
};
let data = self.message.parse()?.into_bytes();
let session = client.session().expect("session loaded by new_jmap_client");
let api_url = session.api_url.clone();
+14 -30
View File
@@ -15,11 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
fmt,
io::{BufRead, IsTerminal, stdin},
path::PathBuf,
};
use std::{fmt, path::PathBuf};
use anyhow::Result;
use clap::Parser;
@@ -27,16 +23,20 @@ use io_maildir::flag::MaildirFlags;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::maildir::{
arg::{MaildirPathFlag, MaildirSubdirArg},
client::MaildirClient,
flag::arg::FlagArg,
use crate::{
maildir::{
arg::{MaildirPathFlag, MaildirSubdirArg},
client::MaildirClient,
flag::arg::FlagArg,
},
shared::messages::arg::MessageArg,
};
/// Save a message to a mailbox.
///
/// This command appends a message to the specified mailbox. The
/// message is read from stdin in RFC 5322 format (raw email).
/// Appends a message to the specified maildir. The message can be
/// passed as a positional file path, an inline raw string, or piped
/// via stdin (see [`MessageArg`] for resolution order).
#[derive(Debug, Parser)]
pub struct MaildirMessageSaveCommand {
#[command(flatten)]
@@ -51,30 +51,14 @@ pub struct MaildirMessageSaveCommand {
#[arg(long = "flag", short, num_args = 0..)]
pub flags: Vec<FlagArg>,
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message", value_name = "MESSAGE")]
pub message: Vec<String>,
#[command(flatten)]
pub message: MessageArg,
}
impl MaildirMessageSaveCommand {
pub fn execute(self, printer: &mut impl Printer, client: MaildirClient) -> Result<()> {
let maildir = client.resolve_maildir(&self.maildir.inner)?;
let msg = if stdin().is_terminal() || printer.is_json() {
self.message
.join(" ")
.replace('\r', "")
.replace('\n', "\r\n")
} else {
stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let msg = self.message.parse()?;
let flags = MaildirFlags::from_iter(self.flags.into_iter().map(Into::into));
let (id, path) = client.store(maildir, self.subdir.into(), flags, msg.into_bytes())?;
+13 -34
View File
@@ -15,28 +15,25 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
fmt,
io::{IsTerminal, Read, stdin},
path::PathBuf,
};
use std::fmt;
use anyhow::{Result, bail};
use anyhow::Result;
use clap::Parser;
use io_email::flag::Flag;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::shared::{client::EmailClient, flags::arg::FlagArg};
use crate::shared::{client::EmailClient, flags::arg::FlagArg, messages::arg::MessageArg};
/// Add a raw RFC 5322 message to a mailbox.
///
/// The message body is read from stdin by default; pass `--file
/// <PATH>` to read from a file instead. 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.
/// 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.
#[derive(Debug, Parser)]
pub struct MessageAddCommand {
/// Destination mailbox name or path. Mandatory.
@@ -47,14 +44,13 @@ pub struct MessageAddCommand {
#[arg(long = "flag", short = 'f', value_name = "FLAG", num_args = 0..)]
pub flag: Vec<FlagArg>,
/// Read the raw message from this file instead of stdin.
#[arg(long = "file", value_name = "PATH")]
pub file: Option<PathBuf>,
#[command(flatten)]
pub message: MessageArg,
}
impl MessageAddCommand {
pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> {
let raw = read_raw(&self.file)?;
let raw = self.message.parse()?.into_bytes();
let flags: Vec<Flag> = self.flag.iter().map(Into::into).collect();
let id = client.add_message(&self.mailbox, &flags, raw)?;
printer.out(MessageAddOutput { id })
@@ -71,20 +67,3 @@ impl fmt::Display for MessageAddOutput {
write!(f, "Message {} successfully added", self.id)
}
}
fn read_raw(file: &Option<PathBuf>) -> Result<Vec<u8>> {
if let Some(path) = file {
return Ok(std::fs::read(path)?);
}
if stdin().is_terminal() {
bail!(
"`messages add` reads the raw message from stdin or `--file <PATH>` — \
nothing was provided"
);
}
let mut buf = Vec::new();
stdin().read_to_end(&mut buf)?;
Ok(buf)
}
+78
View File
@@ -0,0 +1,78 @@
// This file is part of Himalaya, a CLI to manage emails.
//
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
//
// 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 <https://www.gnu.org/licenses/>.
//! Reusable clap arg for raw RFC 5322 message input.
//!
//! Ported verbatim from `mml::cli::args::MessageArg` so every
//! message-source command (shared `messages add`/`send`, per-protocol
//! `imap message save`, `maildir message save`, `jmap email import`,
//! `smtp message send`) accepts the same three forms: a file path, an
//! inline raw message, or stdin.
use std::{
fs,
io::{IsTerminal, stdin},
};
use anyhow::bail;
use clap::Parser;
use pimalaya_cli::clap::parsers::path_parser;
/// Trailing positional that resolves to a raw RFC 5322 message.
///
/// Resolution order:
///
/// 1. When the positional arg is non-empty: join the tokens with a
/// space, replace `\r` / `\n` literals with `\r\n`, and treat the
/// result as a path. If the path parses and the file is readable,
/// return its contents; otherwise treat the joined value as the
/// raw message verbatim.
/// 2. Otherwise, when stdin is piped, return stdin lines joined with
/// `\r\n`.
/// 3. Otherwise, bail.
#[derive(Debug, 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)]
pub raw: Vec<String>,
}
impl MessageArg {
pub fn parse(&self) -> anyhow::Result<String> {
if !self.raw.is_empty() {
let mime = self.raw.join(" ").replace("\\r", "").replace("\\n", "\r\n");
let Ok(path) = path_parser(&mime) else {
return Ok(mime);
};
let Ok(mime) = fs::read_to_string(path) else {
return Ok(mime);
};
return Ok(mime);
}
if !stdin().is_terminal() {
let lines: Vec<_> = stdin().lines().map_while(Result::ok).collect();
return Ok(lines.join("\r\n"));
}
bail!("Message cannot be empty");
}
}
+1
View File
@@ -16,6 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod add;
pub mod arg;
pub mod builder;
pub mod cli;
pub mod compose;
+7 -36
View File
@@ -15,16 +15,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
io::{BufRead, IsTerminal, stdin},
path::PathBuf,
};
use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::shared::client::EmailClient;
use crate::shared::{client::EmailClient, messages::arg::MessageArg};
/// Send a message via the active account.
///
@@ -32,42 +27,18 @@ use crate::shared::client::EmailClient;
/// outgoing backend. The envelope sender is taken from the `From:`
/// header and recipients are collected from `To:` / `Cc:` / `Bcc:`.
///
/// Source priority: `--file <PATH>` (read from file), otherwise stdin
/// when piped, otherwise the positional `<MESSAGE>` args joined with
/// CRLF.
/// The message can be passed as a positional file path, an inline
/// raw string, or piped via stdin (see [`MessageArg`] for resolution
/// order).
#[derive(Debug, Parser)]
pub struct MessageSendCommand {
/// Read the raw message from this file instead of stdin or the
/// positional argument.
#[arg(long = "file", value_name = "PATH")]
pub file: Option<PathBuf>,
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message", value_name = "MESSAGE")]
pub message: Vec<String>,
#[command(flatten)]
pub message: MessageArg,
}
impl MessageSendCommand {
pub fn execute(self, printer: &mut impl Printer, mut client: EmailClient) -> Result<()> {
let raw: Vec<u8> = if let Some(path) = self.file.as_deref() {
std::fs::read(path)?
} else if stdin().is_terminal() || printer.is_json() {
self.message
.join(" ")
.replace('\r', "")
.replace('\n', "\r\n")
.into_bytes()
} else {
stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
.into_bytes()
};
let raw = self.message.parse()?.into_bytes();
client.send_message(raw)?;
printer.out(Message::new("Message successfully sent"))
}
+10 -29
View File
@@ -15,11 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
borrow::Cow,
collections::HashSet,
io::{BufRead, IsTerminal, stdin},
};
use std::{borrow::Cow, collections::HashSet};
use anyhow::{Result, bail};
use clap::Parser;
@@ -30,40 +26,25 @@ use io_smtp::rfc5321::types::{
use mail_parser::{Addr, Address, HeaderName, HeaderValue, MessageParser};
use pimalaya_cli::printer::{Message, Printer};
use crate::smtp::client::SmtpClient;
use crate::{shared::messages::arg::MessageArg, smtp::client::SmtpClient};
/// Send a message to a mailbox.
/// Send a raw RFC 5322 message via SMTP.
///
/// This command appends a message to the specified mailbox. The
/// message is read from stdin in RFC 5322 format (raw email).
/// The message can be passed as a positional file path, an inline
/// raw string, or piped via stdin (see [`MessageArg`] for resolution
/// order). The envelope sender is taken from the `From:` header and
/// recipients are collected from `To:` / `Cc:` / `Bcc:`.
#[derive(Debug, Parser)]
pub struct SmtpMessageSendCommand {
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message", value_name = "MESSAGE")]
pub message: Vec<String>,
#[command(flatten)]
pub message: MessageArg,
}
impl SmtpMessageSendCommand {
pub fn execute(self, printer: &mut impl Printer, mut client: SmtpClient) -> Result<()> {
let message = if stdin().is_terminal() || printer.is_json() {
self.message
.join(" ")
.replace('\r', "")
.replace('\n', "\r\n")
} else {
stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
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())?;
printer.out(Message::new("Message successfully sent"))
}
}