add msg copy move delete save commands

This commit is contained in:
Clément DOUIN
2026-03-04 12:33:43 +01:00
parent fe03e2de67
commit 733e33559a
8 changed files with 328 additions and 1 deletions
+5 -1
View File
@@ -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),
}
}
}
+70
View File
@@ -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"))
}
}
+86
View File
@@ -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()
)))
}
}
+39
View File
@@ -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),
}
}
}
+71
View File
@@ -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"))
}
}
+55
View File
@@ -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"))
}
}
+1
View File
@@ -0,0 +1 @@
pub mod command;
+1
View File
@@ -2,4 +2,5 @@ pub mod command;
pub mod envelope;
pub mod flag;
pub mod mailbox;
pub mod message;
pub mod stream;