clean unused config, add id command

This commit is contained in:
Clément DOUIN
2026-03-09 16:02:01 +01:00
parent 44d2690d59
commit eb6b721ba6
11 changed files with 207 additions and 112 deletions
+17 -24
View File
@@ -8,41 +8,34 @@ use dirs::download_dir;
#[derive(Debug)]
pub struct Account<B> {
pub backend: B,
pub email: String,
pub display_name: Option<String>,
pub signature: String,
pub downloads_dir: PathBuf,
pub table_preset: &'static str,
pub table_preset: String,
}
impl<B> Account<B> {
pub fn new(config: Config, account_config: AccountConfig, backend: B) -> Result<Self> {
Ok(Self {
backend,
email: account_config.email,
display_name: account_config.display_name.or(config.display_name),
signature: match account_config.signature.or(config.signature) {
None => String::new(),
Some(ref signature) => {
account_config
.signature_delim
.or(config.signature_delim)
.unwrap_or(String::from("-- \n"))
+ signature
}
},
downloads_dir: match account_config
downloads_dir: account_config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
{
Some(dir) => PathBuf::from(shellexpand::full(dir)?.to_string()),
None => download_dir().unwrap_or_else(temp_dir),
},
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or(config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string())))
.or(download_dir())
.unwrap_or_else(temp_dir),
table_preset: presets::UTF8_FULL_CONDENSED,
table_preset: config
.table_preset
.or(account_config.table_preset)
.unwrap_or(presets::UTF8_FULL_CONDENSED.to_string()),
})
}
}
+4 -34
View File
@@ -12,11 +12,8 @@ use url::Url;
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Config {
pub display_name: Option<String>,
pub signature: Option<String>,
pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>,
pub accounts: HashMap<String, AccountConfig>,
}
@@ -48,12 +45,8 @@ pub struct AccountConfig {
#[serde(default)]
pub default: bool,
#[serde(deserialize_with = "shell_expanded_string")]
pub email: String,
pub display_name: Option<String>,
pub signature: Option<String>,
pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>,
pub imap: Option<ImapConfig>,
pub smtp: Option<SmtpConfig>,
@@ -64,36 +57,12 @@ pub struct AccountConfig {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapConfig {
pub url: Url,
#[serde(default)]
pub tls: TlsConfig,
#[serde(default)]
pub starttls: bool,
#[serde(default)]
pub sasl: SaslConfig,
#[serde(default)]
pub ext: ImapExtensionsConfig,
}
/// IMAP extensions configuration.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapExtensionsConfig {
#[serde(default)]
id: ImapIdExtensionConfig,
}
/// IMAP ID configuration.
///
/// https://www.rfc-editor.org/rfc/rfc2971.html
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapIdExtensionConfig {
/// Automatically sends the ID command straight after
/// authentication.
#[serde(default)]
send_after_auth: bool,
}
/// SMTP configuration.
@@ -101,7 +70,6 @@ pub struct ImapIdExtensionConfig {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SmtpConfig {
pub url: Url,
#[serde(default)]
pub tls: TlsConfig,
#[serde(default)]
@@ -171,6 +139,7 @@ pub enum SaslMechanismConfig {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslLoginConfig {
#[serde(deserialize_with = "shell_expanded_string")]
pub username: String,
pub password: SecretConfig,
}
@@ -180,6 +149,7 @@ pub struct SaslLoginConfig {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslPlainConfig {
pub authzid: Option<String>,
#[serde(deserialize_with = "shell_expanded_string")]
pub authcid: String,
pub passwd: SecretConfig,
}
+3 -1
View File
@@ -4,7 +4,7 @@ use pimalaya_toolbox::terminal::printer::Printer;
use crate::imap::{
account::ImapAccount, envelope::command::EnvelopeCommand, flag::command::FlagCommand,
mailbox::command::MailboxCommand, message::command::MessageCommand,
id::IdCommand, mailbox::command::MailboxCommand, message::command::MessageCommand,
};
/// IMAP CLI (requires `imap` cargo feature).
@@ -25,6 +25,7 @@ pub enum ImapCommand {
#[command(subcommand)]
#[command(aliases = ["msgs", "msg"])]
Messages(MessageCommand),
Id(IdCommand),
}
impl ImapCommand {
@@ -32,6 +33,7 @@ impl ImapCommand {
match self {
Self::Envelopes(cmd) => cmd.exec(printer, account),
Self::Flags(cmd) => cmd.exec(printer, account),
Self::Id(cmd) => cmd.exec(printer, account),
Self::Mailboxes(cmd) => cmd.exec(printer, account),
Self::Messages(cmd) => cmd.exec(printer, account),
}
+5 -8
View File
@@ -2,14 +2,11 @@ use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::{
account::ImapAccount,
envelope::{
get::GetEnvelopeCommand, list::ListEnvelopesCommand, search::SearchEnvelopesCommand,
sort::SortEnvelopesCommand, thread::ThreadEnvelopesCommand,
},
use crate::imap::{
account::ImapAccount,
envelope::{
get::GetEnvelopeCommand, list::ListEnvelopesCommand, search::SearchEnvelopesCommand,
sort::SortEnvelopesCommand, thread::ThreadEnvelopesCommand,
},
};
+5 -8
View File
@@ -2,14 +2,11 @@ use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::{
account::ImapAccount,
flag::{
add::AddFlagsCommand, list::ListFlagsCommand, remove::RemoveFlagsCommand,
set::SetFlagsCommand,
},
use crate::imap::{
account::ImapAccount,
flag::{
add::AddFlagsCommand, list::ListFlagsCommand, remove::RemoveFlagsCommand,
set::SetFlagsCommand,
},
};
+145
View File
@@ -0,0 +1,145 @@
use std::{collections::HashMap, fmt};
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{Cell, Row, Table};
use io_imap::{
coroutines::id::*,
types::{
core::{IString, NString},
IntoStatic,
},
};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
use crate::imap::{account::ImapAccount, stream};
/// Get information about the IMAP server.
///
/// This command allows you to exchange parameters with the IMAP
/// server accordingly to the [RFC 2971]. Some providers like mail.qq
/// enforce sending ID command before selecting a mailbox.
///
/// [RFC 2971]: https://www.rfc-editor.org/rfc/rfc2971.html
#[derive(Debug, Parser)]
pub struct IdCommand {
#[arg(short, long, num_args = 1..)]
#[arg(value_name = "KEY:VAL", value_parser = parameter_parser)]
parameter: Option<Vec<(IString<'static>, NString<'static>)>>,
}
impl IdCommand {
pub fn exec(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let (context, mut stream) = stream::connect(account.backend)?;
let mut params = HashMap::new();
params.extend([
(
IString::try_from("name").unwrap(),
NString::try_from(env!("CARGO_PKG_NAME")).unwrap(),
),
(
IString::try_from("version").unwrap(),
NString::try_from(env!("CARGO_PKG_VERSION")).unwrap(),
),
(
IString::try_from("vendor").unwrap(),
NString::try_from("Pimalaya").unwrap(),
),
(
IString::try_from("support-url").unwrap(),
NString::try_from("https://github.com/pimalaya/himalaya").unwrap(),
),
]);
if let Some(more) = self.parameter {
params.extend(more);
}
let mut arg = None;
let mut coroutine = ImapId::new(context, Some(params.into_iter().collect()));
let params = loop {
match coroutine.resume(arg.take()) {
ImapIdResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapIdResult::Ok { server_id, .. } => break server_id,
ImapIdResult::Err { err, .. } => bail!(err),
}
};
let table = ServerIdTable {
preset: account.table_preset,
server_id: params
.unwrap_or_default()
.into_iter()
.map(|(key, val)| {
(
String::from_utf8(key.into_inner().into_owned()).unwrap(),
match val.into_option() {
Some(val) => Some(String::from_utf8(val.into_owned()).unwrap()),
None => None,
},
)
})
.collect(),
};
printer.out(table)
}
}
fn parameter_parser(param: &str) -> Result<(IString<'static>, NString<'static>), String> {
let Some((key, val)) = param.split_once(':') else {
return Err(format!("Invalid parameter `{param}`: missing `:`"));
};
let Ok(ikey) = IString::try_from(key.trim()) else {
return Err(format!("Invalid parameter key `{key}`"));
};
let nval = if val.trim().is_empty() {
NString::NIL
} else {
let Ok(nval) = NString::try_from(val.trim()) else {
return Err(format!("Invalid parameter value `{val}` for `{key}`"));
};
nval
};
Ok((ikey.into_static(), nval.into_static()))
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ServerIdTable {
#[serde(skip)]
pub preset: String,
pub server_id: HashMap<String, Option<String>>,
}
impl fmt::Display for ServerIdTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(&self.preset)
.set_header(Row::from([Cell::new("PARAMETER"), Cell::new("VALUE")]));
for (key, val) in &self.server_id {
table.add_row(Row::from([
Cell::new(key),
match val {
Some(val) => Cell::new(val),
None => Cell::new(""),
},
]));
}
writeln!(f)?;
writeln!(f, "{table}")
}
}
+8 -11
View File
@@ -2,17 +2,14 @@ use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::{
account::ImapAccount,
mailbox::{
close::CloseMailboxCommand, create::CreateMailboxCommand, delete::DeleteMailboxCommand,
expunge::ExpungeMailboxCommand, list::ListMailboxesCommand, purge::PurgeMailboxCommand,
rename::RenameMailboxCommand, select::SelectMailboxCommand,
status::StatusMailboxCommand, subscribe::SubscribeMailboxCommand,
unselect::UnselectMailboxCommand, unsubscribe::UnsubscribeMailboxCommand,
},
use crate::imap::{
account::ImapAccount,
mailbox::{
close::CloseMailboxCommand, create::CreateMailboxCommand, delete::DeleteMailboxCommand,
expunge::ExpungeMailboxCommand, list::ListMailboxesCommand, purge::PurgeMailboxCommand,
rename::RenameMailboxCommand, select::SelectMailboxCommand, status::StatusMailboxCommand,
subscribe::SubscribeMailboxCommand, unselect::UnselectMailboxCommand,
unsubscribe::UnsubscribeMailboxCommand,
},
};
+9 -10
View File
@@ -2,7 +2,7 @@ use std::fmt;
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use comfy_table::{Cell, Row, Table};
use io_imap::{
coroutines::{list::*, lsub::*},
types::{core::QuotedChar, flag::FlagNameAttribute, mailbox::Mailbox},
@@ -13,7 +13,7 @@ use serde::{Serialize, Serializer};
use crate::imap::{account::ImapAccount, stream};
/// List mailboxes.
/// List, search and filter mailboxes.
///
/// This command allows you to list mailboxes from your IMAP account.
/// By default, only subscribed mailboxes are listed. Use --all to
@@ -65,8 +65,8 @@ impl ListMailboxesCommand {
};
let table = MailboxesTable {
rows: mailboxes.into_iter().map(From::from).collect(),
preset: account.table_preset,
rows: mailboxes.into_iter().map(From::from).collect(),
};
printer.out(table)
@@ -75,8 +75,8 @@ impl ListMailboxesCommand {
#[derive(Clone, Debug, Default)]
pub struct MailboxesTable {
pub preset: String,
pub rows: Vec<MailboxRow>,
pub preset: &'static str,
}
impl fmt::Display for MailboxesTable {
@@ -84,8 +84,7 @@ impl fmt::Display for MailboxesTable {
let mut table = Table::new();
table
.load_preset(self.preset)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.load_preset(&self.preset)
.set_header(Row::from([
Cell::new("NAME"),
Cell::new("DELIMITER"),
@@ -93,11 +92,11 @@ impl fmt::Display for MailboxesTable {
]))
.add_rows(self.rows.iter().map(|mbox| {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(&mbox.name));
row.add_cell(Cell::new(&mbox.delimiter));
row.add_cell(Cell::new(&mbox.attributes.join(", ")));
row.max_height(1)
.add_cell(Cell::new(&mbox.name))
.add_cell(Cell::new(&mbox.delimiter))
.add_cell(Cell::new(&mbox.attributes.join(", ")));
row
}));
+9 -15
View File
@@ -2,7 +2,7 @@ use std::fmt;
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use comfy_table::{Cell, Row, Table};
use io_imap::{
coroutines::status::*,
types::status::{StatusDataItem, StatusDataItemName},
@@ -47,10 +47,12 @@ impl StatusMailboxCommand {
}
};
let table = MailboxStatusTable::from(items);
let table = MailboxStatusTable {
preset: account.table_preset,
status: items.into(),
};
printer.out(table)?;
Ok(())
printer.out(table)
}
}
@@ -97,15 +99,8 @@ impl From<Vec<StatusDataItem>> for MailboxStatus {
}
pub struct MailboxStatusTable {
status: MailboxStatus,
}
impl From<Vec<StatusDataItem>> for MailboxStatusTable {
fn from(items: Vec<StatusDataItem>) -> Self {
Self {
status: MailboxStatus::from(items),
}
}
pub preset: String,
pub status: MailboxStatus,
}
impl fmt::Display for MailboxStatusTable {
@@ -113,8 +108,7 @@ impl fmt::Display for MailboxStatusTable {
let mut table = Table::new();
table
.load_preset(presets::ASCII_MARKDOWN)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.load_preset(&self.preset)
.set_header(Row::from([Cell::new("ATTRIBUTE"), Cell::new("VALUE")]));
if let Some(n) = self.status.messages {
+1 -1
View File
@@ -126,7 +126,7 @@ impl ExportMessageCommand {
// Generate filename from subject or message-id
let filename = generate_eml_filename(&message, self.id);
let dir = self.directory.unwrap_or_else(|| PathBuf::from("."));
let dir = self.directory.unwrap_or(account.downloads_dir);
if !dir.exists() {
fs::create_dir_all(&dir)?;
+1
View File
@@ -2,6 +2,7 @@ pub mod account;
pub mod command;
pub mod envelope;
pub mod flag;
pub mod id;
pub mod mailbox;
pub mod message;
pub mod stream;