reorganize folder and cli structure

This commit is contained in:
Clément DOUIN 2023-12-05 15:06:26 +01:00
parent 8b1a289f4d
commit 7a10a7fc25
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
49 changed files with 1555 additions and 1658 deletions

View file

@ -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<Option<Cmd>> {
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<Option<Cmd>> {
/// 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")

View file

@ -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.

View file

@ -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;

View file

@ -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)]

View file

@ -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")]

166
src/cache/id_mapper.rs vendored
View file

@ -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<Path>) -> 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<Self> {
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<I>(&self, id: I) -> Result<String>
where
I: AsRef<str>,
{
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<I>(&self, id: I) -> Result<String>
where
I: AsRef<str>,
{
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<i64> = stmt
.query_map([id], |row| row.get(0))
.with_context(|| format!("cannot get alias for id {id}"))?
.collect::<rusqlite::Result<_>>()
.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<A>(&self, alias: A) -> Result<String>
where
A: AsRef<str>,
{
let alias = alias.as_ref();
let alias = alias
.parse::<i64>()
.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<String> = stmt
.query_map([alias], |row| row.get(0))
.with_context(|| format!("cannot get id from alias {alias}"))?
.collect::<rusqlite::Result<_>>()
.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<A, I>(&self, aliases: I) -> Result<Vec<String>>
where
A: AsRef<str>,
I: IntoIterator<Item = A>,
{
aliases
.into_iter()
.map(|alias| self.get_id(alias))
.collect()
}
}

168
src/cache/mod.rs vendored
View file

@ -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<Path>) -> 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<Self> {
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<I>(&self, id: I) -> Result<String>
where
I: AsRef<str>,
{
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<I>(&self, id: I) -> Result<String>
where
I: AsRef<str>,
{
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<i64> = stmt
.query_map([id], |row| row.get(0))
.with_context(|| format!("cannot get alias for id {id}"))?
.collect::<rusqlite::Result<_>>()
.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<A>(&self, alias: A) -> Result<String>
where
A: AsRef<str>,
{
let alias = alias.as_ref();
let alias = alias
.parse::<i64>()
.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<String> = stmt
.query_map([alias], |row| row.get(0))
.with_context(|| format!("cannot get id from alias {alias}"))?
.collect::<rusqlite::Result<_>>()
.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<A, I>(&self, aliases: I) -> Result<Vec<String>>
where
A: AsRef<str>,
I: IntoIterator<Item = A>,
{
aliases
.into_iter()
.map(|alias| self.get_id(alias))
.collect()
}
}

View file

@ -32,7 +32,7 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// 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)])

View file

@ -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<String>,
pub signature_delim: Option<String>,
pub signature: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub folder_listing_page_size: Option<usize>,
pub folder_aliases: Option<HashMap<String, String>>,
pub email_listing_page_size: Option<usize>,
pub email_listing_datetime_fmt: Option<String>,
pub email_listing_datetime_local_tz: Option<bool>,
pub email_reading_headers: Option<Vec<String>>,
#[serde(
default,
with = "OptionEmailTextPlainFormatDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_format: Option<EmailTextPlainFormat>,
pub email_writing_headers: Option<Vec<String>>,
pub email_sending_save_copy: Option<bool>,
#[serde(
default,
with = "OptionEmailHooksDef",
skip_serializing_if = "Option::is_none"
)]
pub email_hooks: Option<EmailHooks>,
#[serde(flatten)]
pub accounts: HashMap<String, TomlAccountConfig>,
}
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<Self> {
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<Self> {
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<Self> {
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<impl Into<PathBuf>>) -> Result<Self> {
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<PathBuf> {
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<PathBuf> {
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<TomlConfig> {
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()
}
);
}
}

View file

@ -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<String>,
pub signature_delim: Option<String>,
pub signature: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub folder_listing_page_size: Option<usize>,
pub folder_aliases: Option<HashMap<String, String>>,
pub email_listing_page_size: Option<usize>,
pub email_listing_datetime_fmt: Option<String>,
pub email_listing_datetime_local_tz: Option<bool>,
pub email_reading_headers: Option<Vec<String>>,
#[serde(
default,
with = "OptionEmailTextPlainFormatDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_format: Option<EmailTextPlainFormat>,
pub email_writing_headers: Option<Vec<String>>,
pub email_sending_save_copy: Option<bool>,
#[serde(
default,
with = "OptionEmailHooksDef",
skip_serializing_if = "Option::is_none"
)]
pub email_hooks: Option<EmailHooks>,
#[serde(flatten)]
pub accounts: HashMap<String, TomlAccountConfig>,
}
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<Self> {
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<Self> {
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<Self> {
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<impl Into<PathBuf>>) -> Result<Self> {
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<PathBuf> {
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<PathBuf> {
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<TomlConfig> {
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()
}
);
}
}

View file

@ -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())
}
}

View file

@ -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::*;

View file

@ -1 +0,0 @@
pub mod config;

View file

@ -1,2 +0,0 @@
pub mod config;
pub mod flag;

View file

@ -1 +0,0 @@
pub mod config;

View file

@ -1,4 +0,0 @@
pub mod args;
pub mod envelope;
pub mod handlers;
pub mod message;

View file

@ -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<String>,
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())
}
}

View file

@ -1,5 +0,0 @@
pub mod envelope;
pub mod envelopes;
pub use envelope::*;
pub use envelopes::*;

View file

@ -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<Flag>);
impl ops::Deref for Flags {
type Target = HashSet<Flag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<email::flag::Flags> for Flags {
fn from(flags: email::flag::Flags) -> Self {
Flags(flags.iter().map(Flag::from).collect())
}
}

View file

@ -1,8 +0,0 @@
pub mod args;
pub mod handlers;
pub mod flag;
pub use flag::*;
pub mod flags;
pub use flags::*;

View file

@ -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())
}
}

View file

@ -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<Folder>);
impl ops::Deref for Folders {
type Target = Vec<Folder>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<email::folder::Folders> 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(())
}
}

View file

@ -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::*;

View file

@ -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::*;

View file

@ -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<PageSize>, Page),
}
/// Email command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
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<usize> {
matches
.get_one::<String>(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::<String>(ARG_PAGE)
.unwrap()
.parse()
.ok()
.map(|page| 1.max(page) - 1)
.unwrap_or_default()
}

View file

@ -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<Option<Cmd<'a>>> {
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<Option<Cmd>> {
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<Option<Cmd<'a>>> {
Ok(cmd)
}
/// Represents the flag subcommands.
pub fn subcmds<'a>() -> Vec<Command> {
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.

View file

@ -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<Flag>);
impl ops::Deref for Flags {
type Target = HashSet<Flag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<email::flag::Flags> for Flags {
fn from(flags: email::flag::Flags) -> Self {
Flags(flags.iter().map(Flag::from).collect())
}
}

View file

@ -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<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &Backend,
folder: &str,
max_width: Option<usize>,
page_size: Option<usize>,
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,
},
)
}

View file

@ -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<String>,
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<Envelope>);
@ -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() {

View file

@ -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<flag::args::Cmd<'a>>),
Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>),
List(table::args::MaxTableWidth, Option<PageSize>, 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<PageSize>, Page),
Send(RawEmail),
Sort(
Criteria,
Query,
table::args::MaxTableWidth,
Option<PageSize>,
Page,
Reply(
Id<'a>,
All,
template::args::Headers<'a>,
template::args::Body<'a>,
),
Tpl(Option<tpl::args::Cmd<'a>>),
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<Option<Cmd<'a>>> {
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<Option<Cmd>> {
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<Command> {
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> {
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<Command> {
.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<usize> {
matches
.get_one::<String>(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::<String>(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)

View file

@ -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<P: Printer>(
config: &AccountConfig,
@ -120,30 +116,6 @@ pub async fn forward<P: Printer>(
Ok(())
}
pub async fn list<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &Backend,
folder: &str,
max_width: Option<usize>,
page_size: Option<usize>,
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<P: Printer>(
Ok(())
}
pub async fn search<P: Printer>(
_config: &AccountConfig,
_printer: &mut P,
_backend: &Backend,
_folder: &str,
_query: String,
_max_width: Option<usize>,
_page_size: Option<usize>,
_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<P: Printer>(
_config: &AccountConfig,
_printer: &mut P,
_backend: &Backend,
_folder: &str,
_sort: String,
_query: String,
_max_width: Option<usize>,
_page_size: Option<usize>,
_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<P: Printer>(
config: &AccountConfig,
printer: &mut P,

4
src/email/message/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod args;
pub mod config;
pub mod handlers;
pub mod template;

View file

@ -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<Option<Cmd<'a>>> {
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<Option<Cmd<'a>>> {
}
/// Represents the template subcommands.
pub fn subcmds<'a>() -> Vec<Command> {
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<Arg> {
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"),

5
src/email/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod envelope;
pub mod message;
#[doc(inline)]
pub use self::{envelope::flag, message::template};

View file

@ -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<Option<Cmd>> {
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<Option<Cmd>> {
/// 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"),

View file

@ -10,9 +10,10 @@ use std::process;
use crate::{
backend::Backend,
printer::{PrintTableOpts, Printer},
Folders,
};
use super::Folders;
pub async fn expunge<P: Printer>(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<P: Printer>(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;

67
src/folder/mod.rs Normal file
View file

@ -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<Folder>);
impl ops::Deref for Folders {
type Target = Vec<Folder>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<email::folder::Folders> 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(())
}
}

View file

@ -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};

View file

@ -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;
}
_ => (),