From 733e33559ab7a4ecb55fb14780ef06705195b93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 4 Mar 2026 12:33:43 +0100 Subject: [PATCH] add msg copy move delete save commands --- src/imap/command.rs | 6 ++- src/imap/message/command/copy.rs | 70 ++++++++++++++++++++++++ src/imap/message/command/delete.rs | 86 ++++++++++++++++++++++++++++++ src/imap/message/command/mod.rs | 39 ++++++++++++++ src/imap/message/command/move.rs | 71 ++++++++++++++++++++++++ src/imap/message/command/save.rs | 55 +++++++++++++++++++ src/imap/message/mod.rs | 1 + src/imap/mod.rs | 1 + 8 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/imap/message/command/copy.rs create mode 100644 src/imap/message/command/delete.rs create mode 100644 src/imap/message/command/mod.rs create mode 100644 src/imap/message/command/move.rs create mode 100644 src/imap/message/command/save.rs create mode 100644 src/imap/message/mod.rs diff --git a/src/imap/command.rs b/src/imap/command.rs index b1fd9fc9..8b0d317e 100644 --- a/src/imap/command.rs +++ b/src/imap/command.rs @@ -6,7 +6,7 @@ use crate::{ config::ImapConfig, imap::{ envelope::command::EnvelopeCommand, flag::command::FlagCommand, - mailbox::command::MailboxCommand, + mailbox::command::MailboxCommand, message::command::MessageCommand, }, }; @@ -26,6 +26,9 @@ pub enum ImapCommand { #[command(subcommand)] #[command(aliases = ["mboxes", "mbox"])] Mailboxes(MailboxCommand), + #[command(subcommand)] + #[command(aliases = ["message", "msg"])] + Messages(MessageCommand), } impl ImapCommand { @@ -34,6 +37,7 @@ impl ImapCommand { Self::Envelopes(cmd) => cmd.execute(printer, config), Self::Flags(cmd) => cmd.execute(printer, config), Self::Mailboxes(cmd) => cmd.execute(printer, config), + Self::Messages(cmd) => cmd.execute(printer, config), } } } diff --git a/src/imap/message/command/copy.rs b/src/imap/message/command/copy.rs new file mode 100644 index 00000000..6b49d08c --- /dev/null +++ b/src/imap/message/command/copy.rs @@ -0,0 +1,70 @@ +use anyhow::{bail, Result}; +use clap::Parser; +use io_imap::{ + coroutines::{copy::*, select::*}, + types::mailbox::Mailbox, +}; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; + +use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream}; + +/// Copy messages to another mailbox. +/// +/// This command copies messages identified by the given sequence set +/// from the source mailbox to the destination mailbox. +#[derive(Debug, Parser)] +pub struct CopyMessageCommand { + #[command(flatten)] + pub mailbox: MailboxNameOptionalFlag, + + /// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). + #[arg(name = "sequence_set", value_name = "SEQUENCE")] + pub sequence_set: String, + + /// The destination mailbox. + #[arg(name = "destination", value_name = "DESTINATION")] + pub destination: String, + + /// Use sequence numbers instead of UIDs. + #[arg(long)] + pub seq: bool, +} + +impl CopyMessageCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + let (context, mut stream) = stream::connect(config)?; + + let mailbox = self.mailbox.name.try_into()?; + + // SELECT mailbox + let mut arg = None; + let mut coroutine = ImapSelect::new(context, mailbox); + + let context = loop { + match coroutine.resume(arg.take()) { + ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapSelectResult::Ok { context, .. } => break context, + ImapSelectResult::Err { err, .. } => bail!(err), + } + }; + + // Parse sequence set and destination + let sequence_set = self.sequence_set.as_str().try_into()?; + let destination: Mailbox<'static> = self.destination.try_into()?; + + // COPY + let mut arg = None; + let mut coroutine = ImapCopy::new(context, sequence_set, destination, !self.seq); + + loop { + match coroutine.resume(arg.take()) { + ImapCopyResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapCopyResult::Ok { .. } => break, + ImapCopyResult::Err { err, .. } => bail!(err), + } + } + + printer.out(Message::new("Message(s) successfully copied")) + } +} diff --git a/src/imap/message/command/delete.rs b/src/imap/message/command/delete.rs new file mode 100644 index 00000000..26dcdbf3 --- /dev/null +++ b/src/imap/message/command/delete.rs @@ -0,0 +1,86 @@ +use anyhow::{bail, Result}; +use clap::Parser; +use io_imap::{ + coroutines::{expunge::*, select::*, store::*}, + types::flag::{Flag, StoreType}, +}; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; + +use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream}; + +/// Delete messages from a mailbox. +/// +/// This command marks messages as deleted and expunges them from the +/// mailbox. The messages are permanently removed. +#[derive(Debug, Parser)] +pub struct DeleteMessageCommand { + #[command(flatten)] + pub mailbox: MailboxNameOptionalFlag, + + /// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). + #[arg(name = "sequence_set", value_name = "SEQUENCE")] + pub sequence_set: String, + + /// Use sequence numbers instead of UIDs. + #[arg(long)] + pub seq: bool, +} + +impl DeleteMessageCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + let (context, mut stream) = stream::connect(config)?; + + let mailbox = self.mailbox.name.try_into()?; + + // SELECT mailbox + let mut arg = None; + let mut coroutine = ImapSelect::new(context, mailbox); + + let context = loop { + match coroutine.resume(arg.take()) { + ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapSelectResult::Ok { context, .. } => break context, + ImapSelectResult::Err { err, .. } => bail!(err), + } + }; + + // Parse sequence set + let sequence_set = self.sequence_set.as_str().try_into()?; + + // STORE +FLAGS \Deleted + let mut arg = None; + let mut coroutine = ImapStoreSilent::new( + context, + sequence_set, + StoreType::Add, + vec![Flag::Deleted], + !self.seq, + ); + + let context = loop { + match coroutine.resume(arg.take()) { + ImapStoreSilentResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapStoreSilentResult::Ok { context, .. } => break context, + ImapStoreSilentResult::Err { err, .. } => bail!(err), + } + }; + + // EXPUNGE + let mut arg = None; + let mut coroutine = ImapExpunge::new(context); + + let expunged = loop { + match coroutine.resume(arg.take()) { + ImapExpungeResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapExpungeResult::Ok { expunged, .. } => break expunged, + ImapExpungeResult::Err { err, .. } => bail!(err), + } + }; + + printer.out(Message::new(format!( + "{} message(s) successfully deleted", + expunged.len() + ))) + } +} diff --git a/src/imap/message/command/mod.rs b/src/imap/message/command/mod.rs new file mode 100644 index 00000000..0f30764e --- /dev/null +++ b/src/imap/message/command/mod.rs @@ -0,0 +1,39 @@ +pub mod copy; +pub mod delete; +pub mod r#move; +pub mod save; + +use anyhow::Result; +use clap::Subcommand; +use pimalaya_toolbox::terminal::printer::Printer; + +use crate::{ + config::ImapConfig, + imap::message::command::{ + copy::CopyMessageCommand, delete::DeleteMessageCommand, r#move::MoveMessageCommand, + save::SaveMessageCommand, + }, +}; + +/// Manage messages. +/// +/// A message is a complete email including headers and body. This +/// subcommand allows you to save, copy, move, and delete messages. +#[derive(Debug, Subcommand)] +pub enum MessageCommand { + Save(SaveMessageCommand), + Copy(CopyMessageCommand), + Move(MoveMessageCommand), + Delete(DeleteMessageCommand), +} + +impl MessageCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + match self { + Self::Save(cmd) => cmd.execute(printer, config), + Self::Copy(cmd) => cmd.execute(printer, config), + Self::Move(cmd) => cmd.execute(printer, config), + Self::Delete(cmd) => cmd.execute(printer, config), + } + } +} diff --git a/src/imap/message/command/move.rs b/src/imap/message/command/move.rs new file mode 100644 index 00000000..86bf94b3 --- /dev/null +++ b/src/imap/message/command/move.rs @@ -0,0 +1,71 @@ +use anyhow::{bail, Result}; +use clap::Parser; +use io_imap::{ + coroutines::{r#move::*, select::*}, + types::mailbox::Mailbox, +}; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; + +use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream}; + +/// Move messages to another mailbox. +/// +/// This command moves messages identified by the given sequence set +/// from the source mailbox to the destination mailbox. Requires the +/// MOVE IMAP extension. +#[derive(Debug, Parser)] +pub struct MoveMessageCommand { + #[command(flatten)] + pub mailbox: MailboxNameOptionalFlag, + + /// The sequence set of messages (e.g., "1", "1,2,3", "1:*"). + #[arg(name = "sequence_set", value_name = "SEQUENCE")] + pub sequence_set: String, + + /// The destination mailbox. + #[arg(name = "destination", value_name = "DESTINATION")] + pub destination: String, + + /// Use sequence numbers instead of UIDs. + #[arg(long)] + pub seq: bool, +} + +impl MoveMessageCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + let (context, mut stream) = stream::connect(config)?; + + let mailbox = self.mailbox.name.try_into()?; + + // SELECT mailbox + let mut arg = None; + let mut coroutine = ImapSelect::new(context, mailbox); + + let context = loop { + match coroutine.resume(arg.take()) { + ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapSelectResult::Ok { context, .. } => break context, + ImapSelectResult::Err { err, .. } => bail!(err), + } + }; + + // Parse sequence set and destination + let sequence_set = self.sequence_set.as_str().try_into()?; + let destination: Mailbox<'static> = self.destination.try_into()?; + + // MOVE + let mut arg = None; + let mut coroutine = ImapMove::new(context, sequence_set, destination, !self.seq); + + loop { + match coroutine.resume(arg.take()) { + ImapMoveResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapMoveResult::Ok { .. } => break, + ImapMoveResult::Err { err, .. } => bail!(err), + } + } + + printer.out(Message::new("Message(s) successfully moved")) + } +} diff --git a/src/imap/message/command/save.rs b/src/imap/message/command/save.rs new file mode 100644 index 00000000..3b14152f --- /dev/null +++ b/src/imap/message/command/save.rs @@ -0,0 +1,55 @@ +use std::io::{self, Read}; + +use anyhow::{bail, Result}; +use clap::Parser; +use io_imap::{ + coroutines::append::*, + types::{core::Literal, extensions::binary::LiteralOrLiteral8, mailbox::Mailbox}, +}; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; + +use crate::{config::ImapConfig, imap::stream}; + +/// 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). +#[derive(Debug, Parser)] +pub struct SaveMessageCommand { + /// The mailbox to save the message to. + #[arg(name = "mailbox", value_name = "MAILBOX")] + pub mailbox: String, +} + +impl SaveMessageCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + let (context, mut stream) = stream::connect(config)?; + + // Read message from stdin + let mut message = Vec::new(); + io::stdin().read_to_end(&mut message)?; + + if message.is_empty() { + bail!("No message provided on stdin"); + } + + let mailbox: Mailbox<'static> = self.mailbox.try_into()?; + let literal = Literal::try_from(message)?; + let message = LiteralOrLiteral8::Literal(literal); + + // APPEND + let mut arg = None; + let mut coroutine = ImapAppend::new(context, mailbox, vec![], None, message); + + loop { + match coroutine.resume(arg.take()) { + ImapAppendResult::Io { io } => arg = Some(handle(&mut stream, io)?), + ImapAppendResult::Ok { .. } => break, + ImapAppendResult::Err { err, .. } => bail!(err), + } + } + + printer.out(Message::new("Message successfully saved")) + } +} diff --git a/src/imap/message/mod.rs b/src/imap/message/mod.rs new file mode 100644 index 00000000..9fe79612 --- /dev/null +++ b/src/imap/message/mod.rs @@ -0,0 +1 @@ +pub mod command; diff --git a/src/imap/mod.rs b/src/imap/mod.rs index e5aefca5..5498e1d1 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -2,4 +2,5 @@ pub mod command; pub mod envelope; pub mod flag; pub mod mailbox; +pub mod message; pub mod stream;