use crate::libs::{
args,
base_config::BaseConfig,
imap::Imap,
render::{new_renderer, Renderer},
};
use clap::Args;
use derive_more::Display;
use exn::{Result, ResultExt as _};
use imap_proto::NameAttribute;
use regex::Regex;
#[derive(Debug, Display)]
pub struct ImapListCommandError(String);
impl std::error::Error for ImapListCommandError {}
#[derive(Args, Debug, Clone)]
#[command(
about = "List mailboxes",
long_about = "This command allows to list mailboxes."
)]
pub struct List {
#[clap(flatten)]
config: args::Generic,
#[arg(long)]
pub include_re: Vec<Regex>,
#[arg(long)]
pub exclude_re: Vec<Regex>,
#[arg(long)]
pub no_select: bool,
#[clap(default_value = Some("*"))]
pattern: Option<String>,
reference: Option<String>,
}
impl List {
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self), err(level = "info"))
)]
pub fn execute(&self) -> Result<(), ImapListCommandError> {
let config =
BaseConfig::new(&self.config).or_raise(|| ImapListCommandError("config".into()))?;
#[cfg(feature = "tracing")]
tracing::trace!(?config);
let mut imap: Imap<()> =
Imap::connect_base(&config).or_raise(|| ImapListCommandError("connect".into()))?;
#[expect(
clippy::literal_string_with_formatting_args,
reason = "We need it for later"
)]
let mut renderer = new_renderer("Mailbox List", "{0:<42} {1}", &["Mailbox", "Attributes"])
.or_raise(|| ImapListCommandError("new renderer".to_owned()))?;
self.run(&mut imap, &mut renderer)
}
fn run(
&self,
imap: &mut Imap<()>,
renderer: &mut Box<dyn Renderer>,
) -> Result<(), ImapListCommandError> {
for mailbox in imap
.session
.list(self.reference.as_deref(), self.pattern.as_deref())
.or_raise(|| {
ImapListCommandError(format!(
"imap list failed with ref:{:?} and pattern:{:?}",
self.reference, self.pattern
))
})?
.iter()
.filter(|mbx| self.no_select || !mbx.attributes().contains(&NameAttribute::NoSelect))
.filter(|mbx| {
if self.include_re.is_empty() {
true
} else {
self.include_re.iter().any(|re| re.is_match(mbx.name()))
}
})
.filter(|mbx| {
if self.exclude_re.is_empty() {
true
} else {
self.exclude_re.iter().all(|re| !re.is_match(mbx.name()))
}
})
{
renderer
.add_row(&[&mailbox.name(), &format!("{:?}", mailbox.attributes())])
.or_raise(|| ImapListCommandError("renderer add row".to_owned()))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::expect_used, reason = "tests")]
use super::*;
use crate::test_helpers::{MockExchange, MockServer};
fn test_base() -> BaseConfig {
BaseConfig::new(&args::Generic {
server: Some("127.0.0.1".to_owned()),
username: Some("test".to_owned()),
password: Some("test".to_owned()),
..Default::default()
})
.expect("test base config")
}
fn default_list() -> List {
List {
config: args::Generic {
server: Some("127.0.0.1".to_owned()),
username: Some("test".to_owned()),
password: Some("test".to_owned()),
..Default::default()
},
include_re: vec![],
exclude_re: vec![],
no_select: false,
pattern: Some("*".to_owned()),
reference: None,
}
}
#[test]
fn list_returns_all_regular_mailboxes() {
let server = MockServer::start(
&[],
vec![MockExchange::ok(vec![
"* LIST () \"/\" INBOX\r\n".into(),
"* LIST () \"/\" Sent\r\n".into(),
])],
);
let base = test_base();
let mut imap: Imap<()> = Imap::connect_base_on_port(&base, server.port).expect("connect");
let cmd = default_list();
let mut renderer = new_renderer("test", "{0}", &["col"]).expect("renderer");
let result = cmd.run(&mut imap, &mut renderer);
drop(imap);
server.join();
assert!(result.is_ok(), "expected Ok, got: {result:?}");
}
#[test]
fn list_excludes_noselect_by_default() {
let server = MockServer::start(
&[],
vec![MockExchange::ok(vec![
"* LIST (\\Noselect) \"/\" [Gmail]\r\n".into(),
"* LIST () \"/\" INBOX\r\n".into(),
])],
);
let base = test_base();
let mut imap: Imap<()> = Imap::connect_base_on_port(&base, server.port).expect("connect");
let cmd = default_list();
let mut renderer = new_renderer("test", "{0}", &["col"]).expect("renderer");
let result = cmd.run(&mut imap, &mut renderer);
drop(imap);
server.join();
assert!(result.is_ok(), "expected Ok, got: {result:?}");
}
#[test]
fn list_no_select_flag_includes_noselect_folders() {
let server = MockServer::start(
&[],
vec![MockExchange::ok(vec![
"* LIST (\\Noselect) \"/\" [Gmail]\r\n".into(),
"* LIST () \"/\" INBOX\r\n".into(),
])],
);
let base = test_base();
let mut imap: Imap<()> = Imap::connect_base_on_port(&base, server.port).expect("connect");
let mut cmd = default_list();
cmd.no_select = true;
let mut renderer = new_renderer("test", "{0}", &["col"]).expect("renderer");
let result = cmd.run(&mut imap, &mut renderer);
drop(imap);
server.join();
assert!(result.is_ok(), "expected Ok, got: {result:?}");
}
#[test]
fn list_include_re_filters_mailboxes() {
let server = MockServer::start(
&[],
vec![MockExchange::ok(vec![
"* LIST () \"/\" INBOX\r\n".into(),
"* LIST () \"/\" Sent\r\n".into(),
])],
);
let base = test_base();
let mut imap: Imap<()> = Imap::connect_base_on_port(&base, server.port).expect("connect");
let mut cmd = default_list();
#[expect(clippy::trivial_regex, reason = "il faut une re")]
{
cmd.include_re = vec![Regex::new("^INBOX$").expect("valid regex")];
}
let mut renderer = new_renderer("test", "{0}", &["col"]).expect("renderer");
let result = cmd.run(&mut imap, &mut renderer);
drop(imap);
server.join();
assert!(result.is_ok(), "expected Ok, got: {result:?}");
}
}