add more mailbox useful commands

This commit is contained in:
Clément DOUIN
2026-03-03 22:01:23 +01:00
parent 715fdb002a
commit 1d72e59c82
8 changed files with 398 additions and 20 deletions
+34
View File
@@ -0,0 +1,34 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::coroutines::close::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::stream};
/// Close a mailbox.
///
/// This command first selects the given mailbox, then closes it.
/// CLOSE permanently removes all messages with the \Deleted flag
/// and returns to the authenticated state.
#[derive(Debug, Parser)]
pub struct CloseMailboxCommand;
impl CloseMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mut arg = None;
let mut close_coroutine = ImapClose::new(context);
loop {
match close_coroutine.resume(arg.take()) {
ImapCloseResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapCloseResult::Ok { .. } => break,
ImapCloseResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Mailbox successfully closed"))
}
}
+30 -19
View File
@@ -4,41 +4,52 @@ use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use crossterm::style::Color;
use io_imap::coroutines::list::*;
use io_imap::coroutines::{list::*, lsub::*};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::stream};
/// List all mailboxes.
/// List mailboxes.
///
/// This command allows you to list all exsting mailboxes from your
/// IMAP account.
/// This command allows you to list mailboxes from your IMAP account.
/// By default, only subscribed mailboxes are listed. Use --all to
/// list all mailboxes.
#[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>,
/// List all mailboxes, not just subscribed ones.
#[arg(short = 'A', long)]
pub all: bool,
}
impl ListMailboxesCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mut arg = None;
let mut coroutine = ImapList::new(context, "".try_into().unwrap(), "*".try_into().unwrap());
let mailboxes = if self.all {
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 { mailboxes, .. } => break mailboxes,
ImapListResult::Err { err, .. } => bail!(err),
loop {
match coroutine.resume(arg.take()) {
ImapListResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapListResult::Ok { mailboxes, .. } => break mailboxes,
ImapListResult::Err { err, .. } => bail!(err),
}
}
} else {
let mut arg = None;
let mut coroutine =
ImapLsub::new(context, "".try_into().unwrap(), "*".try_into().unwrap());
loop {
match coroutine.resume(arg.take()) {
ImapLsubResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapLsubResult::Ok { mailboxes, .. } => break mailboxes,
ImapLsubResult::Err { err, .. } => bail!(err),
}
}
};
+22 -1
View File
@@ -1,8 +1,14 @@
pub mod close;
pub mod create;
pub mod delete;
pub mod expunge;
pub mod list;
pub mod purge;
pub mod rename;
pub mod status;
pub mod subscribe;
pub mod unselect;
pub mod unsubscribe;
use anyhow::Result;
use clap::Subcommand;
@@ -11,8 +17,11 @@ use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::mailbox::command::{
create::CreateMailboxCommand, delete::DeleteMailboxCommand,
close::CloseMailboxCommand, create::CreateMailboxCommand, delete::DeleteMailboxCommand,
expunge::ExpungeMailboxCommand, list::ListMailboxesCommand, purge::PurgeMailboxCommand,
rename::RenameMailboxCommand, status::StatusMailboxCommand,
subscribe::SubscribeMailboxCommand, unselect::UnselectMailboxCommand,
unsubscribe::UnsubscribeMailboxCommand,
},
};
@@ -22,6 +31,7 @@ use crate::{
/// manage them.
#[derive(Debug, Subcommand)]
pub enum MailboxCommand {
Close(CloseMailboxCommand),
#[command(alias = "add", alias = "new")]
Create(CreateMailboxCommand),
#[command(alias = "remove", alias = "rm")]
@@ -29,16 +39,27 @@ pub enum MailboxCommand {
Expunge(ExpungeMailboxCommand),
List(ListMailboxesCommand),
Purge(PurgeMailboxCommand),
Rename(RenameMailboxCommand),
Status(StatusMailboxCommand),
Subscribe(SubscribeMailboxCommand),
Unselect(UnselectMailboxCommand),
Unsubscribe(UnsubscribeMailboxCommand),
}
impl MailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::Close(cmd) => cmd.execute(printer, config),
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::Purge(cmd) => cmd.execute(printer, config),
Self::Rename(cmd) => cmd.execute(printer, config),
Self::Status(cmd) => cmd.execute(printer, config),
Self::Subscribe(cmd) => cmd.execute(printer, config),
Self::Unselect(cmd) => cmd.execute(printer, config),
Self::Unsubscribe(cmd) => cmd.execute(printer, config),
}
}
}
+45
View File
@@ -0,0 +1,45 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::coroutines::rename::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{
config::ImapConfig,
imap::mailbox::arg::name::{MailboxNameArg, TargetMailboxNameArg},
imap::stream,
};
/// Rename a mailbox.
///
/// This command renames an existing mailbox to a new name.
#[derive(Debug, Parser)]
pub struct RenameMailboxCommand {
#[command(flatten)]
pub from: MailboxNameArg,
#[command(flatten)]
pub to: TargetMailboxNameArg,
}
impl RenameMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let from = self.from.name.try_into()?;
let to = self.to.name.try_into()?;
let mut arg = None;
let mut coroutine = ImapRename::new(context, from, to);
loop {
match coroutine.resume(arg.take()) {
ImapRenameResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapRenameResult::Ok { .. } => break,
ImapRenameResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Mailbox successfully renamed"))
}
}
+140
View File
@@ -0,0 +1,140 @@
use std::fmt;
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use io_imap::coroutines::status::*;
use io_imap::types::status::{StatusDataItem, StatusDataItemName};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
/// Get the status of a mailbox.
///
/// This command displays status information about a mailbox,
/// including message counts and UID values.
#[derive(Debug, Parser)]
pub struct StatusMailboxCommand {
#[command(flatten)]
pub mailbox: MailboxNameArg,
}
impl StatusMailboxCommand {
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 item_names = vec![
StatusDataItemName::Messages,
StatusDataItemName::Recent,
StatusDataItemName::Unseen,
StatusDataItemName::UidNext,
StatusDataItemName::UidValidity,
];
let mut arg = None;
let mut coroutine = ImapStatus::new(context, mailbox, item_names);
let items = loop {
match coroutine.resume(arg.take()) {
ImapStatusResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapStatusResult::Ok { items, .. } => break items,
ImapStatusResult::Err { err, .. } => bail!(err),
}
};
let table = MailboxStatusTable::from(items);
printer.out(table)?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize)]
pub struct MailboxStatus {
pub messages: Option<u32>,
pub recent: Option<u32>,
pub unseen: Option<u32>,
pub uidnext: Option<u32>,
pub uidvalidity: Option<u32>,
}
impl From<Vec<StatusDataItem>> for MailboxStatus {
fn from(items: Vec<StatusDataItem>) -> Self {
let mut status = MailboxStatus {
messages: None,
recent: None,
unseen: None,
uidnext: None,
uidvalidity: None,
};
for item in items {
match item {
StatusDataItem::Messages(n) => status.messages = Some(n),
StatusDataItem::Recent(n) => status.recent = Some(n),
StatusDataItem::Unseen(n) => status.unseen = Some(n),
StatusDataItem::UidNext(n) => status.uidnext = Some(n.get()),
StatusDataItem::UidValidity(n) => status.uidvalidity = Some(n.get()),
_ => {}
}
}
status
}
}
pub struct MailboxStatusTable {
status: MailboxStatus,
}
impl From<Vec<StatusDataItem>> for MailboxStatusTable {
fn from(items: Vec<StatusDataItem>) -> Self {
Self {
status: MailboxStatus::from(items),
}
}
}
impl fmt::Display for MailboxStatusTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(presets::ASCII_MARKDOWN)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([Cell::new("ATTRIBUTE"), Cell::new("VALUE")]));
if let Some(messages) = self.status.messages {
table.add_row(Row::from([Cell::new("Messages"), Cell::new(messages)]));
}
if let Some(recent) = self.status.recent {
table.add_row(Row::from([Cell::new("Recent"), Cell::new(recent)]));
}
if let Some(unseen) = self.status.unseen {
table.add_row(Row::from([Cell::new("Unseen"), Cell::new(unseen)]));
}
if let Some(uidnext) = self.status.uidnext {
table.add_row(Row::from([Cell::new("UIDNext"), Cell::new(uidnext)]));
}
if let Some(uidvalidity) = self.status.uidvalidity {
table.add_row(Row::from([
Cell::new("UIDValidity"),
Cell::new(uidvalidity),
]));
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
}
}
impl Serialize for MailboxStatusTable {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.status.serialize(serializer)
}
}
+38
View File
@@ -0,0 +1,38 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::coroutines::subscribe::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
/// Subscribe to the given mailbox.
///
/// This command subscribes to a mailbox, making it appear in the
/// list of subscribed mailboxes.
#[derive(Debug, Parser)]
pub struct SubscribeMailboxCommand {
#[command(flatten)]
pub mailbox: MailboxNameArg,
}
impl SubscribeMailboxCommand {
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 = ImapSubscribe::new(context, mailbox);
loop {
match coroutine.resume(arg.take()) {
ImapSubscribeResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapSubscribeResult::Ok { .. } => break,
ImapSubscribeResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Mailbox successfully subscribed"))
}
}
+51
View File
@@ -0,0 +1,51 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::coroutines::{select::*, unselect::*};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
/// Unselect a mailbox.
///
/// This command first selects the given mailbox, then unselects it.
/// Unlike CLOSE, UNSELECT does not expunge deleted messages.
#[derive(Debug, Parser)]
pub struct UnselectMailboxCommand {
#[command(flatten)]
pub mailbox: MailboxNameArg,
}
impl UnselectMailboxCommand {
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()?;
// First SELECT the mailbox
let mut arg = None;
let mut select_coroutine = ImapSelect::new(context, mailbox);
let context = loop {
match select_coroutine.resume(arg.take()) {
ImapSelectResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { context, .. } => break context,
ImapSelectResult::Err { err, .. } => bail!(err),
}
};
// Then UNSELECT
let mut arg = None;
let mut unselect_coroutine = ImapUnselect::new(context);
loop {
match unselect_coroutine.resume(arg.take()) {
ImapUnselectResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapUnselectResult::Ok { .. } => break,
ImapUnselectResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Mailbox successfully unselected"))
}
}
+38
View File
@@ -0,0 +1,38 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::coroutines::unsubscribe::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
/// Unsubscribe from the given mailbox.
///
/// This command unsubscribes from a mailbox, removing it from the
/// list of subscribed mailboxes.
#[derive(Debug, Parser)]
pub struct UnsubscribeMailboxCommand {
#[command(flatten)]
pub mailbox: MailboxNameArg,
}
impl UnsubscribeMailboxCommand {
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 = ImapUnsubscribe::new(context, mailbox);
loop {
match coroutine.resume(arg.take()) {
ImapUnsubscribeResult::Io(io) => arg = Some(handle(&mut stream, io)?),
ImapUnsubscribeResult::Ok { .. } => break,
ImapUnsubscribeResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Mailbox successfully unsubscribed"))
}
}