add flag commands

This commit is contained in:
Clément DOUIN
2026-03-03 22:49:43 +01:00
parent e1854b6045
commit f993bb58c9
8 changed files with 408 additions and 1 deletions
+7 -1
View File
@@ -2,7 +2,10 @@ use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{config::ImapConfig, imap::mailbox::command::MailboxCommand};
use crate::{
config::ImapConfig,
imap::{flag::command::FlagCommand, mailbox::command::MailboxCommand},
};
/// IMAP CLI (requires `imap` cargo feature).
///
@@ -12,6 +15,8 @@ use crate::{config::ImapConfig, imap::mailbox::command::MailboxCommand};
#[derive(Debug, Subcommand)]
#[command(rename_all = "lowercase")]
pub enum ImapCommand {
#[command(subcommand)]
Flags(FlagCommand),
#[command(subcommand)]
#[command(aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
@@ -20,6 +25,7 @@ pub enum ImapCommand {
impl ImapCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::Flags(cmd) => cmd.execute(printer, config),
Self::Mailboxes(cmd) => cmd.execute(printer, config),
}
}
+80
View File
@@ -0,0 +1,80 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{select::*, store::*},
types::{
flag::{Flag, StoreType},
IntoStatic,
},
};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
/// Add flags to messages.
///
/// This command adds the specified flags to messages identified by
/// the given sequence set.
#[derive(Debug, Parser)]
pub struct AddFlagsCommand {
#[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 flags to add (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>,
/// Use UID STORE instead of STORE.
#[arg(long)]
pub uid: bool,
}
impl AddFlagsCommand {
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 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 flags
let flags: Vec<Flag<'static>> = self
.flags
.iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None;
let mut coroutine =
ImapStoreSilent::new(context, sequence_set, StoreType::Add, flags, self.uid);
loop {
match coroutine.resume(arg.take()) {
ImapStoreSilentResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapStoreSilentResult::Ok { .. } => break,
ImapStoreSilentResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Flag(s) successfully added"))
}
}
+119
View File
@@ -0,0 +1,119 @@
use std::{collections::BTreeMap, fmt};
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use io_imap::{
coroutines::select::*,
types::flag::{Flag, FlagPerm},
};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalArg, imap::stream};
/// List available flags for a mailbox.
///
/// This command displays the flags and permanent flags that are
/// available in the given mailbox. These flags come from the SELECT
/// response.
#[derive(Debug, Parser)]
pub struct ListFlagsCommand {
#[command(flatten)]
pub mailbox: MailboxNameOptionalArg,
}
impl ListFlagsCommand {
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 = ImapSelect::new(context, mailbox);
let (flags, permanent_flags) = loop {
match coroutine.resume(arg.take()) {
ImapSelectResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapSelectResult::Ok { data, .. } => {
break (
data.flags.unwrap_or_default(),
data.permanent_flags.unwrap_or_default(),
)
}
ImapSelectResult::Err { err, .. } => bail!(err),
}
};
let table = FlagsTable { flags, permanent_flags };
printer.out(table)?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize)]
pub struct FlagEntry {
pub name: String,
pub permanent: bool,
}
pub struct FlagsTable {
flags: Vec<Flag<'static>>,
permanent_flags: Vec<FlagPerm<'static>>,
}
impl FlagsTable {
fn build_entries(&self) -> Vec<FlagEntry> {
let mut entries: BTreeMap<String, bool> = BTreeMap::new();
// Add flags
for flag in &self.flags {
entries.entry(flag.to_string()).or_insert(false);
}
// Mark permanent flags
for flag in &self.permanent_flags {
let name = match flag {
FlagPerm::Flag(f) => f.to_string(),
FlagPerm::Asterisk => "\\*".to_string(),
};
entries.insert(name, true);
}
entries
.into_iter()
.map(|(name, permanent)| FlagEntry { name, permanent })
.collect()
}
}
impl fmt::Display for FlagsTable {
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("FLAG"), Cell::new("PERMANENT")]));
for entry in self.build_entries() {
table.add_row(Row::from([
Cell::new(&entry.name),
Cell::new(if entry.permanent { "true" } else { "" }),
]));
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
}
}
impl Serialize for FlagsTable {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.build_entries().serialize(serializer)
}
}
+40
View File
@@ -0,0 +1,40 @@
pub mod add;
pub mod list;
pub mod remove;
pub mod set;
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::flag::command::{
add::AddFlagsCommand, list::ListFlagsCommand, remove::RemoveFlagsCommand,
set::SetFlagsCommand,
},
};
/// Manage message flags.
///
/// A flag is a label attached to a message. This subcommand allows
/// you to manage them: list available flags, add flags to messages,
/// remove flags from messages, etc.
#[derive(Debug, Subcommand)]
pub enum FlagCommand {
List(ListFlagsCommand),
Add(AddFlagsCommand),
Set(SetFlagsCommand),
Remove(RemoveFlagsCommand),
}
impl FlagCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config),
Self::Add(cmd) => cmd.execute(printer, config),
Self::Set(cmd) => cmd.execute(printer, config),
Self::Remove(cmd) => cmd.execute(printer, config),
}
}
}
+80
View File
@@ -0,0 +1,80 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{select::*, store::*},
types::{
flag::{Flag, StoreType},
IntoStatic,
},
};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
/// Remove flags from messages.
///
/// This command removes the specified flags from messages identified
/// by the given sequence set.
#[derive(Debug, Parser)]
pub struct RemoveFlagsCommand {
#[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 flags to remove (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>,
/// Use UID STORE instead of STORE.
#[arg(long)]
pub uid: bool,
}
impl RemoveFlagsCommand {
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 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 flags
let flags: Vec<Flag<'static>> = self
.flags
.iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None;
let mut coroutine =
ImapStoreSilent::new(context, sequence_set, StoreType::Remove, flags, self.uid);
loop {
match coroutine.resume(arg.take()) {
ImapStoreSilentResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapStoreSilentResult::Ok { .. } => break,
ImapStoreSilentResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Flag(s) successfully removed"))
}
}
+80
View File
@@ -0,0 +1,80 @@
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{select::*, store::*},
types::{
flag::{Flag, StoreType},
IntoStatic,
},
};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
/// Set flags on messages (replacing existing flags).
///
/// This command replaces all existing flags on messages identified by
/// the given sequence set with the specified flags.
#[derive(Debug, Parser)]
pub struct SetFlagsCommand {
#[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 flags to set (e.g., "\\Seen", "\\Flagged").
#[arg(short, long, required = true, num_args = 1..)]
pub flags: Vec<String>,
/// Use UID STORE instead of STORE.
#[arg(long)]
pub uid: bool,
}
impl SetFlagsCommand {
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 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 flags
let flags: Vec<Flag<'static>> = self
.flags
.iter()
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
// Parse sequence set
let sequence_set = self.sequence_set.as_str().try_into()?;
// Store flags
let mut arg = None;
let mut coroutine =
ImapStoreSilent::new(context, sequence_set, StoreType::Replace, flags, self.uid);
loop {
match coroutine.resume(arg.take()) {
ImapStoreSilentResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapStoreSilentResult::Ok { .. } => break,
ImapStoreSilentResult::Err { err, .. } => bail!(err),
}
}
printer.out(Message::new("Flag(s) successfully set"))
}
}
+1
View File
@@ -0,0 +1 @@
pub mod command;
+1
View File
@@ -1,3 +1,4 @@
pub mod command;
pub mod flag;
pub mod mailbox;
pub mod stream;