mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 11:27:53 +08:00
fix: raw message arg (imported from mml)
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user