diff --git a/src/domain/account/args.rs b/src/account/args.rs similarity index 96% rename from src/domain/account/args.rs rename to src/account/args.rs index ee3c43b..0917fb1 100644 --- a/src/domain/account/args.rs +++ b/src/account/args.rs @@ -11,7 +11,7 @@ use crate::{folder, ui::table}; const ARG_ACCOUNT: &str = "account"; const ARG_DRY_RUN: &str = "dry-run"; const ARG_RESET: &str = "reset"; -const CMD_ACCOUNTS: &str = "accounts"; +const CMD_ACCOUNT: &str = "account"; const CMD_CONFIGURE: &str = "configure"; const CMD_LIST: &str = "list"; const CMD_SYNC: &str = "sync"; @@ -32,7 +32,7 @@ pub enum Cmd { /// Represents the account command matcher. pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) { + let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) { if let Some(m) = m.subcommand_matches(CMD_SYNC) { info!("sync account subcommand matched"); let dry_run = parse_dry_run_arg(m); @@ -73,8 +73,8 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the account subcommand. pub fn subcmd() -> Command { - Command::new(CMD_ACCOUNTS) - .about("Manage accounts") + Command::new(CMD_ACCOUNT) + .about("Subcommand to manage accounts") .subcommands([ Command::new(CMD_LIST) .about("List all accounts from the config file") diff --git a/src/domain/account/config.rs b/src/account/config.rs similarity index 97% rename from src/domain/account/config.rs rename to src/account/config.rs index 5f289fe..a916dbb 100644 --- a/src/domain/account/config.rs +++ b/src/account/config.rs @@ -22,11 +22,8 @@ use std::{ }; use crate::{ - backend::BackendKind, - config::prelude::*, - domain::config::FolderConfig, - email::envelope::{config::EnvelopeConfig, flag::config::FlagConfig}, - message::config::MessageConfig, + backend::BackendKind, config::prelude::*, envelope::config::EnvelopeConfig, + flag::config::FlagConfig, folder::config::FolderConfig, message::config::MessageConfig, }; /// Represents all existing kind of account config. diff --git a/src/domain/account/handlers.rs b/src/account/handlers.rs similarity index 99% rename from src/domain/account/handlers.rs rename to src/account/handlers.rs index a37c9dd..f1f15be 100644 --- a/src/domain/account/handlers.rs +++ b/src/account/handlers.rs @@ -17,13 +17,13 @@ use once_cell::sync::Lazy; use std::{collections::HashMap, sync::Mutex}; use crate::{ + account::Accounts, backend::BackendContextBuilder, config::{ wizard::{prompt_passwd, prompt_secret}, TomlConfig, }, printer::{PrintTableOpts, Printer}, - Accounts, }; use super::TomlAccountConfig; diff --git a/src/domain/account/accounts.rs b/src/account/mod.rs similarity index 61% rename from src/domain/account/accounts.rs rename to src/account/mod.rs index c567044..1c5708d 100644 --- a/src/domain/account/accounts.rs +++ b/src/account/mod.rs @@ -1,19 +1,62 @@ -//! Account module. -//! -//! This module contains the definition of the printable account, -//! which is only used by the "accounts" command to list all available -//! accounts from the config file. +pub mod args; +pub mod config; +pub mod handlers; +pub(crate) mod wizard; use anyhow::Result; use serde::Serialize; -use std::{collections::hash_map::Iter, ops::Deref}; +use std::{collections::hash_map::Iter, fmt, ops::Deref}; use crate::{ printer::{PrintTable, PrintTableOpts, WriteColor}, - ui::Table, + ui::table::{Cell, Row, Table}, }; -use super::{Account, TomlAccountConfig}; +use self::config::TomlAccountConfig; + +/// Represents the printable account. +#[derive(Debug, Default, PartialEq, Eq, Serialize)] +pub struct Account { + /// Represents the account name. + pub name: String, + /// Represents the backend name of the account. + pub backend: String, + /// Represents the default state of the account. + pub default: bool, +} + +impl Account { + pub fn new(name: &str, backend: &str, default: bool) -> Self { + Self { + name: name.into(), + backend: backend.into(), + default, + } + } +} + +impl fmt::Display for Account { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Table for Account { + fn head() -> Row { + Row::new() + .cell(Cell::new("NAME").shrinkable().bold().underline().white()) + .cell(Cell::new("BACKEND").bold().underline().white()) + .cell(Cell::new("DEFAULT").bold().underline().white()) + } + + fn row(&self) -> Row { + let default = if self.default { "yes" } else { "" }; + Row::new() + .cell(Cell::new(&self.name).shrinkable().green()) + .cell(Cell::new(&self.backend).blue()) + .cell(Cell::new(default).white()) + } +} /// Represents the list of printable accounts. #[derive(Debug, Default, Serialize)] diff --git a/src/domain/account/wizard.rs b/src/account/wizard.rs similarity index 100% rename from src/domain/account/wizard.rs rename to src/account/wizard.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 1c9af19..4084e0b 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -46,7 +46,7 @@ use email::{ }; use serde::{Deserialize, Serialize}; -use crate::{account::TomlAccountConfig, Envelopes, IdMapper}; +use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes}; #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] diff --git a/src/cache/id_mapper.rs b/src/cache/id_mapper.rs deleted file mode 100644 index 7668aa9..0000000 --- a/src/cache/id_mapper.rs +++ /dev/null @@ -1,166 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use email::account::config::AccountConfig; -use log::{debug, trace}; -use std::path::{Path, PathBuf}; - -const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite"; - -#[derive(Debug)] -pub enum IdMapper { - Dummy, - Mapper(String, rusqlite::Connection), -} - -impl IdMapper { - pub fn find_closest_db_path(dir: impl AsRef) -> PathBuf { - let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); - let mut db_parent_dir = dir.as_ref().parent(); - - while !db_path.is_file() { - match db_parent_dir { - Some(dir) => { - db_path = dir.join(ID_MAPPER_DB_FILE_NAME); - db_parent_dir = dir.parent(); - } - None => { - db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); - break; - } - } - } - - db_path - } - - pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result { - let folder = account_config.get_folder_alias(folder)?; - let digest = md5::compute(account_config.name.clone() + &folder); - let table = format!("id_mapper_{digest:x}"); - debug!("creating id mapper table {table} at {db_path:?}…"); - - let db_path = Self::find_closest_db_path(db_path); - let conn = rusqlite::Connection::open(&db_path) - .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; - - let query = format!( - "CREATE TABLE IF NOT EXISTS {table} ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - internal_id TEXT UNIQUE - )", - ); - trace!("create table query: {query:#?}"); - - conn.execute(&query, []) - .context("cannot create id mapper table")?; - - Ok(Self::Mapper(table, conn)) - } - - pub fn create_alias(&self, id: I) -> Result - where - I: AsRef, - { - let id = id.as_ref(); - match self { - Self::Dummy => Ok(id.to_owned()), - Self::Mapper(table, conn) => { - debug!("creating alias for id {id}…"); - - let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table); - trace!("insert query: {query:#?}"); - - conn.execute(&query, [id]) - .with_context(|| format!("cannot create id alias for id {id}"))?; - - let alias = conn.last_insert_rowid().to_string(); - debug!("created alias {alias} for id {id}"); - - Ok(alias) - } - } - } - - pub fn get_or_create_alias(&self, id: I) -> Result - where - I: AsRef, - { - let id = id.as_ref(); - match self { - Self::Dummy => Ok(id.to_owned()), - Self::Mapper(table, conn) => { - debug!("getting alias for id {id}…"); - - let query = format!("SELECT id FROM {} WHERE internal_id = ?", table); - trace!("select query: {query:#?}"); - - let mut stmt = conn - .prepare(&query) - .with_context(|| format!("cannot get alias for id {id}"))?; - let aliases: Vec = stmt - .query_map([id], |row| row.get(0)) - .with_context(|| format!("cannot get alias for id {id}"))? - .collect::>() - .with_context(|| format!("cannot get alias for id {id}"))?; - let alias = match aliases.first() { - Some(alias) => { - debug!("found alias {alias} for id {id}"); - alias.to_string() - } - None => { - debug!("alias not found, creating it…"); - self.create_alias(id)? - } - }; - - Ok(alias) - } - } - } - - pub fn get_id(&self, alias: A) -> Result - where - A: AsRef, - { - let alias = alias.as_ref(); - let alias = alias - .parse::() - .context(format!("cannot parse id mapper alias {alias}"))?; - - match self { - Self::Dummy => Ok(alias.to_string()), - Self::Mapper(table, conn) => { - debug!("getting id from alias {alias}…"); - - let query = format!("SELECT internal_id FROM {} WHERE id = ?", table); - trace!("select query: {query:#?}"); - - let mut stmt = conn - .prepare(&query) - .with_context(|| format!("cannot get id from alias {alias}"))?; - let ids: Vec = stmt - .query_map([alias], |row| row.get(0)) - .with_context(|| format!("cannot get id from alias {alias}"))? - .collect::>() - .with_context(|| format!("cannot get id from alias {alias}"))?; - let id = ids - .first() - .ok_or_else(|| anyhow!("cannot get id from alias {alias}"))? - .to_owned(); - debug!("found id {id} from alias {alias}"); - - Ok(id) - } - } - } - - pub fn get_ids(&self, aliases: I) -> Result> - where - A: AsRef, - I: IntoIterator, - { - aliases - .into_iter() - .map(|alias| self.get_id(alias)) - .collect() - } -} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 2dae5fe..04ac835 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,4 +1,168 @@ pub mod args; -mod id_mapper; -pub use id_mapper::IdMapper; +use anyhow::{anyhow, Context, Result}; +use email::account::config::AccountConfig; +use log::{debug, trace}; +use std::path::{Path, PathBuf}; + +const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite"; + +#[derive(Debug)] +pub enum IdMapper { + Dummy, + Mapper(String, rusqlite::Connection), +} + +impl IdMapper { + pub fn find_closest_db_path(dir: impl AsRef) -> PathBuf { + let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); + let mut db_parent_dir = dir.as_ref().parent(); + + while !db_path.is_file() { + match db_parent_dir { + Some(dir) => { + db_path = dir.join(ID_MAPPER_DB_FILE_NAME); + db_parent_dir = dir.parent(); + } + None => { + db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); + break; + } + } + } + + db_path + } + + pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result { + let folder = account_config.get_folder_alias(folder)?; + let digest = md5::compute(account_config.name.clone() + &folder); + let table = format!("id_mapper_{digest:x}"); + debug!("creating id mapper table {table} at {db_path:?}…"); + + let db_path = Self::find_closest_db_path(db_path); + let conn = rusqlite::Connection::open(&db_path) + .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; + + let query = format!( + "CREATE TABLE IF NOT EXISTS {table} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_id TEXT UNIQUE + )", + ); + trace!("create table query: {query:#?}"); + + conn.execute(&query, []) + .context("cannot create id mapper table")?; + + Ok(Self::Mapper(table, conn)) + } + + pub fn create_alias(&self, id: I) -> Result + where + I: AsRef, + { + let id = id.as_ref(); + match self { + Self::Dummy => Ok(id.to_owned()), + Self::Mapper(table, conn) => { + debug!("creating alias for id {id}…"); + + let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table); + trace!("insert query: {query:#?}"); + + conn.execute(&query, [id]) + .with_context(|| format!("cannot create id alias for id {id}"))?; + + let alias = conn.last_insert_rowid().to_string(); + debug!("created alias {alias} for id {id}"); + + Ok(alias) + } + } + } + + pub fn get_or_create_alias(&self, id: I) -> Result + where + I: AsRef, + { + let id = id.as_ref(); + match self { + Self::Dummy => Ok(id.to_owned()), + Self::Mapper(table, conn) => { + debug!("getting alias for id {id}…"); + + let query = format!("SELECT id FROM {} WHERE internal_id = ?", table); + trace!("select query: {query:#?}"); + + let mut stmt = conn + .prepare(&query) + .with_context(|| format!("cannot get alias for id {id}"))?; + let aliases: Vec = stmt + .query_map([id], |row| row.get(0)) + .with_context(|| format!("cannot get alias for id {id}"))? + .collect::>() + .with_context(|| format!("cannot get alias for id {id}"))?; + let alias = match aliases.first() { + Some(alias) => { + debug!("found alias {alias} for id {id}"); + alias.to_string() + } + None => { + debug!("alias not found, creating it…"); + self.create_alias(id)? + } + }; + + Ok(alias) + } + } + } + + pub fn get_id(&self, alias: A) -> Result + where + A: AsRef, + { + let alias = alias.as_ref(); + let alias = alias + .parse::() + .context(format!("cannot parse id mapper alias {alias}"))?; + + match self { + Self::Dummy => Ok(alias.to_string()), + Self::Mapper(table, conn) => { + debug!("getting id from alias {alias}…"); + + let query = format!("SELECT internal_id FROM {} WHERE id = ?", table); + trace!("select query: {query:#?}"); + + let mut stmt = conn + .prepare(&query) + .with_context(|| format!("cannot get id from alias {alias}"))?; + let ids: Vec = stmt + .query_map([alias], |row| row.get(0)) + .with_context(|| format!("cannot get id from alias {alias}"))? + .collect::>() + .with_context(|| format!("cannot get id from alias {alias}"))?; + let id = ids + .first() + .ok_or_else(|| anyhow!("cannot get id from alias {alias}"))? + .to_owned(); + debug!("found id {id} from alias {alias}"); + + Ok(id) + } + } + } + + pub fn get_ids(&self, aliases: I) -> Result> + where + A: AsRef, + I: IntoIterator, + { + aliases + .into_iter() + .map(|alias| self.get_id(alias)) + .collect() + } +} diff --git a/src/compl/args.rs b/src/completion/args.rs similarity index 93% rename from src/compl/args.rs rename to src/completion/args.rs index 137d31f..00c79e9 100644 --- a/src/compl/args.rs +++ b/src/completion/args.rs @@ -32,7 +32,7 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Completion subcommands. pub fn subcmd() -> Command { Command::new(CMD_COMPLETION) - .about("Generates the completion script for the given shell") + .about("Generate the completion script for the given shell") .args(&[Arg::new(ARG_SHELL) .value_parser(value_parser!(Shell)) .required(true)]) diff --git a/src/compl/handlers.rs b/src/completion/handlers.rs similarity index 100% rename from src/compl/handlers.rs rename to src/completion/handlers.rs diff --git a/src/compl/mod.rs b/src/completion/mod.rs similarity index 100% rename from src/compl/mod.rs rename to src/completion/mod.rs diff --git a/src/config/config.rs b/src/config/config.rs deleted file mode 100644 index 6a166dd..0000000 --- a/src/config/config.rs +++ /dev/null @@ -1,758 +0,0 @@ -//! Deserialized config module. -//! -//! This module contains the raw deserialized representation of the -//! user configuration file. - -use anyhow::{anyhow, Context, Result}; -use dialoguer::Confirm; -use dirs::{config_dir, home_dir}; -use email::{ - account::config::AccountConfig, - config::Config, - email::config::{EmailHooks, EmailTextPlainFormat}, -}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - fs, - path::{Path, PathBuf}, - process, -}; -use toml; - -use crate::{ - account::TomlAccountConfig, - backend::BackendKind, - config::{prelude::*, wizard}, - wizard_prompt, wizard_warn, -}; - -/// Represents the user config file. -#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct TomlConfig { - #[serde(alias = "name")] - pub display_name: Option, - pub signature_delim: Option, - pub signature: Option, - pub downloads_dir: Option, - - pub folder_listing_page_size: Option, - pub folder_aliases: Option>, - - pub email_listing_page_size: Option, - pub email_listing_datetime_fmt: Option, - pub email_listing_datetime_local_tz: Option, - pub email_reading_headers: Option>, - #[serde( - default, - with = "OptionEmailTextPlainFormatDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_format: Option, - pub email_writing_headers: Option>, - pub email_sending_save_copy: Option, - #[serde( - default, - with = "OptionEmailHooksDef", - skip_serializing_if = "Option::is_none" - )] - pub email_hooks: Option, - - #[serde(flatten)] - pub accounts: HashMap, -} - -impl TomlConfig { - /// Read and parse the TOML configuration at the given path. - /// - /// Returns an error if the configuration file cannot be read or - /// if its content cannot be parsed. - fn from_path(path: &Path) -> Result { - let content = - fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?; - toml::from_str(&content).context(format!("cannot parse config file at {path:?}")) - } - - /// Create and save a TOML configuration using the wizard. - /// - /// If the user accepts the confirmation, the wizard starts and - /// help him to create his configuration file. Otherwise the - /// program stops. - /// - /// NOTE: the wizard can only be used with interactive shells. - async fn from_wizard(path: PathBuf) -> Result { - wizard_warn!("Cannot find existing configuration at {path:?}."); - - let confirm = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to create one with the wizard?" - )) - .default(true) - .interact_opt()? - .unwrap_or_default(); - - if !confirm { - process::exit(0); - } - - wizard::configure(path).await - } - - /// Read and parse the TOML configuration from default paths. - pub async fn from_default_paths() -> Result { - match Self::first_valid_default_path() { - Some(path) => Self::from_path(&path), - None => Self::from_wizard(Self::default_path()?).await, - } - } - - /// Read and parse the TOML configuration at the optional given - /// path. - /// - /// If the given path exists, then read and parse the TOML - /// configuration from it. - /// - /// If the given path does not exist, then create it using the - /// wizard. - /// - /// If no path is given, then either read and parse the TOML - /// configuration at the first valid default path, otherwise - /// create it using the wizard. wizard. - pub async fn from_some_path_or_default(path: Option>) -> Result { - match path.map(Into::into) { - Some(ref path) if path.exists() => Self::from_path(path), - Some(path) => Self::from_wizard(path).await, - None => match Self::first_valid_default_path() { - Some(path) => Self::from_path(&path), - None => Self::from_wizard(Self::default_path()?).await, - }, - } - } - - /// Get the default configuration path. - /// - /// Returns an error if the XDG configuration directory cannot be - /// found. - pub fn default_path() -> Result { - Ok(config_dir() - .ok_or(anyhow!("cannot get XDG config directory"))? - .join("himalaya") - .join("config.toml")) - } - - /// Get the first default configuration path that points to a - /// valid file. - /// - /// Tries paths in this order: - /// - /// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to - /// `$XDG_CONFIG_DIR` in other OSes.) - /// - `$HOME/.config/himalaya/config.toml` - /// - `$HOME/.himalayarc` - pub fn first_valid_default_path() -> Option { - Self::default_path() - .ok() - .filter(|p| p.exists()) - .or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml"))) - .filter(|p| p.exists()) - .or_else(|| home_dir().map(|p| p.join(".himalayarc"))) - .filter(|p| p.exists()) - } - - /// Build account configurations from a given account name. - pub fn into_account_configs( - self, - account_name: Option<&str>, - disable_cache: bool, - ) -> Result<(TomlAccountConfig, AccountConfig)> { - let (account_name, mut toml_account_config) = match account_name { - Some("default") | Some("") | None => self - .accounts - .iter() - .find_map(|(name, account)| { - account - .default - .filter(|default| *default == true) - .map(|_| (name.to_owned(), account.clone())) - }) - .ok_or_else(|| anyhow!("cannot find default account")), - Some(name) => self - .accounts - .get(name) - .map(|account| (name.to_owned(), account.clone())) - .ok_or_else(|| anyhow!("cannot find account {name}")), - }?; - - #[cfg(feature = "imap")] - if let Some(imap_config) = toml_account_config.imap.as_mut() { - imap_config - .auth - .replace_undefined_keyring_entries(&account_name); - } - - #[cfg(feature = "smtp")] - if let Some(smtp_config) = toml_account_config.smtp.as_mut() { - smtp_config - .auth - .replace_undefined_keyring_entries(&account_name); - } - - if let Some(true) = toml_account_config.sync { - if !disable_cache { - toml_account_config.backend = Some(BackendKind::MaildirForSync); - } - } - - let config = Config { - display_name: self.display_name, - signature_delim: self.signature_delim, - signature: self.signature, - downloads_dir: self.downloads_dir, - - folder_listing_page_size: self.folder_listing_page_size, - folder_aliases: self.folder_aliases, - - email_listing_page_size: self.email_listing_page_size, - email_listing_datetime_fmt: self.email_listing_datetime_fmt, - email_listing_datetime_local_tz: self.email_listing_datetime_local_tz, - email_reading_headers: self.email_reading_headers, - email_reading_format: self.email_reading_format, - email_writing_headers: self.email_writing_headers, - email_sending_save_copy: self.email_sending_save_copy, - email_hooks: self.email_hooks, - - accounts: HashMap::from_iter(self.accounts.clone().into_iter().map( - |(name, config)| { - ( - name.clone(), - AccountConfig { - name, - email: config.email, - display_name: config.display_name, - signature_delim: config.signature_delim, - signature: config.signature, - downloads_dir: config.downloads_dir, - - folder_listing_page_size: config.folder_listing_page_size, - folder_aliases: config.folder_aliases.unwrap_or_default(), - - email_listing_page_size: config.email_listing_page_size, - email_listing_datetime_fmt: config.email_listing_datetime_fmt, - email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, - - email_reading_headers: config.email_reading_headers, - email_reading_format: config.email_reading_format.unwrap_or_default(), - email_writing_headers: config.email_writing_headers, - email_sending_save_copy: config.email_sending_save_copy, - email_hooks: config.email_hooks.unwrap_or_default(), - - sync: config.sync.unwrap_or_default(), - sync_dir: config.sync_dir, - sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), - - #[cfg(feature = "pgp")] - pgp: config.pgp, - }, - ) - }, - )), - }; - - let account_config = config.account(&account_name)?; - - Ok((toml_account_config, account_config)) - } -} - -#[cfg(test)] -mod tests { - use email::{ - account::config::passwd::PasswdConfig, maildir::config::MaildirConfig, - sendmail::config::SendmailConfig, - }; - use secret::Secret; - - #[cfg(feature = "notmuch")] - use email::backend::NotmuchConfig; - #[cfg(feature = "imap")] - use email::imap::config::{ImapAuthConfig, ImapConfig}; - #[cfg(feature = "smtp")] - use email::smtp::config::{SmtpAuthConfig, SmtpConfig}; - - use std::io::Write; - use tempfile::NamedTempFile; - - use super::*; - - async fn make_config(config: &str) -> Result { - let mut file = NamedTempFile::new().unwrap(); - write!(file, "{}", config).unwrap(); - TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await - } - - #[tokio::test] - async fn empty_config() { - let config = make_config("").await; - - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "config file must contain at least one account" - ); - } - - #[tokio::test] - async fn account_missing_email_field() { - let config = make_config("[account]").await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `email`")); - } - - #[tokio::test] - async fn account_missing_backend_field() { - let config = make_config( - "[account] - email = \"test@localhost\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `backend`")); - } - - #[tokio::test] - async fn account_invalid_backend_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"bad\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("unknown variant `bad`")); - } - - #[tokio::test] - async fn imap_account_missing_host_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-host`")); - } - - #[tokio::test] - async fn account_backend_imap_missing_port_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-port`")); - } - - #[tokio::test] - async fn account_backend_imap_missing_login_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\" - imap-port = 993", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-login`")); - } - - #[tokio::test] - async fn account_backend_imap_missing_passwd_cmd_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\" - imap-port = 993 - imap-login = \"login\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-auth`")); - } - - #[tokio::test] - async fn account_backend_maildir_missing_root_dir_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"maildir\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `maildir-root-dir`")); - } - - #[cfg(feature = "notmuch")] - #[tokio::test] - async fn account_backend_notmuch_missing_db_path_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"notmuch\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `notmuch-db-path`")); - } - - #[tokio::test] - async fn account_missing_sender_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `sender`")); - } - - #[tokio::test] - async fn account_invalid_sender_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"bad\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),); - } - - #[tokio::test] - async fn account_smtp_sender_missing_host_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-host`")); - } - - #[tokio::test] - async fn account_smtp_sender_missing_port_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-port`")); - } - - #[tokio::test] - async fn account_smtp_sender_missing_login_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\" - smtp-port = 25", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-login`")); - } - - #[tokio::test] - async fn account_smtp_sender_missing_auth_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\" - smtp-port = 25 - smtp-login = \"login\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-auth`")); - } - - #[tokio::test] - async fn account_sendmail_sender_missing_cmd_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"sendmail\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - sender: SenderConfig::Sendmail(SendmailConfig { - cmd: "/usr/sbin/sendmail".into() - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[cfg(feature = "smtp")] - #[tokio::test] - async fn account_smtp_sender_minimum_config() { - use email::sender::SenderConfig; - - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\" - smtp-port = 25 - smtp-login = \"login\" - smtp-auth = \"passwd\" - smtp-passwd = { cmd = \"echo password\" }", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - sender: SenderConfig::Smtp(SmtpConfig { - host: "localhost".into(), - port: 25, - login: "login".into(), - auth: SmtpAuthConfig::Passwd(PasswdConfig { - passwd: Secret::new_cmd(String::from("echo password")) - }), - ..SmtpConfig::default() - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[tokio::test] - async fn account_sendmail_sender_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"sendmail\" - sendmail-cmd = \"echo send\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - sender: SenderConfig::Sendmail(SendmailConfig { - cmd: Cmd::from("echo send") - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[tokio::test] - async fn account_backend_imap_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\" - imap-port = 993 - imap-login = \"login\" - imap-auth = \"passwd\" - imap-passwd = { cmd = \"echo password\" }", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - backend: BackendConfig::Imap(ImapConfig { - host: "localhost".into(), - port: 993, - login: "login".into(), - auth: ImapAuthConfig::Passwd(PasswdConfig { - passwd: Secret::new_cmd(String::from("echo password")) - }), - ..ImapConfig::default() - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[tokio::test] - async fn account_backend_maildir_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"maildir\" - maildir-root-dir = \"/tmp/maildir\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - backend: BackendConfig::Maildir(MaildirConfig { - root_dir: "/tmp/maildir".into(), - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[cfg(feature = "notmuch")] - #[tokio::test] - async fn account_backend_notmuch_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"notmuch\" - notmuch-db-path = \"/tmp/notmuch.db\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - backend: BackendConfig::Notmuch(NotmuchConfig { - db_path: "/tmp/notmuch.db".into(), - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ); - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index feafee6..e54f0df 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,6 +1,760 @@ +//! Deserialized config module. +//! +//! This module contains the raw deserialized representation of the +//! user configuration file. + pub mod args; -pub mod config; pub mod prelude; pub mod wizard; -pub use config::*; +use anyhow::{anyhow, Context, Result}; +use dialoguer::Confirm; +use dirs::{config_dir, home_dir}; +use email::{ + account::config::AccountConfig, + config::Config, + email::config::{EmailHooks, EmailTextPlainFormat}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, + process, +}; +use toml; + +use crate::{ + account::config::TomlAccountConfig, backend::BackendKind, config::prelude::*, wizard_prompt, + wizard_warn, +}; + +/// Represents the user config file. +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TomlConfig { + #[serde(alias = "name")] + pub display_name: Option, + pub signature_delim: Option, + pub signature: Option, + pub downloads_dir: Option, + + pub folder_listing_page_size: Option, + pub folder_aliases: Option>, + + pub email_listing_page_size: Option, + pub email_listing_datetime_fmt: Option, + pub email_listing_datetime_local_tz: Option, + pub email_reading_headers: Option>, + #[serde( + default, + with = "OptionEmailTextPlainFormatDef", + skip_serializing_if = "Option::is_none" + )] + pub email_reading_format: Option, + pub email_writing_headers: Option>, + pub email_sending_save_copy: Option, + #[serde( + default, + with = "OptionEmailHooksDef", + skip_serializing_if = "Option::is_none" + )] + pub email_hooks: Option, + + #[serde(flatten)] + pub accounts: HashMap, +} + +impl TomlConfig { + /// Read and parse the TOML configuration at the given path. + /// + /// Returns an error if the configuration file cannot be read or + /// if its content cannot be parsed. + fn from_path(path: &Path) -> Result { + let content = + fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?; + toml::from_str(&content).context(format!("cannot parse config file at {path:?}")) + } + + /// Create and save a TOML configuration using the wizard. + /// + /// If the user accepts the confirmation, the wizard starts and + /// help him to create his configuration file. Otherwise the + /// program stops. + /// + /// NOTE: the wizard can only be used with interactive shells. + async fn from_wizard(path: PathBuf) -> Result { + wizard_warn!("Cannot find existing configuration at {path:?}."); + + let confirm = Confirm::new() + .with_prompt(wizard_prompt!( + "Would you like to create one with the wizard?" + )) + .default(true) + .interact_opt()? + .unwrap_or_default(); + + if !confirm { + process::exit(0); + } + + wizard::configure(path).await + } + + /// Read and parse the TOML configuration from default paths. + pub async fn from_default_paths() -> Result { + match Self::first_valid_default_path() { + Some(path) => Self::from_path(&path), + None => Self::from_wizard(Self::default_path()?).await, + } + } + + /// Read and parse the TOML configuration at the optional given + /// path. + /// + /// If the given path exists, then read and parse the TOML + /// configuration from it. + /// + /// If the given path does not exist, then create it using the + /// wizard. + /// + /// If no path is given, then either read and parse the TOML + /// configuration at the first valid default path, otherwise + /// create it using the wizard. wizard. + pub async fn from_some_path_or_default(path: Option>) -> Result { + match path.map(Into::into) { + Some(ref path) if path.exists() => Self::from_path(path), + Some(path) => Self::from_wizard(path).await, + None => match Self::first_valid_default_path() { + Some(path) => Self::from_path(&path), + None => Self::from_wizard(Self::default_path()?).await, + }, + } + } + + /// Get the default configuration path. + /// + /// Returns an error if the XDG configuration directory cannot be + /// found. + pub fn default_path() -> Result { + Ok(config_dir() + .ok_or(anyhow!("cannot get XDG config directory"))? + .join("himalaya") + .join("config.toml")) + } + + /// Get the first default configuration path that points to a + /// valid file. + /// + /// Tries paths in this order: + /// + /// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to + /// `$XDG_CONFIG_DIR` in other OSes.) + /// - `$HOME/.config/himalaya/config.toml` + /// - `$HOME/.himalayarc` + pub fn first_valid_default_path() -> Option { + Self::default_path() + .ok() + .filter(|p| p.exists()) + .or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml"))) + .filter(|p| p.exists()) + .or_else(|| home_dir().map(|p| p.join(".himalayarc"))) + .filter(|p| p.exists()) + } + + /// Build account configurations from a given account name. + pub fn into_account_configs( + self, + account_name: Option<&str>, + disable_cache: bool, + ) -> Result<(TomlAccountConfig, AccountConfig)> { + let (account_name, mut toml_account_config) = match account_name { + Some("default") | Some("") | None => self + .accounts + .iter() + .find_map(|(name, account)| { + account + .default + .filter(|default| *default == true) + .map(|_| (name.to_owned(), account.clone())) + }) + .ok_or_else(|| anyhow!("cannot find default account")), + Some(name) => self + .accounts + .get(name) + .map(|account| (name.to_owned(), account.clone())) + .ok_or_else(|| anyhow!("cannot find account {name}")), + }?; + + #[cfg(feature = "imap")] + if let Some(imap_config) = toml_account_config.imap.as_mut() { + imap_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + + #[cfg(feature = "smtp")] + if let Some(smtp_config) = toml_account_config.smtp.as_mut() { + smtp_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + + if let Some(true) = toml_account_config.sync { + if !disable_cache { + toml_account_config.backend = Some(BackendKind::MaildirForSync); + } + } + + let config = Config { + display_name: self.display_name, + signature_delim: self.signature_delim, + signature: self.signature, + downloads_dir: self.downloads_dir, + + folder_listing_page_size: self.folder_listing_page_size, + folder_aliases: self.folder_aliases, + + email_listing_page_size: self.email_listing_page_size, + email_listing_datetime_fmt: self.email_listing_datetime_fmt, + email_listing_datetime_local_tz: self.email_listing_datetime_local_tz, + email_reading_headers: self.email_reading_headers, + email_reading_format: self.email_reading_format, + email_writing_headers: self.email_writing_headers, + email_sending_save_copy: self.email_sending_save_copy, + email_hooks: self.email_hooks, + + accounts: HashMap::from_iter(self.accounts.clone().into_iter().map( + |(name, config)| { + ( + name.clone(), + AccountConfig { + name, + email: config.email, + display_name: config.display_name, + signature_delim: config.signature_delim, + signature: config.signature, + downloads_dir: config.downloads_dir, + + folder_listing_page_size: config.folder_listing_page_size, + folder_aliases: config.folder_aliases.unwrap_or_default(), + + email_listing_page_size: config.email_listing_page_size, + email_listing_datetime_fmt: config.email_listing_datetime_fmt, + email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, + + email_reading_headers: config.email_reading_headers, + email_reading_format: config.email_reading_format.unwrap_or_default(), + email_writing_headers: config.email_writing_headers, + email_sending_save_copy: config.email_sending_save_copy, + email_hooks: config.email_hooks.unwrap_or_default(), + + sync: config.sync.unwrap_or_default(), + sync_dir: config.sync_dir, + sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), + + #[cfg(feature = "pgp")] + pgp: config.pgp, + }, + ) + }, + )), + }; + + let account_config = config.account(&account_name)?; + + Ok((toml_account_config, account_config)) + } +} + +#[cfg(test)] +mod tests { + use email::{ + account::config::passwd::PasswdConfig, maildir::config::MaildirConfig, + sendmail::config::SendmailConfig, + }; + use secret::Secret; + + #[cfg(feature = "notmuch")] + use email::backend::NotmuchConfig; + #[cfg(feature = "imap")] + use email::imap::config::{ImapAuthConfig, ImapConfig}; + #[cfg(feature = "smtp")] + use email::smtp::config::{SmtpAuthConfig, SmtpConfig}; + + use std::io::Write; + use tempfile::NamedTempFile; + + use super::*; + + async fn make_config(config: &str) -> Result { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "{}", config).unwrap(); + TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await + } + + #[tokio::test] + async fn empty_config() { + let config = make_config("").await; + + assert_eq!( + config.unwrap_err().root_cause().to_string(), + "config file must contain at least one account" + ); + } + + #[tokio::test] + async fn account_missing_email_field() { + let config = make_config("[account]").await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `email`")); + } + + #[tokio::test] + async fn account_missing_backend_field() { + let config = make_config( + "[account] + email = \"test@localhost\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `backend`")); + } + + #[tokio::test] + async fn account_invalid_backend_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"bad\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("unknown variant `bad`")); + } + + #[tokio::test] + async fn imap_account_missing_host_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-host`")); + } + + #[tokio::test] + async fn account_backend_imap_missing_port_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-port`")); + } + + #[tokio::test] + async fn account_backend_imap_missing_login_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\" + imap-port = 993", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-login`")); + } + + #[tokio::test] + async fn account_backend_imap_missing_passwd_cmd_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\" + imap-port = 993 + imap-login = \"login\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-auth`")); + } + + #[tokio::test] + async fn account_backend_maildir_missing_root_dir_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"maildir\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `maildir-root-dir`")); + } + + #[cfg(feature = "notmuch")] + #[tokio::test] + async fn account_backend_notmuch_missing_db_path_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"notmuch\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `notmuch-db-path`")); + } + + #[tokio::test] + async fn account_missing_sender_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `sender`")); + } + + #[tokio::test] + async fn account_invalid_sender_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"bad\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),); + } + + #[tokio::test] + async fn account_smtp_sender_missing_host_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-host`")); + } + + #[tokio::test] + async fn account_smtp_sender_missing_port_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-port`")); + } + + #[tokio::test] + async fn account_smtp_sender_missing_login_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\" + smtp-port = 25", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-login`")); + } + + #[tokio::test] + async fn account_smtp_sender_missing_auth_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\" + smtp-port = 25 + smtp-login = \"login\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-auth`")); + } + + #[tokio::test] + async fn account_sendmail_sender_missing_cmd_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"sendmail\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + sender: SenderConfig::Sendmail(SendmailConfig { + cmd: "/usr/sbin/sendmail".into() + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[cfg(feature = "smtp")] + #[tokio::test] + async fn account_smtp_sender_minimum_config() { + use email::sender::SenderConfig; + + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\" + smtp-port = 25 + smtp-login = \"login\" + smtp-auth = \"passwd\" + smtp-passwd = { cmd = \"echo password\" }", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + sender: SenderConfig::Smtp(SmtpConfig { + host: "localhost".into(), + port: 25, + login: "login".into(), + auth: SmtpAuthConfig::Passwd(PasswdConfig { + passwd: Secret::new_cmd(String::from("echo password")) + }), + ..SmtpConfig::default() + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[tokio::test] + async fn account_sendmail_sender_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"sendmail\" + sendmail-cmd = \"echo send\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + sender: SenderConfig::Sendmail(SendmailConfig { + cmd: Cmd::from("echo send") + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[tokio::test] + async fn account_backend_imap_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\" + imap-port = 993 + imap-login = \"login\" + imap-auth = \"passwd\" + imap-passwd = { cmd = \"echo password\" }", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + backend: BackendConfig::Imap(ImapConfig { + host: "localhost".into(), + port: 993, + login: "login".into(), + auth: ImapAuthConfig::Passwd(PasswdConfig { + passwd: Secret::new_cmd(String::from("echo password")) + }), + ..ImapConfig::default() + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[tokio::test] + async fn account_backend_maildir_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"maildir\" + maildir-root-dir = \"/tmp/maildir\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + backend: BackendConfig::Maildir(MaildirConfig { + root_dir: "/tmp/maildir".into(), + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[cfg(feature = "notmuch")] + #[tokio::test] + async fn account_backend_notmuch_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"notmuch\" + notmuch-db-path = \"/tmp/notmuch.db\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + backend: BackendConfig::Notmuch(NotmuchConfig { + db_path: "/tmp/notmuch.db".into(), + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ); + } +} diff --git a/src/domain/account/account.rs b/src/domain/account/account.rs deleted file mode 100644 index 8ddae67..0000000 --- a/src/domain/account/account.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Account module. -//! -//! This module contains the definition of the printable account, -//! which is only used by the "accounts" command to list all available -//! accounts from the config file. - -use serde::Serialize; -use std::fmt; - -use crate::ui::table::{Cell, Row, Table}; - -/// Represents the printable account. -#[derive(Debug, Default, PartialEq, Eq, Serialize)] -pub struct Account { - /// Represents the account name. - pub name: String, - /// Represents the backend name of the account. - pub backend: String, - /// Represents the default state of the account. - pub default: bool, -} - -impl Account { - pub fn new(name: &str, backend: &str, default: bool) -> Self { - Self { - name: name.into(), - backend: backend.into(), - default, - } - } -} - -impl fmt::Display for Account { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for Account { - fn head() -> Row { - Row::new() - .cell(Cell::new("NAME").shrinkable().bold().underline().white()) - .cell(Cell::new("BACKEND").bold().underline().white()) - .cell(Cell::new("DEFAULT").bold().underline().white()) - } - - fn row(&self) -> Row { - let default = if self.default { "yes" } else { "" }; - Row::new() - .cell(Cell::new(&self.name).shrinkable().green()) - .cell(Cell::new(&self.backend).blue()) - .cell(Cell::new(default).white()) - } -} diff --git a/src/domain/account/mod.rs b/src/domain/account/mod.rs deleted file mode 100644 index 0f10b25..0000000 --- a/src/domain/account/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod account; -pub mod accounts; -pub mod args; -pub mod config; -pub mod handlers; -pub(crate) mod wizard; - -pub use account::*; -pub use accounts::*; -pub use config::*; diff --git a/src/domain/email/envelope/flag/mod.rs b/src/domain/email/envelope/flag/mod.rs deleted file mode 100644 index ef68c36..0000000 --- a/src/domain/email/envelope/flag/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod config; diff --git a/src/domain/email/envelope/mod.rs b/src/domain/email/envelope/mod.rs deleted file mode 100644 index abaed0b..0000000 --- a/src/domain/email/envelope/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod config; -pub mod flag; diff --git a/src/domain/email/message/mod.rs b/src/domain/email/message/mod.rs deleted file mode 100644 index ef68c36..0000000 --- a/src/domain/email/message/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod config; diff --git a/src/domain/email/mod.rs b/src/domain/email/mod.rs deleted file mode 100644 index 5de2dfe..0000000 --- a/src/domain/email/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod args; -pub mod envelope; -pub mod handlers; -pub mod message; diff --git a/src/domain/envelope/envelope.rs b/src/domain/envelope/envelope.rs deleted file mode 100644 index c683de2..0000000 --- a/src/domain/envelope/envelope.rs +++ /dev/null @@ -1,66 +0,0 @@ -use serde::Serialize; - -use crate::{ - ui::{Cell, Row, Table}, - Flag, Flags, -}; - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Mailbox { - pub name: Option, - pub addr: String, -} - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Envelope { - pub id: String, - pub flags: Flags, - pub subject: String, - pub from: Mailbox, - pub date: String, -} - -impl Table for Envelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("ID").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("FROM").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let id = self.id.to_string(); - let unseen = !self.flags.contains(&Flag::Seen); - let flags = { - let mut flags = String::new(); - flags.push_str(if !unseen { " " } else { "✷" }); - flags.push_str(if self.flags.contains(&Flag::Answered) { - "↵" - } else { - " " - }); - flags.push_str(if self.flags.contains(&Flag::Flagged) { - "⚑" - } else { - " " - }); - flags - }; - let subject = &self.subject; - let sender = if let Some(name) = &self.from.name { - name - } else { - &self.from.addr - }; - let date = &self.date; - - Row::new() - .cell(Cell::new(id).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} diff --git a/src/domain/envelope/mod.rs b/src/domain/envelope/mod.rs deleted file mode 100644 index a893d38..0000000 --- a/src/domain/envelope/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod envelope; -pub mod envelopes; - -pub use envelope::*; -pub use envelopes::*; diff --git a/src/domain/flag/flags.rs b/src/domain/flag/flags.rs deleted file mode 100644 index d0c30d2..0000000 --- a/src/domain/flag/flags.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serde::Serialize; -use std::{collections::HashSet, ops}; - -use crate::Flag; - -#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] -pub struct Flags(pub HashSet); - -impl ops::Deref for Flags { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for Flags { - fn from(flags: email::flag::Flags) -> Self { - Flags(flags.iter().map(Flag::from).collect()) - } -} diff --git a/src/domain/flag/mod.rs b/src/domain/flag/mod.rs deleted file mode 100644 index 5d92934..0000000 --- a/src/domain/flag/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod args; -pub mod handlers; - -pub mod flag; -pub use flag::*; - -pub mod flags; -pub use flags::*; diff --git a/src/domain/folder/folder.rs b/src/domain/folder/folder.rs deleted file mode 100644 index 7d1e410..0000000 --- a/src/domain/folder/folder.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde::Serialize; - -use crate::ui::{Cell, Row, Table}; - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Folder { - pub name: String, - pub desc: String, -} - -impl From<&email::folder::Folder> for Folder { - fn from(folder: &email::folder::Folder) -> Self { - Folder { - name: folder.name.clone(), - desc: folder.desc.clone(), - } - } -} - -impl Table for Folder { - fn head() -> Row { - Row::new() - .cell(Cell::new("NAME").bold().underline().white()) - .cell(Cell::new("DESC").bold().underline().white()) - } - - fn row(&self) -> Row { - Row::new() - .cell(Cell::new(&self.name).blue()) - .cell(Cell::new(&self.desc).green()) - } -} diff --git a/src/domain/folder/folders.rs b/src/domain/folder/folders.rs deleted file mode 100644 index dd5cc1d..0000000 --- a/src/domain/folder/folders.rs +++ /dev/null @@ -1,35 +0,0 @@ -use anyhow::Result; -use serde::Serialize; -use std::ops; - -use crate::{ - printer::{PrintTable, PrintTableOpts, WriteColor}, - ui::Table, - Folder, -}; - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Folders(Vec); - -impl ops::Deref for Folders { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for Folders { - fn from(folders: email::folder::Folders) -> Self { - Folders(folders.iter().map(Folder::from).collect()) - } -} - -impl PrintTable for Folders { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} diff --git a/src/domain/folder/mod.rs b/src/domain/folder/mod.rs deleted file mode 100644 index 321a172..0000000 --- a/src/domain/folder/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod args; -pub mod config; -pub mod folder; -pub mod folders; -pub mod handlers; - -pub use folder::*; -pub use folders::*; diff --git a/src/domain/mod.rs b/src/domain/mod.rs deleted file mode 100644 index 50ca65a..0000000 --- a/src/domain/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod account; -pub mod email; -pub mod envelope; -pub mod flag; -pub mod folder; -pub mod tpl; - -pub use self::account::{args, handlers, Account, Accounts}; -pub use self::email::*; -pub use self::envelope::*; -pub use self::flag::*; -pub use self::folder::*; -pub use self::tpl::*; diff --git a/src/email/envelope/args.rs b/src/email/envelope/args.rs new file mode 100644 index 0000000..717e079 --- /dev/null +++ b/src/email/envelope/args.rs @@ -0,0 +1,90 @@ +//! Email CLI module. +//! +//! This module contains the command matcher, the subcommands and the +//! arguments related to the email domain. + +use anyhow::Result; +use clap::{Arg, ArgMatches, Command}; + +use crate::ui::table; + +const ARG_PAGE: &str = "page"; +const ARG_PAGE_SIZE: &str = "page-size"; +const CMD_LIST: &str = "list"; +const CMD_ENVELOPE: &str = "envelope"; + +pub type Page = usize; +pub type PageSize = usize; + +/// Represents the email commands. +#[derive(Debug, PartialEq, Eq)] +pub enum Cmd { + List(table::args::MaxTableWidth, Option, Page), +} + +/// Email command matcher. +pub fn matches(m: &ArgMatches) -> Result> { + let cmd = if let Some(m) = m.subcommand_matches(CMD_ENVELOPE) { + if let Some(m) = m.subcommand_matches(CMD_LIST) { + let max_table_width = table::args::parse_max_width(m); + let page_size = parse_page_size_arg(m); + let page = parse_page_arg(m); + Some(Cmd::List(max_table_width, page_size, page)) + } else { + Some(Cmd::List(None, None, 0)) + } + } else { + None + }; + + Ok(cmd) +} + +/// Represents the envelope subcommand. +pub fn subcmd() -> Command { + Command::new(CMD_ENVELOPE) + .about("Manage envelopes") + .subcommands([Command::new(CMD_LIST) + .alias("lst") + .about("List envelopes") + .arg(page_size_arg()) + .arg(page_arg()) + .arg(table::args::max_width())]) +} + +/// Represents the page size argument. +fn page_size_arg() -> Arg { + Arg::new(ARG_PAGE_SIZE) + .help("Page size") + .long("page-size") + .short('s') + .value_name("INT") +} + +/// Represents the page size argument parser. +fn parse_page_size_arg(matches: &ArgMatches) -> Option { + matches + .get_one::(ARG_PAGE_SIZE) + .and_then(|s| s.parse().ok()) +} + +/// Represents the page argument. +fn page_arg() -> Arg { + Arg::new(ARG_PAGE) + .help("Page number") + .short('p') + .long("page") + .value_name("INT") + .default_value("1") +} + +/// Represents the page argument parser. +fn parse_page_arg(matches: &ArgMatches) -> usize { + matches + .get_one::(ARG_PAGE) + .unwrap() + .parse() + .ok() + .map(|page| 1.max(page) - 1) + .unwrap_or_default() +} diff --git a/src/domain/email/envelope/config.rs b/src/email/envelope/config.rs similarity index 100% rename from src/domain/email/envelope/config.rs rename to src/email/envelope/config.rs diff --git a/src/domain/flag/args.rs b/src/email/envelope/flag/args.rs similarity index 58% rename from src/domain/flag/args.rs rename to src/email/envelope/flag/args.rs index bf3d438..fd80935 100644 --- a/src/domain/flag/args.rs +++ b/src/email/envelope/flag/args.rs @@ -8,7 +8,7 @@ use anyhow::Result; use clap::{Arg, ArgMatches, Command}; use log::{debug, info}; -use crate::email; +use crate::message; const ARG_FLAGS: &str = "flag"; @@ -21,28 +21,32 @@ pub(crate) const CMD_FLAG: &str = "flags"; /// Represents the flag commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - Add(email::args::Ids<'a>, Flags), - Remove(email::args::Ids<'a>, Flags), - Set(email::args::Ids<'a>, Flags), + Add(message::args::Ids<'a>, Flags), + Remove(message::args::Ids<'a>, Flags), + Set(message::args::Ids<'a>, Flags), } /// Represents the flag command matcher. -pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) { - debug!("add flags command matched"); - let ids = email::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Add(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_REMOVE) { - info!("remove flags command matched"); - let ids = email::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Remove(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_SET) { - debug!("set flags command matched"); - let ids = email::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Set(ids, flags)) +pub fn matches(m: &ArgMatches) -> Result> { + let cmd = if let Some(m) = m.subcommand_matches(CMD_FLAG) { + if let Some(m) = m.subcommand_matches(CMD_ADD) { + debug!("add flags command matched"); + let ids = message::args::parse_ids_arg(m); + let flags = parse_flags_arg(m); + Some(Cmd::Add(ids, flags)) + } else if let Some(m) = m.subcommand_matches(CMD_REMOVE) { + info!("remove flags command matched"); + let ids = message::args::parse_ids_arg(m); + let flags = parse_flags_arg(m); + Some(Cmd::Remove(ids, flags)) + } else if let Some(m) = m.subcommand_matches(CMD_SET) { + debug!("set flags command matched"); + let ids = message::args::parse_ids_arg(m); + let flags = parse_flags_arg(m); + Some(Cmd::Set(ids, flags)) + } else { + None + } } else { None }; @@ -50,32 +54,32 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { Ok(cmd) } -/// Represents the flag subcommands. -pub fn subcmds<'a>() -> Vec { - vec![Command::new(CMD_FLAG) - .about("Handles email flags") +/// Represents the flag subcommand. +pub fn subcmd() -> Command { + Command::new(CMD_FLAG) + .about("Manage flags") .subcommand_required(true) .arg_required_else_help(true) .subcommand( Command::new(CMD_ADD) .about("Adds flags to an email") - .arg(email::args::ids_arg()) + .arg(message::args::ids_arg()) .arg(flags_arg()), ) .subcommand( Command::new(CMD_REMOVE) .aliases(["delete", "del", "d"]) .about("Removes flags from an email") - .arg(email::args::ids_arg()) + .arg(message::args::ids_arg()) .arg(flags_arg()), ) .subcommand( Command::new(CMD_SET) .aliases(["change", "c"]) .about("Sets flags of an email") - .arg(email::args::ids_arg()) + .arg(message::args::ids_arg()) .arg(flags_arg()), - )] + ) } /// Represents the flags argument. diff --git a/src/domain/email/envelope/flag/config.rs b/src/email/envelope/flag/config.rs similarity index 100% rename from src/domain/email/envelope/flag/config.rs rename to src/email/envelope/flag/config.rs diff --git a/src/domain/flag/handlers.rs b/src/email/envelope/flag/handlers.rs similarity index 100% rename from src/domain/flag/handlers.rs rename to src/email/envelope/flag/handlers.rs diff --git a/src/domain/flag/flag.rs b/src/email/envelope/flag/mod.rs similarity index 56% rename from src/domain/flag/flag.rs rename to src/email/envelope/flag/mod.rs index bf3712a..2416abc 100644 --- a/src/domain/flag/flag.rs +++ b/src/email/envelope/flag/mod.rs @@ -1,4 +1,9 @@ +pub mod args; +pub mod config; +pub mod handlers; + use serde::Serialize; +use std::{collections::HashSet, ops}; /// Represents the flag variants. #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)] @@ -24,3 +29,20 @@ impl From<&email::flag::Flag> for Flag { } } } + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +pub struct Flags(pub HashSet); + +impl ops::Deref for Flags { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Flags { + fn from(flags: email::flag::Flags) -> Self { + Flags(flags.iter().map(Flag::from).collect()) + } +} diff --git a/src/email/envelope/handlers.rs b/src/email/envelope/handlers.rs new file mode 100644 index 0000000..0477a66 --- /dev/null +++ b/src/email/envelope/handlers.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use email::account::config::AccountConfig; +use log::{debug, trace}; + +use crate::{ + backend::Backend, + printer::{PrintTableOpts, Printer}, +}; + +pub async fn list( + config: &AccountConfig, + printer: &mut P, + backend: &Backend, + folder: &str, + max_width: Option, + page_size: Option, + page: usize, +) -> Result<()> { + let page_size = page_size.unwrap_or(config.email_listing_page_size()); + debug!("page size: {}", page_size); + + let envelopes = backend.list_envelopes(&folder, page_size, page).await?; + trace!("envelopes: {:?}", envelopes); + + printer.print_table( + Box::new(envelopes), + PrintTableOpts { + format: &config.email_reading_format, + max_width, + }, + ) +} diff --git a/src/domain/envelope/envelopes.rs b/src/email/envelope/mod.rs similarity index 64% rename from src/domain/envelope/envelopes.rs rename to src/email/envelope/mod.rs index ee1fdd3..b43c520 100644 --- a/src/domain/envelope/envelopes.rs +++ b/src/email/envelope/mod.rs @@ -1,14 +1,80 @@ +pub mod args; +pub mod config; +pub mod flag; +pub mod handlers; + use anyhow::Result; use email::account::config::AccountConfig; use serde::Serialize; use std::ops; use crate::{ + cache::IdMapper, + flag::{Flag, Flags}, printer::{PrintTable, PrintTableOpts, WriteColor}, - ui::Table, - Envelope, IdMapper, Mailbox, + ui::{Cell, Row, Table}, }; +#[derive(Clone, Debug, Default, Serialize)] +pub struct Mailbox { + pub name: Option, + pub addr: String, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Envelope { + pub id: String, + pub flags: Flags, + pub subject: String, + pub from: Mailbox, + pub date: String, +} + +impl Table for Envelope { + fn head() -> Row { + Row::new() + .cell(Cell::new("ID").bold().underline().white()) + .cell(Cell::new("FLAGS").bold().underline().white()) + .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) + .cell(Cell::new("FROM").bold().underline().white()) + .cell(Cell::new("DATE").bold().underline().white()) + } + + fn row(&self) -> Row { + let id = self.id.to_string(); + let unseen = !self.flags.contains(&Flag::Seen); + let flags = { + let mut flags = String::new(); + flags.push_str(if !unseen { " " } else { "✷" }); + flags.push_str(if self.flags.contains(&Flag::Answered) { + "↵" + } else { + " " + }); + flags.push_str(if self.flags.contains(&Flag::Flagged) { + "⚑" + } else { + " " + }); + flags + }; + let subject = &self.subject; + let sender = if let Some(name) = &self.from.name { + name + } else { + &self.from.addr + }; + let date = &self.date; + + Row::new() + .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(flags).bold_if(unseen).white()) + .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) + .cell(Cell::new(sender).bold_if(unseen).blue()) + .cell(Cell::new(date).bold_if(unseen).yellow()) + } +} + /// Represents the list of envelopes. #[derive(Clone, Debug, Default, Serialize)] pub struct Envelopes(Vec); @@ -62,7 +128,9 @@ mod tests { use email::account::config::AccountConfig; use std::env; - use crate::{Envelopes, IdMapper}; + use crate::cache::IdMapper; + + use super::Envelopes; #[test] fn default_datetime_fmt() { diff --git a/src/domain/email/args.rs b/src/email/message/args.rs similarity index 57% rename from src/domain/email/args.rs rename to src/email/message/args.rs index 512f58b..92eb099 100644 --- a/src/domain/email/args.rs +++ b/src/email/message/args.rs @@ -6,15 +6,13 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; -use crate::{flag, folder, tpl, ui::table}; +use crate::{folder, template}; const ARG_CRITERIA: &str = "criterion"; const ARG_HEADERS: &str = "headers"; const ARG_ID: &str = "id"; const ARG_IDS: &str = "ids"; const ARG_MIME_TYPE: &str = "mime-type"; -const ARG_PAGE: &str = "page"; -const ARG_PAGE_SIZE: &str = "page-size"; const ARG_QUERY: &str = "query"; const ARG_RAW: &str = "raw"; const ARG_REPLY_ALL: &str = "reply-all"; @@ -22,14 +20,12 @@ const CMD_ATTACHMENTS: &str = "attachments"; const CMD_COPY: &str = "copy"; const CMD_DELETE: &str = "delete"; const CMD_FORWARD: &str = "forward"; -const CMD_LIST: &str = "list"; +const CMD_MESSAGE: &str = "message"; const CMD_MOVE: &str = "move"; const CMD_READ: &str = "read"; const CMD_REPLY: &str = "reply"; const CMD_SAVE: &str = "save"; -const CMD_SEARCH: &str = "search"; const CMD_SEND: &str = "send"; -const CMD_SORT: &str = "sort"; const CMD_WRITE: &str = "write"; pub type All = bool; @@ -38,8 +34,6 @@ pub type Folder<'a> = &'a str; pub type Headers<'a> = Vec<&'a str>; pub type Id<'a> = &'a str; pub type Ids<'a> = Vec<&'a str>; -pub type Page = usize; -pub type PageSize = usize; pub type Query = String; pub type Raw = bool; pub type RawEmail = String; @@ -51,131 +45,93 @@ pub enum Cmd<'a> { Attachments(Ids<'a>), Copy(Ids<'a>, Folder<'a>), Delete(Ids<'a>), - Flag(Option>), - Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>), - List(table::args::MaxTableWidth, Option, Page), + Forward( + Id<'a>, + template::args::Headers<'a>, + template::args::Body<'a>, + ), Move(Ids<'a>, Folder<'a>), Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>), - Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>), - Save(RawEmail), - Search(Query, table::args::MaxTableWidth, Option, Page), - Send(RawEmail), - Sort( - Criteria, - Query, - table::args::MaxTableWidth, - Option, - Page, + Reply( + Id<'a>, + All, + template::args::Headers<'a>, + template::args::Body<'a>, ), - Tpl(Option>), - Write(tpl::args::Headers<'a>, tpl::args::Body<'a>), + Save(RawEmail), + Send(RawEmail), + Write(template::args::Headers<'a>, template::args::Body<'a>), } /// Email command matcher. -pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { - let ids = parse_ids_arg(m); - Cmd::Attachments(ids) - } else if let Some(m) = m.subcommand_matches(CMD_COPY) { - let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); - Cmd::Copy(ids, folder) - } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { - let ids = parse_ids_arg(m); - Cmd::Delete(ids) - } else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) { - Cmd::Flag(flag::args::matches(m)?) - } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - let id = parse_id_arg(m); - let headers = tpl::args::parse_headers_arg(m); - let body = tpl::args::parse_body_arg(m); - Cmd::Forward(id, headers, body) - } else if let Some(m) = m.subcommand_matches(CMD_LIST) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - Cmd::List(max_table_width, page_size, page) - } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { - let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); - Cmd::Move(ids, folder) - } else if let Some(m) = m.subcommand_matches(CMD_READ) { - let ids = parse_ids_arg(m); - let mime = parse_mime_type_arg(m); - let raw = parse_raw_flag(m); - let headers = parse_headers_arg(m); - Cmd::Read(ids, mime, raw, headers) - } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - let id = parse_id_arg(m); - let all = parse_reply_all_flag(m); - let headers = tpl::args::parse_headers_arg(m); - let body = tpl::args::parse_body_arg(m); - Cmd::Reply(id, all, headers, body) - } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { - let email = parse_raw_arg(m); - Cmd::Save(email) - } else if let Some(m) = m.subcommand_matches(CMD_SEARCH) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - let query = parse_query_arg(m); - Cmd::Search(query, max_table_width, page_size, page) - } else if let Some(m) = m.subcommand_matches(CMD_SORT) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - let criteria = parse_criteria_arg(m); - let query = parse_query_arg(m); - Cmd::Sort(criteria, query, max_table_width, page_size, page) - } else if let Some(m) = m.subcommand_matches(CMD_SEND) { - let email = parse_raw_arg(m); - Cmd::Send(email) - } else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) { - Cmd::Tpl(tpl::args::matches(m)?) - } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { - let headers = tpl::args::parse_headers_arg(m); - let body = tpl::args::parse_body_arg(m); - Cmd::Write(headers, body) +pub fn matches(m: &ArgMatches) -> Result> { + let cmd = if let Some(m) = m.subcommand_matches(CMD_MESSAGE) { + if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { + let ids = parse_ids_arg(m); + Some(Cmd::Attachments(ids)) + } else if let Some(m) = m.subcommand_matches(CMD_COPY) { + let ids = parse_ids_arg(m); + let folder = folder::args::parse_target_arg(m); + Some(Cmd::Copy(ids, folder)) + } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { + let ids = parse_ids_arg(m); + Some(Cmd::Delete(ids)) + } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { + let id = parse_id_arg(m); + let headers = template::args::parse_headers_arg(m); + let body = template::args::parse_body_arg(m); + Some(Cmd::Forward(id, headers, body)) + } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { + let ids = parse_ids_arg(m); + let folder = folder::args::parse_target_arg(m); + Some(Cmd::Move(ids, folder)) + } else if let Some(m) = m.subcommand_matches(CMD_READ) { + let ids = parse_ids_arg(m); + let mime = parse_mime_type_arg(m); + let raw = parse_raw_flag(m); + let headers = parse_headers_arg(m); + Some(Cmd::Read(ids, mime, raw, headers)) + } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { + let id = parse_id_arg(m); + let all = parse_reply_all_flag(m); + let headers = template::args::parse_headers_arg(m); + let body = template::args::parse_body_arg(m); + Some(Cmd::Reply(id, all, headers, body)) + } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { + let email = parse_raw_arg(m); + Some(Cmd::Save(email)) + } else if let Some(m) = m.subcommand_matches(CMD_SEND) { + let email = parse_raw_arg(m); + Some(Cmd::Send(email)) + } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { + let headers = template::args::parse_headers_arg(m); + let body = template::args::parse_body_arg(m); + Some(Cmd::Write(headers, body)) + } else { + None + } } else { - Cmd::List(None, None, 0) + None }; - Ok(Some(cmd)) + Ok(cmd) } /// Represents the email subcommands. -pub fn subcmds() -> Vec { - vec![ - flag::args::subcmds(), - tpl::args::subcmds(), - vec![ +pub fn subcmd() -> Command { + Command::new(CMD_MESSAGE) + .about("Manage messages") + .aliases(["msg"]) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommands([ Command::new(CMD_ATTACHMENTS) .about("Downloads all emails attachments") .arg(ids_arg()), - Command::new(CMD_LIST) - .alias("lst") - .about("List envelopes") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()), - Command::new(CMD_SEARCH) - .aliases(["query", "q"]) - .about("Filter envelopes matching the given query") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()) - .arg(query_arg()), - Command::new(CMD_SORT) - .about("Sort envelopes by the given criteria and matching the given query") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()) - .arg(criteria_arg()) - .arg(query_arg()), Command::new(CMD_WRITE) .about("Write a new email") .aliases(["new", "n"]) - .args(tpl::args::args()), + .args(template::args::args()), Command::new(CMD_SEND) .about("Send a raw email") .arg(raw_arg()), @@ -191,12 +147,12 @@ pub fn subcmds() -> Vec { Command::new(CMD_REPLY) .about("Answer to an email") .arg(reply_all_flag()) - .args(tpl::args::args()) + .args(template::args::args()) .arg(id_arg()), Command::new(CMD_FORWARD) .aliases(["fwd", "f"]) .about("Forward an email") - .args(tpl::args::args()) + .args(template::args::args()) .arg(id_arg()), Command::new(CMD_COPY) .alias("cp") @@ -212,9 +168,7 @@ pub fn subcmds() -> Vec { .aliases(["remove", "rm"]) .about("Delete emails") .arg(ids_arg()), - ], - ] - .concat() + ]) } /// Represents the email id argument. @@ -305,43 +259,6 @@ pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool { matches.get_flag(ARG_REPLY_ALL) } -/// Represents the page size argument. -fn page_size_arg() -> Arg { - Arg::new(ARG_PAGE_SIZE) - .help("Page size") - .long("page-size") - .short('s') - .value_name("INT") -} - -/// Represents the page size argument parser. -fn parse_page_size_arg(matches: &ArgMatches) -> Option { - matches - .get_one::(ARG_PAGE_SIZE) - .and_then(|s| s.parse().ok()) -} - -/// Represents the page argument. -fn page_arg() -> Arg { - Arg::new(ARG_PAGE) - .help("Page number") - .short('p') - .long("page") - .value_name("INT") - .default_value("1") -} - -/// Represents the page argument parser. -fn parse_page_arg(matches: &ArgMatches) -> usize { - matches - .get_one::(ARG_PAGE) - .unwrap() - .parse() - .ok() - .map(|page| 1.max(page) - 1) - .unwrap_or_default() -} - /// Represents the email headers argument. pub fn headers_arg() -> Arg { Arg::new(ARG_HEADERS) diff --git a/src/domain/email/message/config.rs b/src/email/message/config.rs similarity index 100% rename from src/domain/email/message/config.rs rename to src/email/message/config.rs diff --git a/src/domain/email/handlers.rs b/src/email/message/handlers.rs similarity index 78% rename from src/domain/email/handlers.rs rename to src/email/message/handlers.rs index b835ce8..01fa6f2 100644 --- a/src/domain/email/handlers.rs +++ b/src/email/message/handlers.rs @@ -4,7 +4,7 @@ use email::{ account::config::AccountConfig, envelope::Id, flag::Flag, message::Message, template::FilterParts, }; -use log::{debug, trace}; +use log::trace; use mail_builder::MessageBuilder; use std::{ fs, @@ -13,11 +13,7 @@ use std::{ use url::Url; use uuid::Uuid; -use crate::{ - backend::Backend, - printer::{PrintTableOpts, Printer}, - ui::editor, -}; +use crate::{backend::Backend, printer::Printer, ui::editor}; pub async fn attachments( config: &AccountConfig, @@ -120,30 +116,6 @@ pub async fn forward( Ok(()) } -pub async fn list( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - max_width: Option, - page_size: Option, - page: usize, -) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - debug!("page size: {}", page_size); - - let envelopes = backend.list_envelopes(&folder, page_size, page).await?; - trace!("envelopes: {:?}", envelopes); - - printer.print_table( - Box::new(envelopes), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} - /// Parses and edits a message from a [mailto] URL string. /// /// [mailto]: https://en.wikipedia.org/wiki/Mailto @@ -284,61 +256,6 @@ pub async fn save( Ok(()) } -pub async fn search( - _config: &AccountConfig, - _printer: &mut P, - _backend: &Backend, - _folder: &str, - _query: String, - _max_width: Option, - _page_size: Option, - _page: usize, -) -> Result<()> { - todo!() - // let page_size = page_size.unwrap_or(config.email_listing_page_size()); - // let envelopes = Envelopes::from_backend( - // config, - // id_mapper, - // backend - // .search_envelopes(&folder, &query, "", page_size, page) - // .await?, - // )?; - // let opts = PrintTableOpts { - // format: &config.email_reading_format, - // max_width, - // }; - - // printer.print_table(Box::new(envelopes), opts) -} - -pub async fn sort( - _config: &AccountConfig, - _printer: &mut P, - _backend: &Backend, - _folder: &str, - _sort: String, - _query: String, - _max_width: Option, - _page_size: Option, - _page: usize, -) -> Result<()> { - todo!() - // let page_size = page_size.unwrap_or(config.email_listing_page_size()); - // let envelopes = Envelopes::from_backend( - // config, - // id_mapper, - // backend - // .search_envelopes(&folder, &query, &sort, page_size, page) - // .await?, - // )?; - // let opts = PrintTableOpts { - // format: &config.email_reading_format, - // max_width, - // }; - - // printer.print_table(Box::new(envelopes), opts) -} - pub async fn send( config: &AccountConfig, printer: &mut P, diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs new file mode 100644 index 0000000..c101d77 --- /dev/null +++ b/src/email/message/mod.rs @@ -0,0 +1,4 @@ +pub mod args; +pub mod config; +pub mod handlers; +pub mod template; diff --git a/src/domain/tpl/args.rs b/src/email/message/template/args.rs similarity index 78% rename from src/domain/tpl/args.rs rename to src/email/message/template/args.rs index 71ae87b..7664aae 100644 --- a/src/domain/tpl/args.rs +++ b/src/email/message/template/args.rs @@ -7,7 +7,7 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; use log::warn; -use crate::email; +use crate::message; const ARG_BODY: &str = "body"; const ARG_HEADERS: &str = "headers"; @@ -27,9 +27,14 @@ pub type Body<'a> = Option<&'a str>; /// Represents the template commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - Forward(email::args::Id<'a>, Headers<'a>, Body<'a>), + Forward(message::args::Id<'a>, Headers<'a>, Body<'a>), Write(Headers<'a>, Body<'a>), - Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>), + Reply( + message::args::Id<'a>, + message::args::All, + Headers<'a>, + Body<'a>, + ), Save(RawTpl), Send(RawTpl), } @@ -37,13 +42,13 @@ pub enum Cmd<'a> { /// Represents the template command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - let id = email::args::parse_id_arg(m); + let id = message::args::parse_id_arg(m); let headers = parse_headers_arg(m); let body = parse_body_arg(m); Some(Cmd::Forward(id, headers, body)) } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - let id = email::args::parse_id_arg(m); - let all = email::args::parse_reply_all_flag(m); + let id = message::args::parse_id_arg(m); + let all = message::args::parse_reply_all_flag(m); let headers = parse_headers_arg(m); let body = parse_body_arg(m); Some(Cmd::Reply(id, all, headers, body)) @@ -65,55 +70,55 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } /// Represents the template subcommands. -pub fn subcmds<'a>() -> Vec { - vec![Command::new(CMD_TPL) +pub fn subcmd() -> Command { + Command::new(CMD_TPL) .alias("tpl") - .about("Handles email templates") + .about("Manage templates") .subcommand_required(true) .arg_required_else_help(true) .subcommand( Command::new(CMD_FORWARD) .alias("fwd") - .about("Generates a template for forwarding an email") - .arg(email::args::id_arg()) + .about("Generate a template for forwarding an email") + .arg(message::args::id_arg()) .args(&args()), ) .subcommand( Command::new(CMD_REPLY) - .about("Generates a template for replying to an email") - .arg(email::args::id_arg()) - .arg(email::args::reply_all_flag()) + .about("Generate a template for replying to an email") + .arg(message::args::id_arg()) + .arg(message::args::reply_all_flag()) .args(&args()), ) .subcommand( Command::new(CMD_SAVE) - .about("Compiles the template into a valid email then saves it") + .about("Compile the template into a valid email then saves it") .arg(Arg::new(ARG_TPL).raw(true)), ) .subcommand( Command::new(CMD_SEND) - .about("Compiles the template into a valid email then sends it") + .about("Compile the template into a valid email then sends it") .arg(Arg::new(ARG_TPL).raw(true)), ) .subcommand( Command::new(CMD_WRITE) .aliases(["new", "n"]) - .about("Generates a template for writing a new email") + .about("Generate a template for writing a new email") .args(&args()), - )] + ) } /// Represents the template arguments. pub fn args() -> Vec { vec![ Arg::new(ARG_HEADERS) - .help("Overrides a specific header") + .help("Override a specific header") .short('H') .long("header") .value_name("KEY:VAL") .action(ArgAction::Append), Arg::new(ARG_BODY) - .help("Overrides the body") + .help("Override the body") .short('B') .long("body") .value_name("STRING"), diff --git a/src/domain/tpl/handlers.rs b/src/email/message/template/handlers.rs similarity index 100% rename from src/domain/tpl/handlers.rs rename to src/email/message/template/handlers.rs diff --git a/src/domain/tpl/mod.rs b/src/email/message/template/mod.rs similarity index 100% rename from src/domain/tpl/mod.rs rename to src/email/message/template/mod.rs diff --git a/src/email/mod.rs b/src/email/mod.rs new file mode 100644 index 0000000..7eae3d8 --- /dev/null +++ b/src/email/mod.rs @@ -0,0 +1,5 @@ +pub mod envelope; +pub mod message; + +#[doc(inline)] +pub use self::{envelope::flag, message::template}; diff --git a/src/domain/folder/args.rs b/src/folder/args.rs similarity index 98% rename from src/domain/folder/args.rs rename to src/folder/args.rs index f83b28f..98f57aa 100644 --- a/src/domain/folder/args.rs +++ b/src/folder/args.rs @@ -19,7 +19,7 @@ const ARG_TARGET: &str = "target"; const CMD_CREATE: &str = "create"; const CMD_DELETE: &str = "delete"; const CMD_EXPUNGE: &str = "expunge"; -const CMD_FOLDERS: &str = "folders"; +const CMD_FOLDER: &str = "folder"; const CMD_LIST: &str = "list"; /// Represents the folder commands. @@ -33,7 +33,7 @@ pub enum Cmd { /// Represents the folder command matcher. pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) { + let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDER) { if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) { info!("expunge folder subcommand matched"); Some(Cmd::Expunge) @@ -60,7 +60,7 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the folder subcommand. pub fn subcmd() -> Command { - Command::new(CMD_FOLDERS) + Command::new(CMD_FOLDER) .about("Manage folders") .subcommands([ Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), diff --git a/src/domain/folder/config.rs b/src/folder/config.rs similarity index 100% rename from src/domain/folder/config.rs rename to src/folder/config.rs diff --git a/src/domain/folder/handlers.rs b/src/folder/handlers.rs similarity index 98% rename from src/domain/folder/handlers.rs rename to src/folder/handlers.rs index 44be274..e43d34a 100644 --- a/src/domain/folder/handlers.rs +++ b/src/folder/handlers.rs @@ -10,9 +10,10 @@ use std::process; use crate::{ backend::Backend, printer::{PrintTableOpts, Printer}, - Folders, }; +use super::Folders; + pub async fn expunge(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { backend.expunge_folder(folder).await?; printer.print(format!("Folder {folder} successfully expunged!")) @@ -58,10 +59,12 @@ pub async fn delete(printer: &mut P, backend: &Backend, folder: &str mod tests { use async_trait::async_trait; use email::{ - account::AccountConfig, + account::config::AccountConfig, backend::Backend, - email::{Envelope, Envelopes, Flags, Messages}, + envelope::{Envelope, Envelopes}, + flag::Flags, folder::{Folder, Folders}, + message::Messages, }; use std::{any::Any, fmt::Debug, io}; use termcolor::ColorSpec; diff --git a/src/folder/mod.rs b/src/folder/mod.rs new file mode 100644 index 0000000..e448920 --- /dev/null +++ b/src/folder/mod.rs @@ -0,0 +1,67 @@ +pub mod args; +pub mod config; +pub mod handlers; + +use anyhow::Result; +use serde::Serialize; +use std::ops; + +use crate::{ + printer::{PrintTable, PrintTableOpts, WriteColor}, + ui::{Cell, Row, Table}, +}; + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Folder { + pub name: String, + pub desc: String, +} + +impl From<&email::folder::Folder> for Folder { + fn from(folder: &email::folder::Folder) -> Self { + Folder { + name: folder.name.clone(), + desc: folder.desc.clone(), + } + } +} + +impl Table for Folder { + fn head() -> Row { + Row::new() + .cell(Cell::new("NAME").bold().underline().white()) + .cell(Cell::new("DESC").bold().underline().white()) + } + + fn row(&self) -> Row { + Row::new() + .cell(Cell::new(&self.name).blue()) + .cell(Cell::new(&self.desc).green()) + } +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Folders(Vec); + +impl ops::Deref for Folders { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Folders { + fn from(folders: email::folder::Folders) -> Self { + Folders(folders.iter().map(Folder::from).collect()) + } +} + +impl PrintTable for Folders { + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 52e83b6..79c29ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,24 @@ +pub mod account; pub mod backend; pub mod cache; -pub mod compl; +pub mod completion; pub mod config; -pub mod domain; +pub mod email; +pub mod folder; #[cfg(feature = "imap")] pub mod imap; +#[cfg(feature = "maildir")] pub mod maildir; pub mod man; #[cfg(feature = "notmuch")] pub mod notmuch; pub mod output; pub mod printer; +#[cfg(feature = "sendmail")] pub mod sendmail; #[cfg(feature = "smtp")] pub mod smtp; pub mod ui; -pub use cache::IdMapper; -pub use domain::*; +#[doc(inline)] +pub use email::{envelope, flag, message, template}; diff --git a/src/main.rs b/src/main.rs index ad2e1df..b399239 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,11 @@ use url::Url; use himalaya::{ account, backend::{Backend, BackendBuilder}, - cache, compl, + cache, completion, config::{self, TomlConfig}, - email, flag, folder, man, output, + envelope, flag, folder, man, message, output, printer::StdoutPrinter, - tpl, + template, }; fn create_app() -> Command { @@ -27,14 +27,16 @@ fn create_app() -> Command { .arg(cache::args::arg()) .args(output::args::args()) .arg(folder::args::source_arg()) - .subcommand(compl::args::subcmd()) + .subcommand(completion::args::subcmd()) .subcommand(man::args::subcmd()) .subcommand(account::args::subcmd()) .subcommand(folder::args::subcmd()) - .subcommands(email::args::subcmds()) + .subcommand(envelope::args::subcmd()) + .subcommand(flag::args::subcmd()) + .subcommand(message::args::subcmd()) + .subcommand(template::args::subcmd()) } -#[allow(clippy::single_match)] #[tokio::main] async fn main() -> Result<()> { #[cfg(not(target_os = "windows"))] @@ -58,22 +60,24 @@ async fn main() -> Result<()> { let backend = backend_builder.build().await?; let mut printer = StdoutPrinter::default(); - return email::handlers::mailto(&account_config, &backend, &mut printer, &url).await; + return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; } let app = create_app(); let m = app.get_matches(); - // check completion command before configs + // check completionetion command before configs // https://github.com/soywod/himalaya/issues/115 - match compl::args::matches(&m)? { - Some(compl::args::Cmd::Generate(shell)) => { - return compl::handlers::generate(create_app(), shell); + #[allow(clippy::single_match)] + match completion::args::matches(&m)? { + Some(completion::args::Cmd::Generate(shell)) => { + return completion::handlers::generate(create_app(), shell); } _ => (), } // check also man command before configs + #[allow(clippy::single_match)] match man::args::matches(&m)? { Some(man::args::Cmd::GenerateAll(dir)) => { return man::handlers::generate(dir, create_app()); @@ -170,48 +174,11 @@ async fn main() -> Result<()> { _ => (), } - // checks email commands - match email::args::matches(&m)? { - Some(email::args::Cmd::Attachments(ids)) => { + match envelope::args::matches(&m)? { + Some(envelope::args::Cmd::List(max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::attachments( - &account_config, - &mut printer, - &backend, - &folder, - ids, - ) - .await; - } - Some(email::args::Cmd::Copy(ids, to_folder)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; - } - Some(email::args::Cmd::Delete(ids)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::delete(&mut printer, &backend, &folder, ids).await; - } - Some(email::args::Cmd::Forward(id, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::forward( - &account_config, - &mut printer, - &backend, - &folder, - id, - headers, - body, - ) - .await; - } - Some(email::args::Cmd::List(max_width, page_size, page)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::list( + return envelope::handlers::list( &account_config, &mut printer, &backend, @@ -222,15 +189,74 @@ async fn main() -> Result<()> { ) .await; } - Some(email::args::Cmd::Move(ids, to_folder)) => { + _ => (), + } + + match flag::args::matches(&m)? { + Some(flag::args::Cmd::Set(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; + return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; } - Some(email::args::Cmd::Read(ids, text_mime, raw, headers)) => { + Some(flag::args::Cmd::Add(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::read( + return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; + } + Some(flag::args::Cmd::Remove(ids, ref flags)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; + } + _ => (), + } + + match message::args::matches(&m)? { + Some(message::args::Cmd::Attachments(ids)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::attachments( + &account_config, + &mut printer, + &backend, + &folder, + ids, + ) + .await; + } + Some(message::args::Cmd::Copy(ids, to_folder)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; + } + Some(message::args::Cmd::Delete(ids)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::delete(&mut printer, &backend, &folder, ids).await; + } + Some(message::args::Cmd::Forward(id, headers, body)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + return message::handlers::forward( + &account_config, + &mut printer, + &backend, + &folder, + id, + headers, + body, + ) + .await; + } + Some(message::args::Cmd::Move(ids, to_folder)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; + } + Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::read( &account_config, &mut printer, &backend, @@ -242,10 +268,10 @@ async fn main() -> Result<()> { ) .await; } - Some(email::args::Cmd::Reply(id, all, headers, body)) => { + Some(message::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::reply( + return message::handlers::reply( &account_config, &mut printer, &backend, @@ -257,119 +283,78 @@ async fn main() -> Result<()> { ) .await; } - Some(email::args::Cmd::Save(raw_email)) => { + Some(message::args::Cmd::Save(raw_email)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::save(&mut printer, &backend, &folder, raw_email).await; + return message::handlers::save(&mut printer, &backend, &folder, raw_email).await; } - Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { + Some(message::args::Cmd::Send(raw_email)) => { + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + return message::handlers::send(&account_config, &mut printer, &backend, raw_email) + .await; + } + Some(message::args::Cmd::Write(headers, body)) => { + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + return message::handlers::write( + &account_config, + &mut printer, + &backend, + headers, + body, + ) + .await; + } + _ => (), + } + + match template::args::matches(&m)? { + Some(template::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::search( + return template::handlers::forward( &account_config, &mut printer, &backend, &folder, - query, - max_width, - page_size, - page, + id, + headers, + body, ) .await; } - Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { + Some(template::args::Cmd::Write(headers, body)) => { + return template::handlers::write(&account_config, &mut printer, headers, body).await; + } + Some(template::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::sort( + return template::handlers::reply( &account_config, &mut printer, &backend, &folder, - criteria, - query, - max_width, - page_size, - page, + id, + all, + headers, + body, ) .await; } - Some(email::args::Cmd::Send(raw_email)) => { - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::send(&account_config, &mut printer, &backend, raw_email).await; + Some(template::args::Cmd::Save(template)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return template::handlers::save( + &account_config, + &mut printer, + &backend, + &folder, + template, + ) + .await; } - Some(email::args::Cmd::Flag(m)) => match m { - Some(flag::args::Cmd::Set(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; - } - Some(flag::args::Cmd::Add(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; - } - Some(flag::args::Cmd::Remove(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; - } - _ => (), - }, - Some(email::args::Cmd::Tpl(m)) => match m { - Some(tpl::args::Cmd::Forward(id, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return tpl::handlers::forward( - &account_config, - &mut printer, - &backend, - &folder, - id, - headers, - body, - ) - .await; - } - Some(tpl::args::Cmd::Write(headers, body)) => { - return tpl::handlers::write(&account_config, &mut printer, headers, body).await; - } - Some(tpl::args::Cmd::Reply(id, all, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return tpl::handlers::reply( - &account_config, - &mut printer, - &backend, - &folder, - id, - all, - headers, - body, - ) - .await; - } - Some(tpl::args::Cmd::Save(tpl)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return tpl::handlers::save(&account_config, &mut printer, &backend, &folder, tpl) - .await; - } - Some(tpl::args::Cmd::Send(tpl)) => { - let backend = - Backend::new(toml_account_config, account_config.clone(), true).await?; - return tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await; - } - _ => (), - }, - Some(email::args::Cmd::Write(headers, body)) => { + Some(template::args::Cmd::Send(template)) => { let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::write(&account_config, &mut printer, &backend, headers, body) + return template::handlers::send(&account_config, &mut printer, &backend, template) .await; } _ => (),