extract account and config from cli to lib (#340)

This commit is contained in:
Clément DOUIN
2022-05-28 16:59:12 +02:00
parent 0e98def513
commit 3f5feed0ff
27 changed files with 219 additions and 103 deletions
+1
View File
@@ -31,6 +31,7 @@ clap = { version = "2.33.3", default-features = false, features = ["suggestions"
convert_case = "0.5.0"
env_logger = "0.8.3"
erased-serde = "0.3.18"
himalaya-lib = { path = "../lib" }
html-escape = "0.2.9"
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
log = "0.4.14"
+1 -1
View File
@@ -3,6 +3,7 @@
//! This module contains the definition of the IMAP backend.
use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::{AccountConfig, ImapBackendConfig};
use log::{debug, log_enabled, trace, Level};
use native_tls::{TlsConnector, TlsStream};
use std::{
@@ -16,7 +17,6 @@ use crate::{
backends::{
imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes,
},
config::{AccountConfig, ImapBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
output::run_cmd,
+1 -1
View File
@@ -4,12 +4,12 @@
//! traits implementation.
use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::{AccountConfig, MaildirBackendConfig};
use log::{debug, info, trace};
use std::{convert::TryInto, env, fs, path::PathBuf};
use crate::{
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
config::{AccountConfig, MaildirBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
};
+1 -1
View File
@@ -1,11 +1,11 @@
use std::{convert::TryInto, fs};
use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::{AccountConfig, NotmuchBackendConfig};
use log::{debug, info, trace};
use crate::{
backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes},
config::{AccountConfig, NotmuchBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
};
+2 -1
View File
@@ -12,8 +12,9 @@ use std::{
ops::Deref,
};
use himalaya_lib::account::DeserializedAccountConfig;
use crate::{
config::DeserializedAccountConfig,
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
-440
View File
@@ -1,440 +0,0 @@
use anyhow::{anyhow, Context, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, info, trace};
use mailparse::MailAddr;
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf};
use crate::{config::*, output::run_cmd};
/// Represents the user account.
#[derive(Debug, Default, Clone)]
pub struct AccountConfig {
/// Represents the name of the user account.
pub name: String,
/// Makes this account the default one.
pub default: bool,
/// Represents the display name of the user account.
pub display_name: String,
/// Represents the email address of the user account.
pub email: String,
/// Represents the downloads directory (mostly for attachments).
pub downloads_dir: PathBuf,
/// Represents the signature of the user.
pub sig: Option<String>,
/// Represents the default page size for listings.
pub default_page_size: usize,
/// Represents the notify command.
pub notify_cmd: Option<String>,
/// Overrides the default IMAP query "NEW" used to fetch new messages
pub notify_query: String,
/// Represents the watch commands.
pub watch_cmds: Vec<String>,
/// Represents the text/plain format as defined in the
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
pub format: Format,
/// Overrides the default headers displayed at the top of
/// the read message.
pub read_headers: Vec<String>,
/// Represents mailbox aliases.
pub mailboxes: HashMap<String, String>,
/// Represents hooks.
pub hooks: Hooks,
/// Represents the SMTP host.
pub smtp_host: String,
/// Represents the SMTP port.
pub smtp_port: u16,
/// Enables StartTLS.
pub smtp_starttls: bool,
/// Trusts any certificate.
pub smtp_insecure: bool,
/// Represents the SMTP login.
pub smtp_login: String,
/// Represents the SMTP password command.
pub smtp_passwd_cmd: String,
/// Represents the command used to encrypt a message.
pub pgp_encrypt_cmd: Option<String>,
/// Represents the command used to decrypt a message.
pub pgp_decrypt_cmd: Option<String>,
}
impl<'a> AccountConfig {
/// tries to create an account from a config and an optional account name.
pub fn from_config_and_opt_account_name(
config: &'a DeserializedConfig,
account_name: Option<&str>,
) -> Result<(AccountConfig, BackendConfig)> {
info!("begin: parsing account and backend configs from config and account name");
debug!("account name: {:?}", account_name.unwrap_or("default"));
let (name, account) = match account_name.map(|name| name.trim()) {
Some("default") | Some("") | None => config
.accounts
.iter()
.find(|(_, account)| match account {
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(account) => account.default.unwrap_or_default(),
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(account) => {
account.default.unwrap_or_default()
}
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(account) => {
account.default.unwrap_or_default()
}
})
.map(|(name, account)| (name.to_owned(), account))
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => config
.accounts
.get(name)
.map(|account| (name.to_owned(), account))
.ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)),
}?;
let base_account = account.to_base();
let downloads_dir = base_account
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or_else(|| {
config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
})
.unwrap_or_else(env::temp_dir);
let default_page_size = base_account
.default_page_size
.as_ref()
.or_else(|| config.default_page_size.as_ref())
.unwrap_or(&DEFAULT_PAGE_SIZE)
.to_owned();
let default_sig_delim = DEFAULT_SIG_DELIM.to_string();
let sig_delim = base_account
.signature_delimiter
.as_ref()
.or_else(|| config.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let sig = base_account
.signature
.as_ref()
.or_else(|| config.signature.as_ref());
let sig = sig
.and_then(|sig| shellexpand::full(sig).ok())
.map(String::from)
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| sig.map(|sig| sig.to_owned()))
.map(|sig| format!("{}{}", sig_delim, sig.trim_end()));
let account_config = AccountConfig {
name,
display_name: base_account
.name
.as_ref()
.unwrap_or(&config.name)
.to_owned(),
downloads_dir,
sig,
default_page_size,
notify_cmd: base_account
.notify_cmd
.as_ref()
.or_else(|| config.notify_cmd.as_ref())
.cloned(),
notify_query: base_account
.notify_query
.as_ref()
.or_else(|| config.notify_query.as_ref())
.unwrap_or(&String::from("NEW"))
.to_owned(),
watch_cmds: base_account
.watch_cmds
.as_ref()
.or_else(|| config.watch_cmds.as_ref())
.unwrap_or(&vec![])
.to_owned(),
format: base_account.format.unwrap_or_default(),
read_headers: base_account.read_headers,
mailboxes: base_account.mailboxes.clone(),
hooks: base_account.hooks.unwrap_or_default(),
default: base_account.default.unwrap_or_default(),
email: base_account.email.to_owned(),
smtp_host: base_account.smtp_host.to_owned(),
smtp_port: base_account.smtp_port,
smtp_starttls: base_account.smtp_starttls.unwrap_or_default(),
smtp_insecure: base_account.smtp_insecure.unwrap_or_default(),
smtp_login: base_account.smtp_login.to_owned(),
smtp_passwd_cmd: base_account.smtp_passwd_cmd.to_owned(),
pgp_encrypt_cmd: base_account.pgp_encrypt_cmd.to_owned(),
pgp_decrypt_cmd: base_account.pgp_decrypt_cmd.to_owned(),
};
trace!("account config: {:?}", account_config);
let backend_config = match account {
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(config) => BackendConfig::Imap(ImapBackendConfig {
imap_host: config.imap_host.clone(),
imap_port: config.imap_port.clone(),
imap_starttls: config.imap_starttls.unwrap_or_default(),
imap_insecure: config.imap_insecure.unwrap_or_default(),
imap_login: config.imap_login.clone(),
imap_passwd_cmd: config.imap_passwd_cmd.clone(),
}),
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => {
BackendConfig::Maildir(MaildirBackendConfig {
maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(),
})
}
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => {
BackendConfig::Notmuch(NotmuchBackendConfig {
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)?
.to_string()
.into(),
})
}
};
trace!("backend config: {:?}", backend_config);
info!("end: parsing account and backend configs from config and account name");
Ok((account_config, backend_config))
}
/// Builds the full RFC822 compliant address of the user account.
pub fn address(&self) -> Result<MailAddr> {
let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c));
let addr = if self.display_name.is_empty() {
self.email.clone()
} else if has_special_chars {
// Wraps the name with double quotes if it contains any special character.
format!("\"{}\" <{}>", self.display_name, self.email)
} else {
format!("{} <{}>", self.display_name, self.email)
};
Ok(mailparse::addrparse(&addr)
.context(format!(
"cannot parse account address {:?}",
self.display_name
))?
.first()
.ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))?
.clone())
}
/// Builds the user account SMTP credentials.
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
let passwd = passwd.lines().next().context("cannot find password")?;
Ok(SmtpCredentials::new(
self.smtp_login.to_owned(),
passwd.to_owned(),
))
}
/// Encrypts a file.
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
run_cmd(&encrypt_file_cmd).map(Some).context(format!(
"cannot run pgp encrypt command {:?}",
encrypt_file_cmd
))
} else {
Ok(None)
}
}
/// Decrypts a file.
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
let decrypt_file_cmd = format!("{} {:?}", cmd, path);
run_cmd(&decrypt_file_cmd).map(Some).context(format!(
"cannot run pgp decrypt command {:?}",
decrypt_file_cmd
))
} else {
Ok(None)
}
}
/// Gets the download path from a file name.
pub fn get_download_file_path<S: AsRef<str>>(&self, file_name: S) -> Result<PathBuf> {
let file_path = self.downloads_dir.join(file_name.as_ref());
self.get_unique_download_file_path(&file_path, |path, _count| path.is_file())
.context(format!(
"cannot get download file path of {:?}",
file_name.as_ref()
))
}
/// Gets the unique download path from a file name by adding suffixes in case of name conflicts.
pub fn get_unique_download_file_path(
&self,
original_file_path: &PathBuf,
is_file: impl Fn(&PathBuf, u8) -> bool,
) -> Result<PathBuf> {
let mut count = 0;
let file_ext = original_file_path
.extension()
.and_then(OsStr::to_str)
.map(|fext| String::from(".") + fext)
.unwrap_or_default();
let mut file_path = original_file_path.clone();
while is_file(&file_path, count) {
count += 1;
file_path.set_file_name(OsStr::new(
&original_file_path
.file_stem()
.and_then(OsStr::to_str)
.map(|fstem| format!("{}_{}{}", fstem, count, file_ext))
.ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?,
));
}
Ok(file_path)
}
/// Runs the notify command.
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
let subject = subject.as_ref();
let sender = sender.as_ref();
let default_cmd = format!(r#"notify-send "New message from {}" "{}""#, sender, subject);
let cmd = self
.notify_cmd
.as_ref()
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd);
debug!("run command: {}", cmd);
run_cmd(&cmd).context("cannot run notify cmd")?;
Ok(())
}
/// Gets the mailbox alias if exists, otherwise returns the
/// mailbox. Also tries to expand shell variables.
pub fn get_mbox_alias(&self, mbox: &str) -> Result<String> {
let mbox = self
.mailboxes
.get(&mbox.trim().to_lowercase())
.map(|s| s.as_str())
.unwrap_or(mbox);
shellexpand::full(mbox)
.map(String::from)
.with_context(|| format!("cannot expand mailbox path {:?}", mbox))
}
}
/// Represents all existing kind of account (backend).
#[derive(Debug, Clone)]
pub enum BackendConfig {
#[cfg(feature = "imap-backend")]
Imap(ImapBackendConfig),
#[cfg(feature = "maildir-backend")]
Maildir(MaildirBackendConfig),
#[cfg(feature = "notmuch-backend")]
Notmuch(NotmuchBackendConfig),
}
/// Represents the IMAP backend.
#[cfg(feature = "imap-backend")]
#[derive(Debug, Default, Clone)]
pub struct ImapBackendConfig {
/// Represents the IMAP host.
pub imap_host: String,
/// Represents the IMAP port.
pub imap_port: u16,
/// Enables StartTLS.
pub imap_starttls: bool,
/// Trusts any certificate.
pub imap_insecure: bool,
/// Represents the IMAP login.
pub imap_login: String,
/// Represents the IMAP password command.
pub imap_passwd_cmd: String,
}
#[cfg(feature = "imap-backend")]
impl ImapBackendConfig {
/// Gets the IMAP password of the user account.
pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
let passwd = passwd.lines().next().context("cannot find password")?;
Ok(passwd.to_string())
}
}
/// Represents the Maildir backend.
#[cfg(feature = "maildir-backend")]
#[derive(Debug, Default, Clone)]
pub struct MaildirBackendConfig {
/// Represents the Maildir directory path.
pub maildir_dir: PathBuf,
}
/// Represents the Notmuch backend.
#[cfg(feature = "notmuch-backend")]
#[derive(Debug, Default, Clone)]
pub struct NotmuchBackendConfig {
/// Represents the Notmuch database path.
pub notmuch_database_dir: PathBuf,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_get_unique_download_file_path() {
let account = AccountConfig::default();
let path = PathBuf::from("downloads/file.ext");
// When file path is unique
assert!(matches!(
account.get_unique_download_file_path(&path, |_, _| false),
Ok(path) if path == PathBuf::from("downloads/file.ext")
));
// When 1 file path already exist
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 1),
Ok(path) if path == PathBuf::from("downloads/file_1.ext")
));
// When 5 file paths already exist
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file_5.ext")
));
// When file path has no extension
let path = PathBuf::from("downloads/file");
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file_5")
));
// When file path has 2 extensions
let path = PathBuf::from("downloads/file.ext.ext2");
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file.ext_5.ext2")
));
}
}
+6 -5
View File
@@ -3,10 +3,11 @@
//! This module gathers all account actions triggered by the CLI.
use anyhow::Result;
use himalaya_lib::account::{AccountConfig, DeserializedConfig};
use log::{info, trace};
use crate::{
config::{AccountConfig, Accounts, DeserializedConfig},
config::Accounts,
output::{PrintTableOpts, PrinterService},
};
@@ -36,13 +37,13 @@ pub fn list<'a, P: PrinterService>(
#[cfg(test)]
mod tests {
use himalaya_lib::account::{
AccountConfig, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig,
};
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
use termcolor::ColorSpec;
use crate::{
config::{DeserializedAccountConfig, DeserializedImapAccountConfig},
output::{Print, PrintTable, WriteColor},
};
use crate::output::{Print, PrintTable, WriteColor};
use super::*;
@@ -1,152 +0,0 @@
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use crate::config::{Format, Hooks};
pub trait ToDeserializedBaseAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig;
}
/// Represents all existing kind of account config.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum DeserializedAccountConfig {
#[cfg(feature = "imap-backend")]
Imap(DeserializedImapAccountConfig),
#[cfg(feature = "maildir-backend")]
Maildir(DeserializedMaildirAccountConfig),
#[cfg(feature = "notmuch-backend")]
Notmuch(DeserializedNotmuchAccountConfig),
}
impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig {
match self {
#[cfg(feature = "imap-backend")]
Self::Imap(config) => config.to_base(),
#[cfg(feature = "maildir-backend")]
Self::Maildir(config) => config.to_base(),
#[cfg(feature = "notmuch-backend")]
Self::Notmuch(config) => config.to_base(),
}
}
}
macro_rules! make_account_config {
($AccountConfig:ident, $($element: ident: $ty: ty),*) => {
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct $AccountConfig {
/// Overrides the display name of the user for this account.
pub name: Option<String>,
/// Overrides the downloads directory (mostly for attachments).
pub downloads_dir: Option<PathBuf>,
/// Overrides the signature for this account.
pub signature: Option<String>,
/// Overrides the signature delimiter for this account.
pub signature_delimiter: Option<String>,
/// Overrides the default page size for this account.
pub default_page_size: Option<usize>,
/// Overrides the notify command for this account.
pub notify_cmd: Option<String>,
/// Overrides the IMAP query used to fetch new messages for this account.
pub notify_query: Option<String>,
/// Overrides the watch commands for this account.
pub watch_cmds: Option<Vec<String>>,
/// Represents the text/plain format as defined in the
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
pub format: Option<Format>,
/// Represents the default headers displayed at the top of
/// the read message.
#[serde(default)]
pub read_headers: Vec<String>,
/// Makes this account the default one.
pub default: Option<bool>,
/// Represents the account email address.
pub email: String,
/// Represents the SMTP host.
pub smtp_host: String,
/// Represents the SMTP port.
pub smtp_port: u16,
/// Enables StartTLS.
pub smtp_starttls: Option<bool>,
/// Trusts any certificate.
pub smtp_insecure: Option<bool>,
/// Represents the SMTP login.
pub smtp_login: String,
/// Represents the SMTP password command.
pub smtp_passwd_cmd: String,
/// Represents the command used to encrypt a message.
pub pgp_encrypt_cmd: Option<String>,
/// Represents the command used to decrypt a message.
pub pgp_decrypt_cmd: Option<String>,
/// Represents mailbox aliases.
#[serde(default)]
pub mailboxes: HashMap<String, String>,
/// Represents hooks.
pub hooks: Option<Hooks>,
$(pub $element: $ty),*
}
impl ToDeserializedBaseAccountConfig for $AccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig {
DeserializedBaseAccountConfig {
name: self.name.clone(),
downloads_dir: self.downloads_dir.clone(),
signature: self.signature.clone(),
signature_delimiter: self.signature_delimiter.clone(),
default_page_size: self.default_page_size.clone(),
notify_cmd: self.notify_cmd.clone(),
notify_query: self.notify_query.clone(),
watch_cmds: self.watch_cmds.clone(),
format: self.format.clone(),
read_headers: self.read_headers.clone(),
default: self.default.clone(),
email: self.email.clone(),
smtp_host: self.smtp_host.clone(),
smtp_port: self.smtp_port.clone(),
smtp_starttls: self.smtp_starttls.clone(),
smtp_insecure: self.smtp_insecure.clone(),
smtp_login: self.smtp_login.clone(),
smtp_passwd_cmd: self.smtp_passwd_cmd.clone(),
pgp_encrypt_cmd: self.pgp_encrypt_cmd.clone(),
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
mailboxes: self.mailboxes.clone(),
hooks: self.hooks.clone(),
}
}
}
}
}
make_account_config!(DeserializedBaseAccountConfig,);
#[cfg(feature = "imap-backend")]
make_account_config!(
DeserializedImapAccountConfig,
imap_host: String,
imap_port: u16,
imap_starttls: Option<bool>,
imap_insecure: Option<bool>,
imap_login: String,
imap_passwd_cmd: String
);
#[cfg(feature = "maildir-backend")]
make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String);
#[cfg(feature = "notmuch-backend")]
make_account_config!(
DeserializedNotmuchAccountConfig,
notmuch_database_dir: String
);
-97
View File
@@ -1,97 +0,0 @@
use anyhow::{Context, Result};
use log::{debug, info, trace};
use serde::Deserialize;
use std::{collections::HashMap, env, fs, path::PathBuf};
use toml;
use crate::config::DeserializedAccountConfig;
pub const DEFAULT_PAGE_SIZE: usize = 10;
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
/// Represents the user config file.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig {
/// Represents the display name of the user.
pub name: String,
/// Represents the downloads directory (mostly for attachments).
pub downloads_dir: Option<PathBuf>,
/// Represents the signature of the user.
pub signature: Option<String>,
/// Overrides the default signature delimiter "`-- \n`".
pub signature_delimiter: Option<String>,
/// Represents the default page size for listings.
pub default_page_size: Option<usize>,
/// Represents the notify command.
pub notify_cmd: Option<String>,
/// Overrides the default IMAP query "NEW" used to fetch new messages
pub notify_query: Option<String>,
/// Represents the watch commands.
pub watch_cmds: Option<Vec<String>>,
/// Represents all the user accounts.
#[serde(flatten)]
pub accounts: HashMap<String, DeserializedAccountConfig>,
}
impl DeserializedConfig {
/// Tries to create a config from an optional path.
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
info!("begin: try to parse config from path");
debug!("path: {:?}", path);
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config = toml::from_str(&content).context("cannot parse config file")?;
info!("end: try to parse config from path");
trace!("config: {:?}", config);
Ok(config)
}
/// Tries to get the XDG config file path from XDG_CONFIG_HOME environment variable.
fn path_from_xdg() -> Result<PathBuf> {
let path =
env::var("XDG_CONFIG_HOME").context("cannot find \"XDG_CONFIG_HOME\" env var")?;
let path = PathBuf::from(path).join("himalaya").join("config.toml");
Ok(path)
}
/// Tries to get the XDG config file path from HOME environment variable.
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
let path = PathBuf::from(path)
.join(".config")
.join("himalaya")
.join("config.toml");
Ok(path)
}
/// Tries to get the .himalayarc config file path from HOME environment variable.
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
let path = PathBuf::from(path).join(".himalayarc");
Ok(path)
}
/// Tries to get the config file path.
pub fn path() -> Result<PathBuf> {
Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
.context("cannot find config path")
}
}
-23
View File
@@ -1,23 +0,0 @@
use serde::Deserialize;
/// Represents the text/plain format as defined in the [RFC2646]. The
/// format is then used by the table system to adjust the way it is
/// rendered.
///
/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(tag = "type", content = "width", rename_all = "lowercase")]
pub enum Format {
// Forces the content width with a fixed amount of pixels.
Fixed(usize),
// Makes the content fit the terminal.
Auto,
// Does not restrict the content.
Flowed,
}
impl Default for Format {
fn default() -> Self {
Self::Auto
}
}
-7
View File
@@ -1,7 +0,0 @@
use serde::Deserialize;
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Hooks {
pub pre_send: Option<String>,
}
-15
View File
@@ -107,12 +107,6 @@ pub mod smtp {
}
pub mod config {
pub mod deserialized_config;
pub use deserialized_config::*;
pub mod deserialized_account_config;
pub use deserialized_account_config::*;
pub mod config_args;
pub mod account_args;
@@ -120,15 +114,6 @@ pub mod config {
pub mod account;
pub use account::*;
pub mod account_config;
pub use account_config::*;
pub mod format;
pub use format::*;
pub mod hooks;
pub use hooks::*;
}
pub mod compl;
+7 -5
View File
@@ -1,14 +1,14 @@
use anyhow::Result;
use himalaya_lib::account::{
AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER,
};
use std::{convert::TryFrom, env};
use url::Url;
use himalaya::{
backends::Backend,
compl::{compl_args, compl_handlers},
config::{
account_args, account_handlers, config_args, AccountConfig, BackendConfig,
DeserializedConfig, DEFAULT_INBOX_FOLDER,
},
config::{account_args, account_handlers, config_args},
mbox::{mbox_args, mbox_handlers},
msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers},
output::{output_args, OutputFmt, StdoutPrinter},
@@ -22,7 +22,9 @@ use himalaya::backends::{imap_args, imap_handlers, ImapBackend};
use himalaya::backends::MaildirBackend;
#[cfg(feature = "notmuch-backend")]
use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig};
use himalaya::backends::NotmuchBackend;
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::account::MaildirBackendConfig;
fn create_app<'a>() -> clap::App<'a, 'a> {
let app = clap::App::new(env!("CARGO_PKG_NAME"))
+1 -1
View File
@@ -3,11 +3,11 @@
//! This module gathers all mailbox actions triggered by the CLI.
use anyhow::Result;
use himalaya_lib::account::AccountConfig;
use log::{info, trace};
use crate::{
backends::Backend,
config::AccountConfig,
output::{PrintTableOpts, PrinterService},
};
+3 -1
View File
@@ -2,6 +2,9 @@ use ammonia;
use anyhow::{anyhow, Context, Error, Result};
use chrono::{DateTime, Local, TimeZone, Utc};
use convert_case::{Case, Casing};
use himalaya_lib::account::{
AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM,
};
use html_escape;
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
use log::{info, trace, warn};
@@ -18,7 +21,6 @@ use uuid::Uuid;
use crate::{
backends::Backend,
config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM},
msg::{
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils,
Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride,
+1 -1
View File
@@ -4,6 +4,7 @@
use anyhow::{Context, Result};
use atty::Stream;
use himalaya_lib::account::{AccountConfig, DEFAULT_SENT_FOLDER};
use log::{debug, info, trace};
use mailparse::addrparse;
use std::{
@@ -15,7 +16,6 @@ use url::Url;
use crate::{
backends::Backend,
config::{AccountConfig, DEFAULT_SENT_FOLDER},
msg::{Msg, Part, Parts, TextPlainPart},
output::{PrintTableOpts, PrinterService},
smtp::SmtpService,
+1 -2
View File
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::AccountConfig;
use mailparse::MailHeaderMap;
use serde::Serialize;
use std::{
@@ -7,8 +8,6 @@ use std::{
};
use uuid::Uuid;
use crate::config::AccountConfig;
#[derive(Debug, Clone, Default, Serialize)]
pub struct TextPlainPart {
pub content: String,
+1 -1
View File
@@ -4,11 +4,11 @@
use anyhow::Result;
use atty::Stream;
use himalaya_lib::account::AccountConfig;
use std::io::{self, BufRead};
use crate::{
backends::Backend,
config::AccountConfig,
msg::{Msg, TplOverride},
output::PrinterService,
smtp::SmtpService,
+1 -2
View File
@@ -1,9 +1,8 @@
use anyhow::Result;
use himalaya_lib::account::Format;
use std::io;
use termcolor::{self, StandardStream};
use crate::config::Format;
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}
+3 -2
View File
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use himalaya_lib::account::AccountConfig;
use lettre::{
self,
transport::smtp::{
@@ -9,7 +10,7 @@ use lettre::{
};
use std::convert::TryInto;
use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd};
use crate::{msg::Msg, output::pipe_cmd};
pub trait SmtpService {
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
@@ -62,7 +63,7 @@ impl SmtpService for LettreService<'_> {
if let Some(cmd) = account.hooks.pre_send.as_deref() {
for cmd in cmd.split('|') {
raw_msg = pipe_cmd(cmd.trim(), &raw_msg)
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?;
}
let parsed_mail = mailparse::parse_mail(&raw_msg)?;
Msg::from_parsed_mail(parsed_mail, account)?.try_into()
+2 -4
View File
@@ -5,15 +5,13 @@
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use anyhow::{Context, Result};
use himalaya_lib::account::Format;
use log::trace;
use termcolor::{Color, ColorSpec};
use terminal_size;
use unicode_width::UnicodeWidthStr;
use crate::{
config::Format,
output::{Print, PrintTableOpts, WriteColor},
};
use crate::output::{Print, PrintTableOpts, WriteColor};
/// Defines the default terminal size.
/// This is used when the size cannot be determined by the `terminal_size` crate.