diff --git a/src/account.rs b/src/account.rs index 43073955..2240ce40 100644 --- a/src/account.rs +++ b/src/account.rs @@ -8,41 +8,34 @@ use dirs::download_dir; #[derive(Debug)] pub struct Account { pub backend: B, - - pub email: String, - pub display_name: Option, - pub signature: String, pub downloads_dir: PathBuf, - - pub table_preset: &'static str, + pub table_preset: String, } impl Account { pub fn new(config: Config, account_config: AccountConfig, backend: B) -> Result { 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()), }) } } diff --git a/src/config.rs b/src/config.rs index 105d725e..323f5636 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, - pub signature: Option, - pub signature_delim: Option, pub downloads_dir: Option, - + pub table_preset: Option, pub accounts: HashMap, } @@ -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, - pub signature: Option, - pub signature_delim: Option, pub downloads_dir: Option, + pub table_preset: Option, pub imap: Option, pub smtp: Option, @@ -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, + #[serde(deserialize_with = "shell_expanded_string")] pub authcid: String, pub passwd: SecretConfig, } diff --git a/src/imap/command.rs b/src/imap/command.rs index b417a454..bd552f18 100644 --- a/src/imap/command.rs +++ b/src/imap/command.rs @@ -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), } diff --git a/src/imap/envelope/command.rs b/src/imap/envelope/command.rs index f9b41b11..e4bc5cae 100644 --- a/src/imap/envelope/command.rs +++ b/src/imap/envelope/command.rs @@ -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, }, }; diff --git a/src/imap/flag/command.rs b/src/imap/flag/command.rs index 85a72e0b..ef91f5ae 100644 --- a/src/imap/flag/command.rs +++ b/src/imap/flag/command.rs @@ -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, }, }; diff --git a/src/imap/id.rs b/src/imap/id.rs new file mode 100644 index 00000000..c30769da --- /dev/null +++ b/src/imap/id.rs @@ -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, 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>, +} + +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}") + } +} diff --git a/src/imap/mailbox/command.rs b/src/imap/mailbox/command.rs index 5448b379..152748cf 100644 --- a/src/imap/mailbox/command.rs +++ b/src/imap/mailbox/command.rs @@ -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, }, }; diff --git a/src/imap/mailbox/list.rs b/src/imap/mailbox/list.rs index a50566c8..0d441556 100644 --- a/src/imap/mailbox/list.rs +++ b/src/imap/mailbox/list.rs @@ -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, - 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 })); diff --git a/src/imap/mailbox/status.rs b/src/imap/mailbox/status.rs index c82b48f8..73dc8057 100644 --- a/src/imap/mailbox/status.rs +++ b/src/imap/mailbox/status.rs @@ -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> for MailboxStatus { } pub struct MailboxStatusTable { - status: MailboxStatus, -} - -impl From> for MailboxStatusTable { - fn from(items: Vec) -> 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 { diff --git a/src/imap/message/export.rs b/src/imap/message/export.rs index d573ceb2..4433d438 100644 --- a/src/imap/message/export.rs +++ b/src/imap/message/export.rs @@ -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)?; diff --git a/src/imap/mod.rs b/src/imap/mod.rs index a32c162e..b29c5cdb 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -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;