From 715fdb002a4e637f2de8e53f26b6ed4e388868dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 3 Mar 2026 21:37:57 +0100 Subject: [PATCH] re-implement old mailbox commands --- src/imap/mailbox/arg/name.rs | 51 ++++++------- src/imap/mailbox/command/add.rs | 61 ---------------- src/imap/mailbox/command/create.rs | 38 ++++++++++ src/imap/mailbox/command/delete.rs | 81 ++++++--------------- src/imap/mailbox/command/expunge.rs | 80 ++++++++++----------- src/imap/mailbox/command/mod.rs | 41 +++++------ src/imap/mailbox/command/purge.rs | 108 +++++++++++++++------------- src/imap/mailbox/mod.rs | 2 +- 8 files changed, 202 insertions(+), 260 deletions(-) delete mode 100644 src/imap/mailbox/command/add.rs create mode 100644 src/imap/mailbox/command/create.rs diff --git a/src/imap/mailbox/arg/name.rs b/src/imap/mailbox/arg/name.rs index 139c98dc..0fb8ecb4 100644 --- a/src/imap/mailbox/arg/name.rs +++ b/src/imap/mailbox/arg/name.rs @@ -1,16 +1,17 @@ use clap::Parser; -use email::folder::INBOX; -/// The optional folder name flag parser. +const INBOX: &str = "INBOX"; + +/// The optional mailbox name flag parser. #[derive(Debug, Parser)] -pub struct FolderNameOptionalFlag { - /// The name of the folder. - #[arg(long = "folder", short = 'f')] - #[arg(name = "folder_name", value_name = "NAME", default_value = INBOX)] +pub struct MailboxNameOptionalFlag { + /// The name of the mailbox. + #[arg(long = "mailbox", short = 'm')] + #[arg(name = "mailbox_name", value_name = "NAME", default_value = INBOX)] pub name: String, } -impl Default for FolderNameOptionalFlag { +impl Default for MailboxNameOptionalFlag { fn default() -> Self { Self { name: INBOX.to_owned(), @@ -18,15 +19,15 @@ impl Default for FolderNameOptionalFlag { } } -/// The optional folder name argument parser. +/// The optional mailbox name argument parser. #[derive(Debug, Parser)] -pub struct FolderNameOptionalArg { - /// The name of the folder. - #[arg(name = "folder_name", value_name = "FOLDER", default_value = INBOX)] +pub struct MailboxNameOptionalArg { + /// The name of the mailbox. + #[arg(name = "mailbox_name", value_name = "MAILBOX", default_value = INBOX)] pub name: String, } -impl Default for FolderNameOptionalArg { +impl Default for MailboxNameOptionalArg { fn default() -> Self { Self { name: INBOX.to_owned(), @@ -34,27 +35,27 @@ impl Default for FolderNameOptionalArg { } } -/// The required folder name argument parser. +/// The required mailbox name argument parser. #[derive(Debug, Parser)] -pub struct FolderNameArg { - /// The name of the folder. - #[arg(name = "folder_name", value_name = "FOLDER")] +pub struct MailboxNameArg { + /// The name of the mailbox. + #[arg(name = "mailbox_name", value_name = "MAILBOX")] pub name: String, } -/// The optional source folder name flag parser. +/// The optional source mailbox name flag parser. #[derive(Debug, Parser)] -pub struct SourceFolderNameOptionalFlag { - /// The name of the source folder. - #[arg(long = "folder", short = 'f')] - #[arg(name = "source_folder_name", value_name = "SOURCE", default_value = INBOX)] +pub struct SourceMailboxNameOptionalFlag { + /// The name of the source mailbox. + #[arg(long = "mailbox", short = 'm')] + #[arg(name = "source_mailbox_name", value_name = "SOURCE", default_value = INBOX)] pub name: String, } -/// The target folder name argument parser. +/// The target mailbox name argument parser. #[derive(Debug, Parser)] -pub struct TargetFolderNameArg { - /// The name of the target folder. - #[arg(name = "target_folder_name", value_name = "TARGET")] +pub struct TargetMailboxNameArg { + /// The name of the target mailbox. + #[arg(name = "target_mailbox_name", value_name = "TARGET")] pub name: String, } diff --git a/src/imap/mailbox/command/add.rs b/src/imap/mailbox/command/add.rs deleted file mode 100644 index 9eeab2a9..00000000 --- a/src/imap/mailbox/command/add.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::sync::Arc; - -use clap::Parser; -use color_eyre::Result; -use email::{ - config::Config, - {backend::feature::BackendFeatureSource, folder::add::AddFolder}, -}; -use pimalaya_tui::{ - himalaya::backend::BackendBuilder, - terminal::{cli::printer::Printer, config::TomlConfig as _}, -}; -use tracing::info; - -use crate::{ - account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, -}; - -/// Create the given folder. -/// -/// This command allows you to create a new folder using the given -/// name. -#[derive(Debug, Parser)] -pub struct FolderAddCommand { - #[command(flatten)] - pub folder: FolderNameArg, - - #[command(flatten)] - pub account: AccountNameFlag, -} - -impl FolderAddCommand { - pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { - info!("executing create folder command"); - - let folder = &self.folder.name; - - let (toml_account_config, account_config) = config - .clone() - .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { - c.account(name).ok() - })?; - - let backend = BackendBuilder::new( - Arc::new(toml_account_config), - Arc::new(account_config), - |builder| { - builder - .without_features() - .with_add_folder(BackendFeatureSource::Context) - }, - ) - .without_sending_backend() - .build() - .await?; - - backend.add_folder(folder).await?; - - printer.out(format!("Folder {folder} successfully created!\n")) - } -} diff --git a/src/imap/mailbox/command/create.rs b/src/imap/mailbox/command/create.rs new file mode 100644 index 00000000..e22811cb --- /dev/null +++ b/src/imap/mailbox/command/create.rs @@ -0,0 +1,38 @@ +use anyhow::{bail, Result}; +use clap::Parser; +use io_imap::coroutines::create::*; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; + +use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream}; + +/// Create the given mailbox. +/// +/// This command allows you to create a new mailbox using the given +/// name. +#[derive(Debug, Parser)] +pub struct CreateMailboxCommand { + #[command(flatten)] + pub mailbox: MailboxNameArg, +} + +impl CreateMailboxCommand { + 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()?; + + let mut arg = None; + let mut coroutine = ImapCreate::new(context, mailbox); + + loop { + match coroutine.resume(arg.take()) { + ImapCreateResult::Io(io) => arg = Some(handle(&mut stream, io)?), + ImapCreateResult::Ok { .. } => break, + ImapCreateResult::Err { err, .. } => bail!(err), + } + } + + printer.out(Message::new("Mailbox successfully created")) + } +} diff --git a/src/imap/mailbox/command/delete.rs b/src/imap/mailbox/command/delete.rs index b03b7bc9..ccbaa92a 100644 --- a/src/imap/mailbox/command/delete.rs +++ b/src/imap/mailbox/command/delete.rs @@ -1,73 +1,38 @@ -use std::{process, sync::Arc}; - +use anyhow::{bail, Result}; use clap::Parser; -use color_eyre::Result; -use email::{ - config::Config, - {backend::feature::BackendFeatureSource, folder::delete::DeleteFolder}, -}; -use pimalaya_tui::{ - himalaya::backend::BackendBuilder, - terminal::{cli::printer::Printer, config::TomlConfig as _, prompt}, -}; -use tracing::info; +use io_imap::coroutines::delete::*; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; -use crate::{ - account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, -}; +use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream}; -/// Delete the given folder. +/// Delete the given mailbox. /// -/// All emails from the given folder are definitely deleted. The -/// folder is also deleted after execution of the command. +/// All emails from the given mailbox are definitely deleted. The +/// mailbox is also deleted after execution of the command. #[derive(Debug, Parser)] -pub struct FolderDeleteCommand { +pub struct DeleteMailboxCommand { #[command(flatten)] - pub folder: FolderNameArg, - - #[command(flatten)] - pub account: AccountNameFlag, - - #[arg(long, short)] - pub yes: bool, + pub mailbox: MailboxNameArg, } -impl FolderDeleteCommand { - pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { - info!("executing delete folder command"); +impl DeleteMailboxCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + let (context, mut stream) = stream::connect(config)?; - let folder = &self.folder.name; + let mailbox = self.mailbox.name.try_into()?; - if !self.yes { - let confirm = format!("Do you really want to delete the folder {folder}"); - let confirm = format!("{confirm}? All emails will be definitely deleted."); + let mut arg = None; + let mut coroutine = ImapDelete::new(context, mailbox); - if !prompt::bool(confirm, false)? { - process::exit(0); - }; + loop { + match coroutine.resume(arg.take()) { + ImapDeleteResult::Io(io) => arg = Some(handle(&mut stream, io)?), + ImapDeleteResult::Ok { .. } => break, + ImapDeleteResult::Err { err, .. } => bail!(err), + } } - let (toml_account_config, account_config) = config - .clone() - .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { - c.account(name).ok() - })?; - - let backend = BackendBuilder::new( - Arc::new(toml_account_config), - Arc::new(account_config), - |builder| { - builder - .without_features() - .with_delete_folder(BackendFeatureSource::Context) - }, - ) - .without_sending_backend() - .build() - .await?; - - backend.delete_folder(folder).await?; - - printer.out(format!("Folder {folder} successfully deleted!\n")) + printer.out(Message::new("Mailbox successfully deleted")) } } diff --git a/src/imap/mailbox/command/expunge.rs b/src/imap/mailbox/command/expunge.rs index f095e4ae..91c68ee9 100644 --- a/src/imap/mailbox/command/expunge.rs +++ b/src/imap/mailbox/command/expunge.rs @@ -1,60 +1,52 @@ -use std::sync::Arc; - +use anyhow::{bail, Result}; use clap::Parser; -use color_eyre::Result; -use email::{ - backend::feature::BackendFeatureSource, config::Config, folder::expunge::ExpungeFolder, -}; -use pimalaya_tui::{ - himalaya::backend::BackendBuilder, - terminal::{cli::printer::Printer, config::TomlConfig as _}, -}; -use tracing::info; +use io_imap::coroutines::{expunge::*, select::*}; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; -use crate::{ - account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, -}; +use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream}; -/// Expunge the given folder. +/// Expunge the given mailbox. /// /// The concept of expunging is similar to the IMAP one: it definitely -/// deletes emails from the given folder that contain the "deleted" +/// deletes emails from the given mailbox that contain the "deleted" /// flag. #[derive(Debug, Parser)] -pub struct FolderExpungeCommand { +pub struct ExpungeMailboxCommand { #[command(flatten)] - pub folder: FolderNameArg, - - #[command(flatten)] - pub account: AccountNameFlag, + pub mailbox: MailboxNameArg, } -impl FolderExpungeCommand { - pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { - info!("executing expunge folder command"); +impl ExpungeMailboxCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + let (context, mut stream) = stream::connect(config)?; - let folder = &self.folder.name; - let (toml_account_config, account_config) = config - .clone() - .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { - c.account(name).ok() - })?; + let mailbox = self.mailbox.name.try_into()?; - let backend = BackendBuilder::new( - Arc::new(toml_account_config), - Arc::new(account_config), - |builder| { - builder - .without_features() - .with_expunge_folder(BackendFeatureSource::Context) - }, - ) - .without_sending_backend() - .build() - .await?; + // First, select the mailbox + let mut arg = None; + let mut coroutine = ImapSelect::new(context, mailbox); - backend.expunge_folder(folder).await?; + 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), + } + }; - printer.out(format!("Folder {folder} successfully expunged!\n")) + // Then, expunge it + let mut arg = None; + let mut coroutine = ImapExpunge::new(context); + + loop { + match coroutine.resume(arg.take()) { + ImapExpungeResult::Io(io) => arg = Some(handle(&mut stream, io)?), + ImapExpungeResult::Ok { .. } => break, + ImapExpungeResult::Err { err, .. } => bail!(err), + } + } + + printer.out(Message::new("Mailbox successfully expunged")) } } diff --git a/src/imap/mailbox/command/mod.rs b/src/imap/mailbox/command/mod.rs index 58350f60..1fa09c9c 100644 --- a/src/imap/mailbox/command/mod.rs +++ b/src/imap/mailbox/command/mod.rs @@ -1,14 +1,20 @@ -// mod add; -// mod delete; -// mod expunge; +pub mod create; +pub mod delete; +pub mod expunge; pub mod list; -// mod purge; +pub mod purge; use anyhow::Result; use clap::Subcommand; use pimalaya_toolbox::terminal::printer::Printer; -use crate::{config::ImapConfig, imap::mailbox::command::list::ListMailboxesCommand}; +use crate::{ + config::ImapConfig, + imap::mailbox::command::{ + create::CreateMailboxCommand, delete::DeleteMailboxCommand, + expunge::ExpungeMailboxCommand, list::ListMailboxesCommand, purge::PurgeMailboxCommand, + }, +}; /// Create, list and purge mailboxes. /// @@ -16,28 +22,23 @@ use crate::{config::ImapConfig, imap::mailbox::command::list::ListMailboxesComma /// manage them. #[derive(Debug, Subcommand)] pub enum MailboxCommand { - // #[command(visible_alias = "create", alias = "new")] - // Add(FolderAddCommand), + #[command(alias = "add", alias = "new")] + Create(CreateMailboxCommand), + #[command(alias = "remove", alias = "rm")] + Delete(DeleteMailboxCommand), + Expunge(ExpungeMailboxCommand), List(ListMailboxesCommand), - // #[command()] - // Expunge(FolderExpungeCommand), - - // #[command()] - // Purge(FolderPurgeCommand), - - // #[command(alias = "remove", alias = "rm")] - // Delete(FolderDeleteCommand), + Purge(PurgeMailboxCommand), } impl MailboxCommand { - #[allow(unused)] pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { match self { - // Self::Add(cmd) => cmd.execute(printer, config).await, + Self::Create(cmd) => cmd.execute(printer, config), + Self::Delete(cmd) => cmd.execute(printer, config), + Self::Expunge(cmd) => cmd.execute(printer, config), Self::List(cmd) => cmd.execute(printer, config), - // Self::Expunge(cmd) => cmd.execute(printer, config).await, - // Self::Purge(cmd) => cmd.execute(printer, config).await, - // Self::Delete(cmd) => cmd.execute(printer, config).await, + Self::Purge(cmd) => cmd.execute(printer, config), } } } diff --git a/src/imap/mailbox/command/purge.rs b/src/imap/mailbox/command/purge.rs index 4a185de4..e85ee6e2 100644 --- a/src/imap/mailbox/command/purge.rs +++ b/src/imap/mailbox/command/purge.rs @@ -1,70 +1,76 @@ -use std::{process, sync::Arc}; - +use anyhow::{bail, Result}; use clap::Parser; -use color_eyre::Result; -use email::{backend::feature::BackendFeatureSource, config::Config, folder::purge::PurgeFolder}; -use pimalaya_tui::{ - himalaya::backend::BackendBuilder, - terminal::{cli::printer::Printer, config::TomlConfig as _, prompt}, +use io_imap::{ + coroutines::{expunge::*, select::*, store::*}, + types::{ + flag::{Flag, StoreType}, + sequence::SequenceSet, + }, }; -use tracing::info; +use io_stream::runtimes::std::handle; +use pimalaya_toolbox::terminal::printer::{Message, Printer}; -use crate::{ - account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, -}; +use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream}; -/// Purge the given folder. +/// Purge the given mailbox. /// -/// All emails from the given folder are definitely deleted. The -/// purged folder will remain empty after execution of the command. +/// All emails from the given mailbox are definitely deleted. The +/// purged mailbox will remain empty after execution of the command. #[derive(Debug, Parser)] -pub struct FolderPurgeCommand { +pub struct PurgeMailboxCommand { #[command(flatten)] - pub folder: FolderNameArg, - - #[command(flatten)] - pub account: AccountNameFlag, - - #[arg(long, short)] - pub yes: bool, + pub mailbox: MailboxNameArg, } -impl FolderPurgeCommand { - pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { - info!("executing purge folder command"); +impl PurgeMailboxCommand { + pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> { + let (context, mut stream) = stream::connect(config)?; - let folder = &self.folder.name; + let mailbox = self.mailbox.name.try_into()?; - if !self.yes { - let confirm = format!("Do you really want to purge the folder {folder}"); - let confirm = format!("{confirm}? All emails will be definitely deleted."); + // First, select the mailbox + let mut arg = None; + let mut coroutine = ImapSelect::new(context, mailbox); - if !prompt::bool(confirm, false)? { - process::exit(0); - }; + 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), + } }; - let (toml_account_config, account_config) = config - .clone() - .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { - c.account(name).ok() - })?; + // Then, mark all messages as deleted (1:*) + let mut arg = None; + let sequence_set: SequenceSet = "1:*".try_into()?; + let mut coroutine = ImapStoreSilent::new( + context, + sequence_set, + StoreType::Add, + vec![Flag::Deleted], + false, + ); - let backend = BackendBuilder::new( - Arc::new(toml_account_config), - Arc::new(account_config), - |builder| { - builder - .without_features() - .with_purge_folder(BackendFeatureSource::Context) - }, - ) - .without_sending_backend() - .build() - .await?; + 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), + } + }; - backend.purge_folder(folder).await?; + // Finally, expunge the mailbox + let mut arg = None; + let mut coroutine = ImapExpunge::new(context); - printer.out(format!("Folder {folder} successfully purged!\n")) + loop { + match coroutine.resume(arg.take()) { + ImapExpungeResult::Io(io) => arg = Some(handle(&mut stream, io)?), + ImapExpungeResult::Ok { .. } => break, + ImapExpungeResult::Err { err, .. } => bail!(err), + } + } + + printer.out(Message::new("Mailbox successfully purged")) } } diff --git a/src/imap/mailbox/mod.rs b/src/imap/mailbox/mod.rs index 783d63ad..1a3a73a9 100644 --- a/src/imap/mailbox/mod.rs +++ b/src/imap/mailbox/mod.rs @@ -1,2 +1,2 @@ -// pub mod arg; +pub mod arg; pub mod command;