rename imap folder into mailbox

This commit is contained in:
Clément DOUIN
2026-03-02 11:45:27 +01:00
parent 9811619629
commit 0ec0159a28
16 changed files with 36 additions and 208 deletions
+26
View File
@@ -0,0 +1,26 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{account::Account, imap::mailbox::command::MailboxCommand};
/// IMAP CLI (requires `imap` cargo feature).
///
/// This command gives you access to the IMAP CLI API, and allows
/// you to manage IMAP mailboxes: list mailboxes, read messages,
/// add flags etc.
#[derive(Debug, Subcommand)]
#[command(rename_all = "lowercase")]
pub enum ImapCommand {
#[command(subcommand)]
#[command(aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
}
impl ImapCommand {
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
match self {
Self::Mailboxes(cmd) => cmd.execute(printer, account),
}
}
}
+1
View File
@@ -0,0 +1 @@
pub mod name;
+60
View File
@@ -0,0 +1,60 @@
use clap::Parser;
use email::folder::INBOX;
/// The optional folder 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 name: String,
}
impl Default for FolderNameOptionalFlag {
fn default() -> Self {
Self {
name: INBOX.to_owned(),
}
}
}
/// The optional folder 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 name: String,
}
impl Default for FolderNameOptionalArg {
fn default() -> Self {
Self {
name: INBOX.to_owned(),
}
}
}
/// The required folder name argument parser.
#[derive(Debug, Parser)]
pub struct FolderNameArg {
/// The name of the folder.
#[arg(name = "folder_name", value_name = "FOLDER")]
pub name: String,
}
/// The optional source folder 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 name: String,
}
/// The target folder name argument parser.
#[derive(Debug, Parser)]
pub struct TargetFolderNameArg {
/// The name of the target folder.
#[arg(name = "target_folder_name", value_name = "TARGET")]
pub name: String,
}
+61
View File
@@ -0,0 +1,61 @@
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"))
}
}
+73
View File
@@ -0,0 +1,73 @@
use std::{process, sync::Arc};
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 crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Delete the given folder.
///
/// All emails from the given folder are definitely deleted. The
/// folder is also deleted after execution of the command.
#[derive(Debug, Parser)]
pub struct FolderDeleteCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[command(flatten)]
pub account: AccountNameFlag,
#[arg(long, short)]
pub yes: bool,
}
impl FolderDeleteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing delete folder command");
let folder = &self.folder.name;
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.");
if !prompt::bool(confirm, false)? {
process::exit(0);
};
}
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"))
}
}
+60
View File
@@ -0,0 +1,60 @@
use std::sync::Arc;
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 crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Expunge the given folder.
///
/// The concept of expunging is similar to the IMAP one: it definitely
/// deletes emails from the given folder that contain the "deleted"
/// flag.
#[derive(Debug, Parser)]
pub struct FolderExpungeCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl FolderExpungeCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing expunge 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_expunge_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.expunge_folder(folder).await?;
printer.out(format!("Folder {folder} successfully expunged!\n"))
}
}
+123
View File
@@ -0,0 +1,123 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{
authenticate::*, authenticate_anonymous::*, authenticate_plain::*, list::*, login::*,
},
types::response::Capability,
};
use io_stream::runtimes::std::handle;
use log::warn;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{account::Account, sasl::SaslMechanism, stream};
/// List all mailboxes.
///
/// This command allows you to list all exsting mailboxes from your
/// IMAP account.
#[derive(Debug, Parser)]
pub struct ListMailboxesCommand {
// /// The maximum width the table should not exceed.
// ///
// /// This argument will force the table not to exceed the given
// /// width, in pixels. Columns may shrink with ellipsis in order to
// /// fit the width.
// #[arg(long = "max-width", short = 'w')]
// #[arg(name = "table_max_width", value_name = "PIXELS")]
// pub table_max_width: Option<u16>,
}
impl ListMailboxesCommand {
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
let imap = account.imap.unwrap();
let (mut context, mut stream) = if imap.tls.disable {
let port = imap.port.unwrap_or(143);
stream::tcp(imap.host, port)?
} else {
let port = imap.port.unwrap_or(if imap.starttls { 143 } else { 993 });
stream::rustls(imap.host, port, imap.starttls, imap.tls.cert)?
};
let ir = context.capability.contains(&Capability::SaslIr);
let mut candidates = vec![];
for mechanism in imap.sasl.mechanisms {
match mechanism {
SaslMechanism::Login => {
let Some(ref auth) = imap.sasl.login else {
warn!("missing SASL LOGIN configuration, skipping it");
continue;
};
let params = ImapLoginParams::new(&auth.username, auth.password.get()?)?;
candidates.push(ImapAuthenticateCandidate::Login(params));
}
SaslMechanism::Plain => {
let Some(ref auth) = imap.sasl.plain else {
warn!("missing SASL PLAIN configuration, skipping it");
continue;
};
let params = ImapAuthenticatePlainParams::new(
auth.authzid.as_ref(),
&auth.authcid,
auth.passwd.get()?,
ir,
);
candidates.push(ImapAuthenticateCandidate::Plain(params))
}
SaslMechanism::Anonymous => {
let msg = imap
.sasl
.anonymous
.as_ref()
.and_then(|auth| auth.message.as_ref());
let params = ImapAuthenticateAnonymousParams::new(msg, ir);
candidates.push(ImapAuthenticateCandidate::Anonymous(params))
}
}
}
let mut arg = None;
let mut coroutine = ImapAuthenticate::new(context, candidates);
loop {
match coroutine.resume(arg.take()) {
ImapAuthenticateResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapAuthenticateResult::Ok { context: c } => break context = c,
ImapAuthenticateResult::Err { err, .. } => bail!(err),
}
}
let mut arg = None;
let mut coroutine = ImapList::new(context, "".try_into().unwrap(), "*".try_into().unwrap());
let mailboxes = loop {
match coroutine.resume(arg.take()) {
ImapListResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapListResult::Ok(ok) => break ok.mailboxes,
ImapListResult::Err(err) => bail!(err),
}
};
println!("mailboxes: {mailboxes:#?}");
// TODO: list folders
// let folders = Folders::from(backend.list_folders().await?);
// let table = FoldersTable::from(folders)
// .with_some_width(self.table_max_width)
// .with_some_preset(toml_account_config.folder_list_table_preset())
// .with_some_name_color(toml_account_config.folder_list_table_name_color())
// .with_some_desc_color(toml_account_config.folder_list_table_desc_color());
// printer.out(table)?;
Ok(())
}
}
+43
View File
@@ -0,0 +1,43 @@
// mod add;
// mod delete;
// mod expunge;
pub mod list;
// mod purge;
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{account::Account, imap::mailbox::command::list::ListMailboxesCommand};
/// Create, list and purge mailboxes.
///
/// A mailbox is a message container. This subcommand allows you to
/// manage them.
#[derive(Debug, Subcommand)]
pub enum MailboxCommand {
// #[command(visible_alias = "create", alias = "new")]
// Add(FolderAddCommand),
List(ListMailboxesCommand),
// #[command()]
// Expunge(FolderExpungeCommand),
// #[command()]
// Purge(FolderPurgeCommand),
// #[command(alias = "remove", alias = "rm")]
// Delete(FolderDeleteCommand),
}
impl MailboxCommand {
#[allow(unused)]
pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
match self {
// Self::Add(cmd) => cmd.execute(printer, config).await,
Self::List(cmd) => cmd.execute(printer, account),
// Self::Expunge(cmd) => cmd.execute(printer, config).await,
// Self::Purge(cmd) => cmd.execute(printer, config).await,
// Self::Delete(cmd) => cmd.execute(printer, config).await,
}
}
}
+70
View File
@@ -0,0 +1,70 @@
use std::{process, sync::Arc};
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 tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Purge the given folder.
///
/// All emails from the given folder are definitely deleted. The
/// purged folder will remain empty after execution of the command.
#[derive(Debug, Parser)]
pub struct FolderPurgeCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[command(flatten)]
pub account: AccountNameFlag,
#[arg(long, short)]
pub yes: bool,
}
impl FolderPurgeCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing purge folder command");
let folder = &self.folder.name;
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.");
if !prompt::bool(confirm, false)? {
process::exit(0);
};
};
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_purge_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.purge_folder(folder).await?;
printer.out(format!("Folder {folder} successfully purged!\n"))
}
}
+2
View File
@@ -0,0 +1,2 @@
// pub mod arg;
pub mod command;
+2
View File
@@ -0,0 +1,2 @@
pub mod command;
pub mod mailbox;