refactor config and account system

This commit is contained in:
Clément DOUIN 2021-09-14 00:34:34 +02:00
parent 5a9481910f
commit 979c6ef1c9
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
18 changed files with 1130 additions and 1051 deletions

View file

@ -1,2 +1 @@
pub mod cli; pub mod cli;
pub mod model;

View file

@ -1,16 +1,12 @@
use clap; use clap;
use crate::{ use crate::{domain::config::entity::Config, output::model::Output};
config::model::{Account, Config},
output::model::Output,
};
/// `Ctx` stands for `Context` and includes the most "important" structs which are used quite often /// `Ctx` stands for `Context` and includes the most "important" structs which are used quite often
/// in this crate. /// in this crate.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Ctx<'a> { pub struct Ctx<'a> {
pub config: Config, pub config: Config,
pub account: Account,
pub output: Output, pub output: Output,
pub mbox: String, pub mbox: String,
pub arg_matches: clap::ArgMatches<'a>, pub arg_matches: clap::ArgMatches<'a>,
@ -19,7 +15,6 @@ pub struct Ctx<'a> {
impl<'a> Ctx<'a> { impl<'a> Ctx<'a> {
pub fn new<S: ToString>( pub fn new<S: ToString>(
config: Config, config: Config,
account: Account,
output: Output, output: Output,
mbox: S, mbox: S,
arg_matches: clap::ArgMatches<'a>, arg_matches: clap::ArgMatches<'a>,
@ -28,7 +23,6 @@ impl<'a> Ctx<'a> {
Self { Self {
config, config,
account,
output, output,
mbox, mbox,
arg_matches, arg_matches,

View file

@ -0,0 +1,296 @@
use anyhow::{anyhow, Context, Error, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::debug;
use std::{convert::TryFrom, env, fs, path::PathBuf};
use crate::{domain::config::entity::Config, output::utils::run_cmd};
const DEFAULT_PAGE_SIZE: usize = 10;
const DEFAULT_SIG_DELIM: &str = "-- \n";
/// Representation of a user account.
#[derive(Debug, Default)]
pub struct Account {
pub name: String,
pub from: String,
pub downloads_dir: PathBuf,
pub signature: String,
pub default_page_size: usize,
pub watch_cmds: Vec<String>,
pub default: bool,
pub email: String,
pub imap_host: String,
pub imap_port: u16,
pub imap_starttls: bool,
pub imap_insecure: bool,
pub imap_login: String,
pub imap_passwd_cmd: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_starttls: bool,
pub smtp_insecure: bool,
pub smtp_login: String,
pub smtp_passwd_cmd: String,
}
impl Account {
/// This is a little helper-function like which uses the the name and email
/// of the account to create a valid address for the header of the headers
/// of a msg.
///
/// # Hint
/// If the name includes some special characters like a whitespace, comma or semicolon, then
/// the name will be automatically wrapped between two `"`.
///
/// # Exapmle
/// ```
/// use himalaya::config::model::{Account, Config};
///
/// fn main() {
/// let config = Config::default();
///
/// let normal_account = Account::new(Some("Acc1"), "acc1@mail.com");
/// // notice the semicolon in the name!
/// let special_account = Account::new(Some("TL;DR"), "acc2@mail.com");
///
/// // -- Expeced outputs --
/// let expected_normal = Account {
/// name: Some("Acc1".to_string()),
/// email: "acc1@mail.com".to_string(),
/// .. Account::default()
/// };
///
/// let expected_special = Account {
/// name: Some("\"TL;DR\"".to_string()),
/// email: "acc2@mail.com".to_string(),
/// .. Account::default()
/// };
///
/// assert_eq!(config.address(&normal_account), "Acc1 <acc1@mail.com>");
/// assert_eq!(config.address(&special_account), "\"TL;DR\" <acc2@mail.com>");
/// }
/// ```
pub fn address(&self) -> String {
let name = &self.from;
let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char));
if name.is_empty() {
format!("{}", self.email)
} else if has_special_chars {
// so the name has special characters => Wrap it with '"'
format!("\"{}\" <{}>", name, self.email)
} else {
format!("{} <{}>", name, self.email)
}
}
/// Returns the imap-host address + the port usage of the account
///
/// # Example
/// ```rust
/// use himalaya::config::model::Account;
/// fn main () {
/// let account = Account {
/// imap_host: String::from("hostExample"),
/// imap_port: 42,
/// .. Account::default()
/// };
///
/// let expected_output = ("hostExample", 42);
///
/// assert_eq!(account.imap_addr(), expected_output);
/// }
/// ```
pub fn imap_addr(&self) -> (&str, u16) {
debug!("host: {}", self.imap_host);
debug!("port: {}", self.imap_port);
(&self.imap_host, self.imap_port)
}
/// Runs the given command in your password string and returns it.
pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(passwd)
}
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
}
/// Creates a new account with the given values and returns it. All other attributes of the
/// account are gonna be empty/None.
///
/// # Example
/// ```rust
/// use himalaya::config::model::Account;
///
/// fn main() {
/// let account1 = Account::new(Some("Name1"), "email@address.com");
/// let account2 = Account::new(None, "email@address.com");
///
/// let expected1 = Account {
/// name: Some("Name1".to_string()),
/// email: "email@address.com".to_string(),
/// .. Account::default()
/// };
///
/// let expected2 = Account {
/// email: "email@address.com".to_string(),
/// .. Account::default()
/// };
///
/// assert_eq!(account1, expected1);
/// assert_eq!(account2, expected2);
/// }
/// ```
pub fn new<S: ToString + Default>(name: Option<S>, email_addr: S) -> Self {
Self {
name: name.unwrap_or_default().to_string(),
email: email_addr.to_string(),
..Self::default()
}
}
/// Creates a new account with a custom signature. Passing `None` to `signature` sets the
/// signature to `Account Signature`.
///
/// # Examples
/// ```rust
/// use himalaya::config::model::Account;
///
/// fn main() {
///
/// // the testing accounts
/// let account_with_custom_signature = Account::new_with_signature(
/// Some("Email name"), "some@mail.com", Some("Custom signature! :)"));
/// let account_with_default_signature = Account::new_with_signature(
/// Some("Email name"), "some@mail.com", None);
///
/// // How they should look like
/// let account_cmp1 = Account {
/// name: Some("Email name".to_string()),
/// email: "some@mail.com".to_string(),
/// signature: Some("Custom signature! :)".to_string()),
/// .. Account::default()
/// };
///
/// let account_cmp2 = Account {
/// name: Some("Email name".to_string()),
/// email: "some@mail.com".to_string(),
/// .. Account::default()
/// };
///
/// assert_eq!(account_with_custom_signature, account_cmp1);
/// assert_eq!(account_with_default_signature, account_cmp2);
/// }
/// ```
pub fn new_with_signature<S: AsRef<str> + ToString + Default>(
name: Option<S>,
email_addr: S,
signature: Option<S>,
) -> Self {
let mut account = Account::new(name, email_addr);
account.signature = signature.unwrap_or_default().to_string();
account
}
}
impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
type Error = Error;
fn try_from((config, account_name): (&'a Config, Option<&str>)) -> Result<Self, Self::Error> {
let (name, account) = match account_name {
Some("") | None => config
.accounts
.iter()
.find(|(_, account)| account.default.unwrap_or(false))
.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!(format!("cannot find account `{}`", name))),
}?;
let downloads_dir = 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 = 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 signature_delim = account
.signature_delimiter
.as_ref()
.or_else(|| config.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let signature = account
.signature
.as_ref()
.or_else(|| config.signature.as_ref());
let signature = signature
.and_then(|sig| shellexpand::full(sig).ok())
.map(|sig| sig.to_string())
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| signature.map(|sig| sig.to_owned()))
.map(|sig| format!("\n{}{}", signature_delim, sig))
.unwrap_or_default();
Ok(Account {
name,
from: account.name.as_ref().unwrap_or(&config.name).to_owned(),
downloads_dir,
signature,
default_page_size,
watch_cmds: account
.watch_cmds
.as_ref()
.or_else(|| config.watch_cmds.as_ref())
.unwrap_or(&vec![])
.to_owned(),
default: account.default.unwrap_or(false),
email: account.email.to_owned(),
imap_host: account.imap_host.to_owned(),
imap_port: account.imap_port,
imap_starttls: account.imap_starttls.unwrap_or_default(),
imap_insecure: account.imap_insecure.unwrap_or_default(),
imap_login: account.imap_login.to_owned(),
imap_passwd_cmd: account.imap_passwd_cmd.to_owned(),
smtp_host: account.smtp_host.to_owned(),
smtp_port: account.smtp_port,
smtp_starttls: account.smtp_starttls.unwrap_or_default(),
smtp_insecure: account.smtp_insecure.unwrap_or_default(),
smtp_login: account.smtp_login.to_owned(),
smtp_passwd_cmd: account.smtp_passwd_cmd.to_owned(),
})
}
}

View file

@ -0,0 +1,3 @@
//! Modules related to the user's accounts.
pub mod entity;

View file

@ -1,48 +1,32 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Error, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use shellexpand; use shellexpand;
use std::{ use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf, thread};
collections::HashMap,
env,
fs::{self, File},
io::Read,
path::PathBuf,
thread,
};
use toml; use toml;
use crate::output::utils::run_cmd; use crate::output::utils::run_cmd;
const DEFAULT_PAGE_SIZE: usize = 10; const DEFAULT_PAGE_SIZE: usize = 10;
// --- Account --- #[derive(Debug, Default, Clone, PartialEq, Deserialize)]
/// Represents an account section in your config file.
///
/// [account section]: https://github.com/soywod/himalaya/wiki/Configuration:config-file#account-specific-settings
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Account { pub struct Account {
// Override // TODO: rename with `from`
pub name: Option<String>, pub name: Option<String>,
pub downloads_dir: Option<PathBuf>, pub downloads_dir: Option<PathBuf>,
pub signature_delimiter: Option<String>, pub signature_delimiter: Option<String>,
pub signature: Option<String>, pub signature: Option<String>,
pub default_page_size: Option<usize>, pub default_page_size: Option<usize>,
pub watch_cmds: Option<Vec<String>>, pub watch_cmds: Option<Vec<String>>,
// Specific
pub default: Option<bool>, pub default: Option<bool>,
pub email: String, pub email: String,
pub imap_host: String, pub imap_host: String,
pub imap_port: u16, pub imap_port: u16,
pub imap_starttls: Option<bool>, pub imap_starttls: Option<bool>,
pub imap_insecure: Option<bool>, pub imap_insecure: Option<bool>,
pub imap_login: String, pub imap_login: String,
pub imap_passwd_cmd: String, pub imap_passwd_cmd: String,
pub smtp_host: String, pub smtp_host: String,
pub smtp_port: u16, pub smtp_port: u16,
pub smtp_starttls: Option<bool>, pub smtp_starttls: Option<bool>,
@ -51,193 +35,13 @@ pub struct Account {
pub smtp_passwd_cmd: String, pub smtp_passwd_cmd: String,
} }
impl Account { pub type AccountsMap = HashMap<String, Account>;
/// Returns the imap-host address + the port usage of the account
///
/// # Example
/// ```rust
/// use himalaya::config::model::Account;
/// fn main () {
/// let account = Account {
/// imap_host: String::from("hostExample"),
/// imap_port: 42,
/// .. Account::default()
/// };
///
/// let expected_output = ("hostExample", 42);
///
/// assert_eq!(account.imap_addr(), expected_output);
/// }
/// ```
pub fn imap_addr(&self) -> (&str, u16) {
debug!("host: {}", self.imap_host);
debug!("port: {}", self.imap_port);
(&self.imap_host, self.imap_port)
}
/// Runs the given command in your password string and returns it.
pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(passwd)
}
pub fn imap_starttls(&self) -> bool {
let starttls = match self.imap_starttls {
Some(true) => true,
_ => false,
};
debug!("STARTTLS: {}", starttls);
starttls
}
pub fn imap_insecure(&self) -> bool {
let insecure = match self.imap_insecure {
Some(true) => true,
_ => false,
};
debug!("insecure: {}", insecure);
insecure
}
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
}
pub fn smtp_starttls(&self) -> bool {
match self.smtp_starttls {
Some(true) => true,
_ => false,
}
}
pub fn smtp_insecure(&self) -> bool {
match self.smtp_insecure {
Some(true) => true,
_ => false,
}
}
/// Creates a new account with the given values and returns it. All other attributes of the
/// account are gonna be empty/None.
///
/// # Example
/// ```rust
/// use himalaya::config::model::Account;
///
/// fn main() {
/// let account1 = Account::new(Some("Name1"), "email@address.com");
/// let account2 = Account::new(None, "email@address.com");
///
/// let expected1 = Account {
/// name: Some("Name1".to_string()),
/// email: "email@address.com".to_string(),
/// .. Account::default()
/// };
///
/// let expected2 = Account {
/// email: "email@address.com".to_string(),
/// .. Account::default()
/// };
///
/// assert_eq!(account1, expected1);
/// assert_eq!(account2, expected2);
/// }
/// ```
pub fn new<S: ToString>(name: Option<S>, email_addr: S) -> Self {
Self {
name: name.and_then(|name| Some(name.to_string())),
email: email_addr.to_string(),
..Self::default()
}
}
/// Creates a new account with a custom signature. Passing `None` to `signature` sets the
/// signature to `Account Signature`.
///
/// # Examples
/// ```rust
/// use himalaya::config::model::Account;
///
/// fn main() {
///
/// // the testing accounts
/// let account_with_custom_signature = Account::new_with_signature(
/// Some("Email name"), "some@mail.com", Some("Custom signature! :)"));
/// let account_with_default_signature = Account::new_with_signature(
/// Some("Email name"), "some@mail.com", None);
///
/// // How they should look like
/// let account_cmp1 = Account {
/// name: Some("Email name".to_string()),
/// email: "some@mail.com".to_string(),
/// signature: Some("Custom signature! :)".to_string()),
/// .. Account::default()
/// };
///
/// let account_cmp2 = Account {
/// name: Some("Email name".to_string()),
/// email: "some@mail.com".to_string(),
/// .. Account::default()
/// };
///
/// assert_eq!(account_with_custom_signature, account_cmp1);
/// assert_eq!(account_with_default_signature, account_cmp2);
/// }
/// ```
pub fn new_with_signature<S: AsRef<str> + ToString>(
name: Option<S>,
email_addr: S,
signature: Option<S>,
) -> Self {
let mut account = Account::new(name, email_addr);
account.signature = signature.and_then(|signature| Some(signature.to_string()));
account
}
}
impl Default for Account {
fn default() -> Self {
Self {
name: None,
downloads_dir: None,
signature_delimiter: None,
signature: None,
default_page_size: None,
default: None,
email: String::new(),
watch_cmds: None,
imap_host: String::new(),
imap_port: 0,
imap_starttls: None,
imap_insecure: None,
imap_login: String::new(),
imap_passwd_cmd: String::new(),
smtp_host: String::new(),
smtp_port: 0,
smtp_starttls: None,
smtp_insecure: None,
smtp_login: String::new(),
smtp_passwd_cmd: String::new(),
}
}
}
// --- Config ---
/// Represents the whole config file. /// Represents the whole config file.
#[derive(Debug, Default, Deserialize, Clone)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Config { pub struct Config {
// TODO: rename with `from`
pub name: String, pub name: String,
pub downloads_dir: Option<PathBuf>, pub downloads_dir: Option<PathBuf>,
pub notify_cmd: Option<String>, pub notify_cmd: Option<String>,
@ -290,22 +94,13 @@ impl Config {
Ok(path) Ok(path)
} }
/// Parses the config file by the given path and stores the values into the struct. pub fn path() -> Result<PathBuf> {
pub fn new(path: Option<PathBuf>) -> Result<Self> { let path = Self::path_from_xdg()
let path = match path { .or_else(|_| Self::path_from_xdg_alt())
Some(path) => path, .or_else(|_| Self::path_from_home())
None => Self::path_from_xdg() .context("cannot find config path")?;
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
.context("cannot find config path")?,
};
let mut file = File::open(path).context("cannot open config file")?; Ok(path)
let mut content = vec![];
file.read_to_end(&mut content)
.context("cannot read config file")?;
Ok(toml::from_slice(&content).context("cannot parse config file")?)
} }
/// Returns the account by the given name. /// Returns the account by the given name.
@ -386,9 +181,7 @@ impl Config {
/// ``` /// ```
pub fn address(&self, account: &Account) -> String { pub fn address(&self, account: &Account) -> String {
let name = account.name.as_ref().unwrap_or(&self.name); let name = account.name.as_ref().unwrap_or(&self.name);
let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char));
let has_special_chars: bool =
"()<>[]:;@.,".contains(|special_char| name.contains(special_char));
if name.is_empty() { if name.is_empty() {
format!("{}", account.email) format!("{}", account.email)
@ -491,57 +284,62 @@ impl Config {
} }
} }
#[cfg(test)] impl TryFrom<PathBuf> for Config {
mod tests { type Error = Error;
#[cfg(test)] fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
mod config_test { let file_content = fs::read_to_string(path).context("cannot read config file")?;
Ok(toml::from_str(&file_content).context("cannot parse config file")?)
use crate::config::model::{Account, Config};
// a quick way to get a config instance for testing
fn get_config() -> Config {
Config {
name: String::from("Config Name"),
..Config::default()
}
}
#[test]
fn test_find_account_by_name() {
let mut config = get_config();
let account1 = Account::new(None, "one@mail.com");
let account2 = Account::new(Some("Two"), "two@mail.com");
// add some accounts
config.accounts.insert("One".to_string(), account1.clone());
config.accounts.insert("Two".to_string(), account2.clone());
let ret1 = config.find_account_by_name(Some("One")).unwrap();
let ret2 = config.find_account_by_name(Some("Two")).unwrap();
assert_eq!(*ret1, account1);
assert_eq!(*ret2, account2);
}
#[test]
fn test_address() {
let config = get_config();
let account1 = Account::new(None, "one@mail.com");
let account2 = Account::new(Some("Two"), "two@mail.com");
let account3 = Account::new(Some("TL;DR"), "three@mail.com");
let account4 = Account::new(Some("TL,DR"), "lol@mail.com");
let account5 = Account::new(Some("TL:DR"), "rofl@mail.com");
let account6 = Account::new(Some("TL.DR"), "rust@mail.com");
assert_eq!(&config.address(&account1), "Config Name <one@mail.com>");
assert_eq!(&config.address(&account2), "Two <two@mail.com>");
assert_eq!(&config.address(&account3), "\"TL;DR\" <three@mail.com>");
assert_eq!(&config.address(&account4), "\"TL,DR\" <lol@mail.com>");
assert_eq!(&config.address(&account5), "\"TL:DR\" <rofl@mail.com>");
assert_eq!(&config.address(&account6), "\"TL.DR\" <rust@mail.com>");
}
} }
} }
// FIXME: tests
// #[cfg(test)]
// mod tests {
// use crate::domain::{account::entity::Account, config::entity::Config};
// // a quick way to get a config instance for testing
// fn get_config() -> Config {
// Config {
// name: String::from("Config Name"),
// ..Config::default()
// }
// }
// #[test]
// fn test_find_account_by_name() {
// let mut config = get_config();
// let account1 = Account::new(None, "one@mail.com");
// let account2 = Account::new(Some("Two"), "two@mail.com");
// // add some accounts
// config.accounts.insert("One".to_string(), account1.clone());
// config.accounts.insert("Two".to_string(), account2.clone());
// let ret1 = config.find_account_by_name(Some("One")).unwrap();
// let ret2 = config.find_account_by_name(Some("Two")).unwrap();
// assert_eq!(*ret1, account1);
// assert_eq!(*ret2, account2);
// }
// #[test]
// fn test_address() {
// let config = get_config();
// let account1 = Account::new(None, "one@mail.com");
// let account2 = Account::new(Some("Two"), "two@mail.com");
// let account3 = Account::new(Some("TL;DR"), "three@mail.com");
// let account4 = Account::new(Some("TL,DR"), "lol@mail.com");
// let account5 = Account::new(Some("TL:DR"), "rofl@mail.com");
// let account6 = Account::new(Some("TL.DR"), "rust@mail.com");
// assert_eq!(&config.address(&account1), "Config Name <one@mail.com>");
// assert_eq!(&config.address(&account2), "Two <two@mail.com>");
// assert_eq!(&config.address(&account3), "\"TL;DR\" <three@mail.com>");
// assert_eq!(&config.address(&account4), "\"TL,DR\" <lol@mail.com>");
// assert_eq!(&config.address(&account5), "\"TL:DR\" <rofl@mail.com>");
// assert_eq!(&config.address(&account6), "\"TL.DR\" <rust@mail.com>");
// }
// }

3
src/domain/config/mod.rs Normal file
View file

@ -0,0 +1,3 @@
//! Modules related to the user's configuration.
pub mod entity;

View file

@ -1,3 +1,5 @@
//! Domain-specific modules. //! Domain-specific modules.
pub mod account;
pub mod config;
pub mod smtp; pub mod smtp;

View file

@ -5,9 +5,9 @@ use lettre::{
Transport, Transport,
}; };
use crate::config::model::Account; use crate::domain::account::entity::Account;
pub trait SMTPServiceInterface<'a> { pub trait SMTPServiceInterface {
fn send(&self, msg: &lettre::Message) -> Result<()>; fn send(&self, msg: &lettre::Message) -> Result<()>;
} }
@ -16,24 +16,24 @@ pub struct SMTPService<'a> {
} }
impl<'a> SMTPService<'a> { impl<'a> SMTPService<'a> {
pub fn init(account: &'a Account) -> Self { pub fn new(account: &'a Account) -> Result<Self> {
Self { account } Ok(Self { account })
} }
} }
impl<'a> SMTPServiceInterface<'a> for SMTPService<'a> { impl<'a> SMTPServiceInterface for SMTPService<'a> {
fn send(&self, msg: &lettre::Message) -> Result<()> { fn send(&self, msg: &lettre::Message) -> Result<()> {
let smtp_relay = if self.account.smtp_starttls() { let smtp_relay = if self.account.smtp_starttls {
SmtpTransport::starttls_relay SmtpTransport::starttls_relay
} else { } else {
SmtpTransport::relay SmtpTransport::relay
}; };
let tls = TlsParameters::builder(self.account.smtp_host.to_string()) let tls = TlsParameters::builder(self.account.smtp_host.to_string())
.dangerous_accept_invalid_hostnames(self.account.smtp_insecure()) .dangerous_accept_invalid_hostnames(self.account.smtp_insecure)
.dangerous_accept_invalid_certs(self.account.smtp_insecure()) .dangerous_accept_invalid_certs(self.account.smtp_insecure)
.build()?; .build()?;
let tls = if self.account.smtp_starttls() { let tls = if self.account.smtp_starttls {
Tls::Required(tls) Tls::Required(tls)
} else { } else {
Tls::Wrapper(tls) Tls::Wrapper(tls)

View file

@ -2,7 +2,10 @@ use anyhow::Result;
use clap; use clap;
use log::debug; use log::debug;
use crate::{ctx::Ctx, flag::model::Flags, imap::model::ImapConnector, msg::cli::uid_arg}; use crate::{
ctx::Ctx, domain::account::entity::Account, flag::model::Flags, imap::model::ImapConnector,
msg::cli::uid_arg,
};
fn flags_arg<'a>() -> clap::Arg<'a, 'a> { fn flags_arg<'a>() -> clap::Arg<'a, 'a> {
clap::Arg::with_name("flags") clap::Arg::with_name("flags")
@ -36,7 +39,7 @@ pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
)] )]
} }
pub fn matches(ctx: &Ctx) -> Result<bool> { pub fn matches(ctx: &Ctx, account: &Account) -> Result<bool> {
if let Some(matches) = ctx.arg_matches.subcommand_matches("set") { if let Some(matches) = ctx.arg_matches.subcommand_matches("set") {
debug!("set command matched"); debug!("set command matched");
@ -47,7 +50,7 @@ pub fn matches(ctx: &Ctx) -> Result<bool> {
debug!("flags: {}", flags); debug!("flags: {}", flags);
let flags = Flags::from(flags); let flags = Flags::from(flags);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
imap_conn.set_flags(&ctx.mbox, uid, flags)?; imap_conn.set_flags(&ctx.mbox, uid, flags)?;
imap_conn.logout(); imap_conn.logout();
@ -64,7 +67,7 @@ pub fn matches(ctx: &Ctx) -> Result<bool> {
debug!("flags: {}", flags); debug!("flags: {}", flags);
let flags = Flags::from(flags); let flags = Flags::from(flags);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
imap_conn.add_flags(&ctx.mbox, uid, flags)?; imap_conn.add_flags(&ctx.mbox, uid, flags)?;
imap_conn.logout(); imap_conn.logout();
@ -81,7 +84,7 @@ pub fn matches(ctx: &Ctx) -> Result<bool> {
debug!("flags: {}", flags); debug!("flags: {}", flags);
let flags = Flags::from(flags); let flags = Flags::from(flags);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
imap_conn.remove_flags(&ctx.mbox, uid, flags)?; imap_conn.remove_flags(&ctx.mbox, uid, flags)?;
imap_conn.logout(); imap_conn.logout();

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use clap; use clap;
use log::debug; use log::debug;
use crate::{ctx::Ctx, imap::model::ImapConnector}; use crate::{ctx::Ctx, domain::account::entity::Account, imap::model::ImapConnector};
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
vec![ vec![
@ -30,14 +30,14 @@ pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
] ]
} }
pub fn matches(ctx: &Ctx) -> Result<bool> { pub fn matches(ctx: &Ctx, account: &Account) -> Result<bool> {
if let Some(matches) = ctx.arg_matches.subcommand_matches("notify") { if let Some(matches) = ctx.arg_matches.subcommand_matches("notify") {
debug!("notify command matched"); debug!("notify command matched");
let keepalive = clap::value_t_or_exit!(matches.value_of("keepalive"), u64); let keepalive = clap::value_t_or_exit!(matches.value_of("keepalive"), u64);
debug!("keepalive: {}", &keepalive); debug!("keepalive: {}", &keepalive);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
imap_conn.notify(&ctx, keepalive)?; imap_conn.notify(&ctx, keepalive)?;
imap_conn.logout(); imap_conn.logout();
@ -50,7 +50,7 @@ pub fn matches(ctx: &Ctx) -> Result<bool> {
let keepalive = clap::value_t_or_exit!(matches.value_of("keepalive"), u64); let keepalive = clap::value_t_or_exit!(matches.value_of("keepalive"), u64);
debug!("keepalive: {}", &keepalive); debug!("keepalive: {}", &keepalive);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
imap_conn.watch(&ctx, keepalive)?; imap_conn.watch(&ctx, keepalive)?;
imap_conn.logout(); imap_conn.logout();

View file

@ -4,7 +4,7 @@ use log::{debug, trace};
use native_tls::{self, TlsConnector, TlsStream}; use native_tls::{self, TlsConnector, TlsStream};
use std::{collections::HashSet, convert::TryFrom, iter::FromIterator, net::TcpStream}; use std::{collections::HashSet, convert::TryFrom, iter::FromIterator, net::TcpStream};
use crate::{config::model::Account, ctx::Ctx, flag::model::Flags, msg::model::Msg}; use crate::{ctx::Ctx, domain::account::entity::Account, flag::model::Flags, msg::model::Msg};
/// A little helper function to create a similiar error output. (to avoid duplicated code) /// A little helper function to create a similiar error output. (to avoid duplicated code)
fn format_err_msg(description: &str, account: &Account) -> String { fn format_err_msg(description: &str, account: &Account) -> String {
@ -41,16 +41,15 @@ impl<'a> ImapConnector<'a> {
/// to the server ;) /// to the server ;)
pub fn new(account: &'a Account) -> Result<Self> { pub fn new(account: &'a Account) -> Result<Self> {
debug!("create TLS builder"); debug!("create TLS builder");
let insecure = account.imap_insecure();
let ssl_conn = TlsConnector::builder() let ssl_conn = TlsConnector::builder()
.danger_accept_invalid_certs(insecure) .danger_accept_invalid_certs(account.imap_insecure)
.danger_accept_invalid_hostnames(insecure) .danger_accept_invalid_hostnames(account.imap_insecure)
.build() .build()
.context(format_err_msg("cannot create TLS connector", account))?; .context(format_err_msg("cannot create TLS connector", account))?;
debug!("create client"); debug!("create client");
let mut client_builder = imap::ClientBuilder::new(&account.imap_host, account.imap_port); let mut client_builder = imap::ClientBuilder::new(&account.imap_host, account.imap_port);
if account.imap_starttls() { if account.imap_starttls {
debug!("enable STARTTLS"); debug!("enable STARTTLS");
client_builder.starttls(); client_builder.starttls();
} }
@ -260,7 +259,8 @@ impl<'a> ImapConnector<'a> {
}) })
}) })
.context("cannot start the idle mode")?; .context("cannot start the idle mode")?;
ctx.config.exec_watch_cmds(&ctx.account)?; // FIXME
// ctx.config.exec_watch_cmds(&ctx.account)?;
debug!("end loop"); debug!("end loop");
} }
} }

View file

@ -42,8 +42,5 @@ pub mod msg;
/// Handles the output. For example the JSON and HTML output. /// Handles the output. For example the JSON and HTML output.
pub mod output; pub mod output;
/// This module takes care for sending your mails!
pub mod smtp;
pub mod domain; pub mod domain;
pub mod ui; pub mod ui;

View file

@ -1,16 +1,15 @@
use anyhow::Result; use anyhow::Result;
use clap::{self, ArgMatches}; use clap;
use env_logger; use env_logger;
use log::{debug, trace}; use log::{debug, trace};
use std::{env, path::PathBuf}; use std::{convert::TryFrom, env, path::PathBuf};
use url::{self, Url};
use himalaya::{ use himalaya::{
comp, comp,
config::{cli::config_args, model::Config}, config::cli::config_args,
ctx::Ctx, ctx::Ctx,
domain, flag, imap, mbox, domain::{account::entity::Account, config::entity::Config, smtp::service::SMTPService},
msg::{self, cli::msg_matches_mailto}, flag, imap, mbox, msg,
output::{cli::output_args, model::Output}, output::{cli::output_args, model::Output},
}; };
@ -35,20 +34,20 @@ fn main() -> Result<()> {
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "off"), env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "off"),
); );
let raw_args: Vec<String> = env::args().collect(); // let raw_args: Vec<String> = env::args().collect();
// This is used if you click on a mailaddress in the webbrowser // // This is used if you click on a mailaddress in the webbrowser
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { // if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let config = Config::new(None)?; // let config = Config::new(None)?;
let account = config.find_account_by_name(None)?.clone(); // let account = config.find_account_by_name(None)?.clone();
let output = Output::new("plain"); // let output = Output::new("plain");
let mbox = "INBOX"; // let mbox = "INBOX";
let arg_matches = ArgMatches::default(); // let arg_matches = ArgMatches::default();
let app = Ctx::new(config, account, output, mbox, arg_matches); // let app = Ctx::new(config, output, mbox, arg_matches);
let url = Url::parse(&raw_args[1])?; // let url = Url::parse(&raw_args[1])?;
let smtp = domain::smtp::service::SMTPService::init(&app.account); // let smtp = domain::smtp::service::SMTPService::new(&app.account);
return Ok(msg_matches_mailto(&app, &url, smtp)?); // return Ok(msg_matches_mailto(&app, &url, smtp)?);
} // }
let args = parse_args(); let args = parse_args();
let arg_matches = args.get_matches(); let arg_matches = args.get_matches();
@ -63,27 +62,32 @@ fn main() -> Result<()> {
debug!("init config"); debug!("init config");
let custom_config: Option<PathBuf> = arg_matches.value_of("config").map(|s| s.into()); let config_path: PathBuf = arg_matches
debug!("custom config path: {:?}", custom_config); .value_of("config")
let config = Config::new(custom_config)?; .map(|s| s.into())
.unwrap_or(Config::path()?);
debug!("config path: {:?}", config_path);
let config = Config::try_from(config_path.clone())?;
trace!("config: {:?}", config); trace!("config: {:?}", config);
let account_name = arg_matches.value_of("account"); let account_name = arg_matches.value_of("account");
debug!("init account: {}", account_name.unwrap_or("default")); let account = Account::try_from((&config, account_name))?;
let account = config.find_account_by_name(account_name)?.clone(); let smtp_service = SMTPService::new(&account)?;
debug!("account name: {}", account_name.unwrap_or("default"));
trace!("account: {:?}", account); trace!("account: {:?}", account);
let mbox = arg_matches.value_of("mailbox").unwrap().to_string(); let mbox = arg_matches.value_of("mailbox").unwrap().to_string();
debug!("mailbox: {}", mbox); debug!("mailbox: {}", mbox);
debug!("begin matching"); let ctx = Ctx::new(config, output, mbox, arg_matches);
trace!("context: {:?}", ctx);
let app = Ctx::new(config, account, output, mbox, arg_matches); debug!("begin matching");
let smtp = domain::smtp::service::SMTPService::init(&app.account); let _matched = mbox::cli::matches(&ctx, &account)?
let _matched = mbox::cli::matches(&app)? || flag::cli::matches(&ctx, &account)?
|| flag::cli::matches(&app)? || imap::cli::matches(&ctx, &account)?
|| imap::cli::matches(&app)? || msg::cli::matches(&ctx, &account, smtp_service)?;
|| msg::cli::matches(&app, smtp)?;
Ok(()) Ok(())
} }

View file

@ -2,7 +2,9 @@ use anyhow::Result;
use clap; use clap;
use log::{debug, trace}; use log::{debug, trace};
use crate::{ctx::Ctx, imap::model::ImapConnector, mbox::model::Mboxes}; use crate::{
ctx::Ctx, domain::account::entity::Account, imap::model::ImapConnector, mbox::model::Mboxes,
};
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
vec![clap::SubCommand::with_name("mailboxes") vec![clap::SubCommand::with_name("mailboxes")
@ -10,11 +12,11 @@ pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
.about("Lists all mailboxes")] .about("Lists all mailboxes")]
} }
pub fn matches(ctx: &Ctx) -> Result<bool> { pub fn matches(ctx: &Ctx, account: &Account) -> Result<bool> {
if let Some(_) = ctx.arg_matches.subcommand_matches("mailboxes") { if let Some(_) = ctx.arg_matches.subcommand_matches("mailboxes") {
debug!("mailboxes command matched"); debug!("mailboxes command matched");
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let names = imap_conn.list_mboxes()?; let names = imap_conn.list_mboxes()?;
let mboxes = Mboxes::from(&names); let mboxes = Mboxes::from(&names);
debug!("found {} mailboxes", mboxes.0.len()); debug!("found {} mailboxes", mboxes.0.len());

View file

@ -19,7 +19,11 @@ use super::{
model::{Msg, MsgSerialized, Msgs}, model::{Msg, MsgSerialized, Msgs},
}; };
use crate::{ use crate::{
ctx::Ctx, domain::smtp, flag::model::Flags, imap::model::ImapConnector, input, ctx::Ctx,
domain::{account::entity::Account, smtp},
flag::model::Flags,
imap::model::ImapConnector,
input,
mbox::cli::mbox_target_arg, mbox::cli::mbox_target_arg,
}; };
@ -123,26 +127,27 @@ pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
] ]
} }
pub fn matches<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>( pub fn matches<SMTP: smtp::service::SMTPServiceInterface>(
ctx: &Ctx, ctx: &Ctx,
account: &Account,
smtp: SMTP, smtp: SMTP,
) -> Result<bool> { ) -> Result<bool> {
match ctx.arg_matches.subcommand() { match ctx.arg_matches.subcommand() {
("attachments", Some(matches)) => msg_matches_attachments(ctx, matches), ("attachments", Some(matches)) => msg_matches_attachments(&ctx, &account, &matches),
("copy", Some(matches)) => msg_matches_copy(ctx, matches), ("copy", Some(matches)) => msg_matches_copy(&ctx, &account, &matches),
("delete", Some(matches)) => msg_matches_delete(ctx, matches), ("delete", Some(matches)) => msg_matches_delete(&ctx, &account, &matches),
("forward", Some(matches)) => msg_matches_forward(ctx, matches, smtp), ("forward", Some(matches)) => msg_matches_forward(&ctx, &account, &matches, smtp),
("move", Some(matches)) => msg_matches_move(ctx, matches), ("move", Some(matches)) => msg_matches_move(&ctx, &account, &matches),
("read", Some(matches)) => msg_matches_read(ctx, matches), ("read", Some(matches)) => msg_matches_read(&ctx, &account, &matches),
("reply", Some(matches)) => msg_matches_reply(ctx, matches, smtp), ("reply", Some(matches)) => msg_matches_reply(&ctx, &account, &matches, smtp),
("save", Some(matches)) => msg_matches_save(ctx, matches), ("save", Some(matches)) => msg_matches_save(&ctx, &account, matches),
("search", Some(matches)) => msg_matches_search(ctx, matches), ("search", Some(matches)) => msg_matches_search(&ctx, &account, &matches),
("send", Some(matches)) => msg_matches_send(ctx, matches, smtp), ("send", Some(matches)) => msg_matches_send(&ctx, &account, &matches, smtp),
("write", Some(matches)) => msg_matches_write(ctx, matches, smtp), ("write", Some(matches)) => msg_matches_write(&ctx, &account, &matches, smtp),
("template", Some(matches)) => Ok(msg_matches_tpl(ctx, matches)?), ("template", Some(matches)) => Ok(msg_matches_tpl(&ctx, &account, &matches)?),
("list", opt_matches) => msg_matches_list(ctx, opt_matches), ("list", opt_matches) => msg_matches_list(&ctx, &account, opt_matches),
(_other, opt_matches) => msg_matches_list(ctx, opt_matches), (_other, opt_matches) => msg_matches_list(&ctx, &account, opt_matches),
} }
} }
@ -239,12 +244,16 @@ fn tpl_args<'a>() -> Vec<clap::Arg<'a, 'a>> {
] ]
} }
fn msg_matches_list(ctx: &Ctx, opt_matches: Option<&clap::ArgMatches>) -> Result<bool> { fn msg_matches_list(
ctx: &Ctx,
account: &Account,
opt_matches: Option<&clap::ArgMatches>,
) -> Result<bool> {
debug!("list command matched"); debug!("list command matched");
let page_size: usize = opt_matches let page_size: usize = opt_matches
.and_then(|matches| matches.value_of("page-size").and_then(|s| s.parse().ok())) .and_then(|matches| matches.value_of("page-size").and_then(|s| s.parse().ok()))
.unwrap_or_else(|| ctx.config.default_page_size(&ctx.account)); .unwrap_or(account.default_page_size);
debug!("page size: {:?}", page_size); debug!("page size: {:?}", page_size);
let page: usize = opt_matches let page: usize = opt_matches
.and_then(|matches| matches.value_of("page").unwrap().parse().ok()) .and_then(|matches| matches.value_of("page").unwrap().parse().ok())
@ -252,7 +261,7 @@ fn msg_matches_list(ctx: &Ctx, opt_matches: Option<&clap::ArgMatches>) -> Result
.unwrap_or_default(); .unwrap_or_default();
debug!("page: {}", &page); debug!("page: {}", &page);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let msgs = imap_conn.list_msgs(&ctx.mbox, &page_size, &page)?; let msgs = imap_conn.list_msgs(&ctx.mbox, &page_size, &page)?;
let msgs = if let Some(ref fetches) = msgs { let msgs = if let Some(ref fetches) = msgs {
Msgs::try_from(fetches)? Msgs::try_from(fetches)?
@ -268,13 +277,13 @@ fn msg_matches_list(ctx: &Ctx, opt_matches: Option<&clap::ArgMatches>) -> Result
Ok(true) Ok(true)
} }
fn msg_matches_search(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn msg_matches_search(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("search command matched"); debug!("search command matched");
let page_size: usize = matches let page_size: usize = matches
.value_of("page-size") .value_of("page-size")
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())
.unwrap_or(ctx.config.default_page_size(&ctx.account)); .unwrap_or(account.default_page_size);
debug!("page size: {}", &page_size); debug!("page size: {}", &page_size);
let page: usize = matches let page: usize = matches
.value_of("page") .value_of("page")
@ -310,7 +319,7 @@ fn msg_matches_search(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
.join(" "); .join(" ");
debug!("query: {}", &page); debug!("query: {}", &page);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let msgs = imap_conn.search_msgs(&ctx.mbox, &query, &page_size, &page)?; let msgs = imap_conn.search_msgs(&ctx.mbox, &query, &page_size, &page)?;
let msgs = if let Some(ref fetches) = msgs { let msgs = if let Some(ref fetches) = msgs {
Msgs::try_from(fetches)? Msgs::try_from(fetches)?
@ -324,7 +333,7 @@ fn msg_matches_search(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
fn msg_matches_read(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn msg_matches_read(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("read command matched"); debug!("read command matched");
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
@ -334,7 +343,7 @@ fn msg_matches_read(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
let raw = matches.is_present("raw"); let raw = matches.is_present("raw");
debug!("raw: {}", raw); debug!("raw: {}", raw);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
if raw { if raw {
@ -346,14 +355,18 @@ fn msg_matches_read(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
fn msg_matches_attachments(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn msg_matches_attachments(
ctx: &Ctx,
account: &Account,
matches: &clap::ArgMatches,
) -> Result<bool> {
debug!("attachments command matched"); debug!("attachments command matched");
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
debug!("uid: {}", &uid); debug!("uid: {}", &uid);
// get the msg and than it's attachments // get the msg and than it's attachments
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
let attachments = msg.attachments.clone(); let attachments = msg.attachments.clone();
@ -366,12 +379,8 @@ fn msg_matches_attachments(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool
// Iterate through all attachments and download them to the download // Iterate through all attachments and download them to the download
// directory of the account. // directory of the account.
for attachment in &attachments { for attachment in &attachments {
let filepath = ctx let filepath = account.downloads_dir.join(&attachment.filename);
.config
.downloads_filepath(&ctx.account, &attachment.filename);
debug!("downloading {}…", &attachment.filename); debug!("downloading {}…", &attachment.filename);
fs::write(&filepath, &attachment.body_raw) fs::write(&filepath, &attachment.body_raw)
.with_context(|| format!("cannot save attachment {:?}", filepath))?; .with_context(|| format!("cannot save attachment {:?}", filepath))?;
} }
@ -390,19 +399,20 @@ fn msg_matches_attachments(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool
Ok(true) Ok(true)
} }
fn msg_matches_write<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>( fn msg_matches_write<'a, SMTP: smtp::service::SMTPServiceInterface>(
ctx: &Ctx, ctx: &Ctx,
account: &Account,
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
smtp: SMTP, smtp: SMTP,
) -> Result<bool> { ) -> Result<bool> {
debug!("write command matched"); debug!("write command matched");
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
// create the new msg // create the new msg
// TODO: Make the header starting customizeable like from template // TODO: Make the header starting customizeable like from template
let mut msg = Msg::new_with_headers( let mut msg = Msg::new_with_headers(
&ctx, &account,
Headers { Headers {
subject: Some(String::new()), subject: Some(String::new()),
to: Vec::new(), to: Vec::new(),
@ -428,22 +438,23 @@ fn msg_matches_write<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>(
Ok(true) Ok(true)
} }
fn msg_matches_reply<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>( fn msg_matches_reply<'a, SMTP: smtp::service::SMTPServiceInterface>(
ctx: &Ctx, ctx: &Ctx,
account: &Account,
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
smtp: SMTP, smtp: SMTP,
) -> Result<bool> { ) -> Result<bool> {
debug!("reply command matched"); debug!("reply command matched");
// -- Preparations -- // -- Preparations --
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
debug!("uid: {}", uid); debug!("uid: {}", uid);
// Change the msg to a reply-msg. // Change the msg to a reply-msg.
msg.change_to_reply(&ctx, matches.is_present("reply-all"))?; msg.change_to_reply(&account, matches.is_present("reply-all"))?;
// Apply the given attachments to the reply-msg. // Apply the given attachments to the reply-msg.
let attachments: Vec<&str> = matches let attachments: Vec<&str> = matches
@ -462,14 +473,15 @@ fn msg_matches_reply<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>(
Ok(true) Ok(true)
} }
pub fn msg_matches_mailto<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>( pub fn msg_matches_mailto<'a, SMTP: smtp::service::SMTPServiceInterface>(
ctx: &Ctx, ctx: &Ctx,
account: &Account,
url: &Url, url: &Url,
smtp: SMTP, smtp: SMTP,
) -> Result<()> { ) -> Result<()> {
debug!("mailto command matched"); debug!("mailto command matched");
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let mut cc = Vec::new(); let mut cc = Vec::new();
let mut bcc = Vec::new(); let mut bcc = Vec::new();
@ -495,17 +507,17 @@ pub fn msg_matches_mailto<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>(
} }
let headers = Headers { let headers = Headers {
from: vec![ctx.config.address(&ctx.account)], from: vec![account.address()],
to: vec![url.path().to_string()], to: vec![url.path().to_string()],
encoding: ContentTransferEncoding::Base64, encoding: ContentTransferEncoding::Base64,
bcc: Some(bcc), bcc: Some(bcc),
cc: Some(cc), cc: Some(cc),
signature: ctx.config.signature(&ctx.account), signature: Some(account.signature.to_owned()),
subject: Some(subject.into()), subject: Some(subject.into()),
..Headers::default() ..Headers::default()
}; };
let mut msg = Msg::new_with_headers(&ctx, headers); let mut msg = Msg::new_with_headers(&account, headers);
msg.body = Body::new_with_text(body); msg.body = Body::new_with_text(body);
msg_interaction(&ctx, &mut msg, &mut imap_conn, smtp)?; msg_interaction(&ctx, &mut msg, &mut imap_conn, smtp)?;
@ -513,22 +525,23 @@ pub fn msg_matches_mailto<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>(
Ok(()) Ok(())
} }
fn msg_matches_forward<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>( fn msg_matches_forward<'a, SMTP: smtp::service::SMTPServiceInterface>(
ctx: &Ctx, ctx: &Ctx,
account: &Account,
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
smtp: SMTP, smtp: SMTP,
) -> Result<bool> { ) -> Result<bool> {
debug!("forward command matched"); debug!("forward command matched");
// fetch the msg // fetch the msg
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
debug!("uid: {}", uid); debug!("uid: {}", uid);
// prepare to forward it // prepare to forward it
msg.change_to_forwarding(&ctx); msg.change_to_forwarding(&account);
let attachments: Vec<&str> = matches let attachments: Vec<&str> = matches
.values_of("attachments") .values_of("attachments")
@ -548,11 +561,11 @@ fn msg_matches_forward<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>(
Ok(true) Ok(true)
} }
fn msg_matches_copy(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn msg_matches_copy(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("copy command matched"); debug!("copy command matched");
// fetch the message to be copyied // fetch the message to be copyied
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let target = matches.value_of("target").unwrap(); let target = matches.value_of("target").unwrap();
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
@ -576,11 +589,11 @@ fn msg_matches_copy(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
fn msg_matches_move(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn msg_matches_move(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("move command matched"); debug!("move command matched");
// fetch the msg which should be moved // fetch the msg which should be moved
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let target = matches.value_of("target").unwrap(); let target = matches.value_of("target").unwrap();
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
@ -607,10 +620,10 @@ fn msg_matches_move(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
fn msg_matches_delete(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn msg_matches_delete(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("delete command matched"); debug!("delete command matched");
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
// remove the message according to its UID // remove the message according to its UID
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
@ -626,14 +639,15 @@ fn msg_matches_delete(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
fn msg_matches_send<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>( fn msg_matches_send<'a, SMTP: smtp::service::SMTPServiceInterface>(
ctx: &Ctx, ctx: &Ctx,
account: &Account,
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
smtp: SMTP, smtp: SMTP,
) -> Result<bool> { ) -> Result<bool> {
debug!("send command matched"); debug!("send command matched");
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let msg = if atty::is(Stream::Stdin) || ctx.output.is_json() { let msg = if atty::is(Stream::Stdin) || ctx.output.is_json() {
matches matches
@ -667,10 +681,10 @@ fn msg_matches_send<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>(
Ok(true) Ok(true)
} }
fn msg_matches_save(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn msg_matches_save(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("save command matched"); debug!("save command matched");
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let msg: &str = matches.value_of("message").unwrap(); let msg: &str = matches.value_of("message").unwrap();
let mut msg = Msg::try_from(msg)?; let mut msg = Msg::try_from(msg)?;
@ -683,11 +697,11 @@ fn msg_matches_save(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
pub fn msg_matches_tpl(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { pub fn msg_matches_tpl(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
match matches.subcommand() { match matches.subcommand() {
("new", Some(matches)) => tpl_matches_new(ctx, matches), ("new", Some(matches)) => tpl_matches_new(&ctx, &account, matches),
("reply", Some(matches)) => tpl_matches_reply(ctx, matches), ("reply", Some(matches)) => tpl_matches_reply(&ctx, &account, matches),
("forward", Some(matches)) => tpl_matches_forward(ctx, matches), ("forward", Some(matches)) => tpl_matches_forward(&ctx, &account, matches),
// TODO: find a way to show the help message for template subcommand // TODO: find a way to show the help message for template subcommand
_ => Err(anyhow!("Subcommand not found")), _ => Err(anyhow!("Subcommand not found")),
@ -784,10 +798,10 @@ fn override_msg_with_args(msg: &mut Msg, matches: &clap::ArgMatches) {
msg.body = body; msg.body = body;
} }
fn tpl_matches_new(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn tpl_matches_new(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("new command matched"); debug!("new command matched");
let mut msg = Msg::new(&ctx); let mut msg = Msg::new(&account);
override_msg_with_args(&mut msg, &matches); override_msg_with_args(&mut msg, &matches);
@ -797,16 +811,16 @@ fn tpl_matches_new(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
fn tpl_matches_reply(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn tpl_matches_reply(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("reply command matched"); debug!("reply command matched");
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
debug!("uid: {}", uid); debug!("uid: {}", uid);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
msg.change_to_reply(&ctx, matches.is_present("reply-all"))?; msg.change_to_reply(&account, matches.is_present("reply-all"))?;
override_msg_with_args(&mut msg, &matches); override_msg_with_args(&mut msg, &matches);
trace!("Message: {:?}", msg); trace!("Message: {:?}", msg);
@ -815,15 +829,15 @@ fn tpl_matches_reply(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
Ok(true) Ok(true)
} }
fn tpl_matches_forward(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> { fn tpl_matches_forward(ctx: &Ctx, account: &Account, matches: &clap::ArgMatches) -> Result<bool> {
debug!("forward command matched"); debug!("forward command matched");
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
debug!("uid: {}", uid); debug!("uid: {}", uid);
let mut imap_conn = ImapConnector::new(&ctx.account)?; let mut imap_conn = ImapConnector::new(&account)?;
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?; let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
msg.change_to_forwarding(&ctx); msg.change_to_forwarding(&account);
override_msg_with_args(&mut msg, &matches); override_msg_with_args(&mut msg, &matches);
@ -835,7 +849,7 @@ fn tpl_matches_forward(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
/// This function opens the prompt to do some actions to the msg like sending, editing it again and /// This function opens the prompt to do some actions to the msg like sending, editing it again and
/// so on. /// so on.
fn msg_interaction<'a, SMTP: smtp::service::SMTPServiceInterface<'a>>( fn msg_interaction<SMTP: smtp::service::SMTPServiceInterface>(
ctx: &Ctx, ctx: &Ctx,
msg: &mut Msg, msg: &mut Msg,
imap_conn: &mut ImapConnector, imap_conn: &mut ImapConnector,

File diff suppressed because it is too large Load diff

View file

@ -1,35 +0,0 @@
use anyhow::Result;
use lettre::{
self,
transport::{smtp::client::Tls, smtp::client::TlsParameters, smtp::SmtpTransport},
Transport,
};
use crate::config::model::Account;
pub fn send(account: &Account, msg: &lettre::Message) -> Result<()> {
let smtp_relay = if account.smtp_starttls() {
SmtpTransport::starttls_relay
} else {
SmtpTransport::relay
};
let tls = TlsParameters::builder(account.smtp_host.to_string())
.dangerous_accept_invalid_hostnames(account.smtp_insecure())
.dangerous_accept_invalid_certs(account.smtp_insecure())
.build()?;
let tls = if account.smtp_starttls() {
Tls::Required(tls)
} else {
Tls::Wrapper(tls)
};
smtp_relay(&account.smtp_host)?
.port(account.smtp_port)
.tls(tls)
.credentials(account.smtp_creds()?)
.build()
.send(msg)?;
Ok(())
}

View file

@ -1,149 +1,150 @@
use std::convert::TryFrom; // FIXME: fix tests
// use std::convert::TryFrom;
use himalaya::{ // use himalaya::{
config::model::Account, flag::model::Flags, imap::model::ImapConnector, mbox::model::Mboxes, // domain::account::entity::Account, flag::model::Flags, imap::model::ImapConnector,
msg::model::Msgs, smtp, // mbox::model::Mboxes, msg::model::Msgs,
}; // };
use imap::types::Flag; // use imap::types::Flag;
use lettre::message::SinglePart; // use lettre::message::SinglePart;
use lettre::Message; // use lettre::Message;
fn get_account(addr: &str) -> Account { // fn get_account(addr: &str) -> Account {
Account { // Account {
name: None, // name: None,
downloads_dir: None, // downloads_dir: None,
signature_delimiter: None, // signature_delimiter: None,
signature: None, // signature: None,
default_page_size: None, // default_page_size: None,
default: Some(true), // default: Some(true),
email: addr.into(), // email: addr.into(),
watch_cmds: None, // watch_cmds: None,
imap_host: String::from("localhost"), // imap_host: String::from("localhost"),
imap_port: 3993, // imap_port: 3993,
imap_starttls: Some(false), // imap_starttls: Some(false),
imap_insecure: Some(true), // imap_insecure: Some(true),
imap_login: addr.into(), // imap_login: addr.into(),
imap_passwd_cmd: String::from("echo 'password'"), // imap_passwd_cmd: String::from("echo 'password'"),
smtp_host: String::from("localhost"), // smtp_host: String::from("localhost"),
smtp_port: 3465, // smtp_port: 3465,
smtp_starttls: Some(false), // smtp_starttls: Some(false),
smtp_insecure: Some(true), // smtp_insecure: Some(true),
smtp_login: addr.into(), // smtp_login: addr.into(),
smtp_passwd_cmd: String::from("echo 'password'"), // smtp_passwd_cmd: String::from("echo 'password'"),
} // }
} // }
#[test] // #[test]
fn mbox() { // fn mbox() {
let account = get_account("inbox@localhost"); // let account = get_account("inbox@localhost");
let mut imap_conn = ImapConnector::new(&account).unwrap(); // let mut imap_conn = ImapConnector::new(&account).unwrap();
let names = imap_conn.list_mboxes().unwrap(); // let names = imap_conn.list_mboxes().unwrap();
let mboxes: Vec<String> = Mboxes::from(&names) // let mboxes: Vec<String> = Mboxes::from(&names)
.0 // .0
.into_iter() // .into_iter()
.map(|mbox| mbox.name) // .map(|mbox| mbox.name)
.collect(); // .collect();
assert_eq!(mboxes, vec![String::from("INBOX")]); // assert_eq!(mboxes, vec![String::from("INBOX")]);
imap_conn.logout(); // imap_conn.logout();
} // }
#[test] // #[test]
fn msg() { // fn msg() {
// Preparations // // Preparations
// Get the test-account and clean up the server. // // Get the test-account and clean up the server.
let account = get_account("inbox@localhost"); // let account = get_account("inbox@localhost");
// Login // // Login
let mut imap_conn = ImapConnector::new(&account).unwrap(); // let mut imap_conn = ImapConnector::new(&account).unwrap();
// remove all previous mails first // // remove all previous mails first
let fetches = imap_conn.list_msgs("INBOX", &10, &0).unwrap(); // let fetches = imap_conn.list_msgs("INBOX", &10, &0).unwrap();
let msgs = if let Some(ref fetches) = fetches { // let msgs = if let Some(ref fetches) = fetches {
Msgs::try_from(fetches).unwrap() // Msgs::try_from(fetches).unwrap()
} else { // } else {
Msgs::new() // Msgs::new()
}; // };
// mark all mails as deleted // // mark all mails as deleted
for msg in msgs.0.iter() { // for msg in msgs.0.iter() {
imap_conn // imap_conn
.add_flags( // .add_flags(
"INBOX", // "INBOX",
&msg.get_uid().unwrap().to_string(), // &msg.get_uid().unwrap().to_string(),
Flags::from(vec![Flag::Deleted]), // Flags::from(vec![Flag::Deleted]),
) // )
.unwrap(); // .unwrap();
} // }
imap_conn.expunge("INBOX").unwrap(); // imap_conn.expunge("INBOX").unwrap();
// make sure, that they are *really* deleted // // make sure, that they are *really* deleted
assert!(imap_conn.list_msgs("INBOX", &10, &0).unwrap().is_none()); // assert!(imap_conn.list_msgs("INBOX", &10, &0).unwrap().is_none());
// == Testing == // // == Testing ==
// Add messages // // Add messages
let message_a = Message::builder() // let message_a = Message::builder()
.from("sender-a@localhost".parse().unwrap()) // .from("sender-a@localhost".parse().unwrap())
.to("inbox@localhost".parse().unwrap()) // .to("inbox@localhost".parse().unwrap())
.subject("Subject A") // .subject("Subject A")
.singlepart(SinglePart::builder().body("Body A".as_bytes().to_vec())) // .singlepart(SinglePart::builder().body("Body A".as_bytes().to_vec()))
.unwrap(); // .unwrap();
let message_b = Message::builder() // let message_b = Message::builder()
.from("Sender B <sender-b@localhost>".parse().unwrap()) // .from("Sender B <sender-b@localhost>".parse().unwrap())
.to("inbox@localhost".parse().unwrap()) // .to("inbox@localhost".parse().unwrap())
.subject("Subject B") // .subject("Subject B")
.singlepart(SinglePart::builder().body("Body B".as_bytes().to_vec())) // .singlepart(SinglePart::builder().body("Body B".as_bytes().to_vec()))
.unwrap(); // .unwrap();
smtp::send(&account, &message_a).unwrap(); // smtp::send(&account, &message_a).unwrap();
smtp::send(&account, &message_b).unwrap(); // smtp::send(&account, &message_b).unwrap();
// -- Get the messages -- // // -- Get the messages --
// TODO: check non-existance of \Seen flag // // TODO: check non-existance of \Seen flag
let msgs = imap_conn.list_msgs("INBOX", &10, &0).unwrap(); // let msgs = imap_conn.list_msgs("INBOX", &10, &0).unwrap();
let msgs = if let Some(ref fetches) = msgs { // let msgs = if let Some(ref fetches) = msgs {
Msgs::try_from(fetches).unwrap() // Msgs::try_from(fetches).unwrap()
} else { // } else {
Msgs::new() // Msgs::new()
}; // };
// make sure that there are both mails which we sended // // make sure that there are both mails which we sended
assert_eq!(msgs.0.len(), 2); // assert_eq!(msgs.0.len(), 2);
let msg_a = msgs // let msg_a = msgs
.0 // .0
.iter() // .iter()
.find(|msg| msg.headers.subject.clone().unwrap() == "Subject A") // .find(|msg| msg.headers.subject.clone().unwrap() == "Subject A")
.unwrap(); // .unwrap();
let msg_b = msgs // let msg_b = msgs
.0 // .0
.iter() // .iter()
.find(|msg| msg.headers.subject.clone().unwrap() == "Subject B") // .find(|msg| msg.headers.subject.clone().unwrap() == "Subject B")
.unwrap(); // .unwrap();
// -- Checkup -- // // -- Checkup --
// look, if we received the correct credentials of the msgs. // // look, if we received the correct credentials of the msgs.
assert_eq!( // assert_eq!(
msg_a.headers.subject.clone().unwrap_or_default(), // msg_a.headers.subject.clone().unwrap_or_default(),
"Subject A" // "Subject A"
); // );
assert_eq!(&msg_a.headers.from[0], "sender-a@localhost"); // assert_eq!(&msg_a.headers.from[0], "sender-a@localhost");
assert_eq!( // assert_eq!(
msg_b.headers.subject.clone().unwrap_or_default(), // msg_b.headers.subject.clone().unwrap_or_default(),
"Subject B" // "Subject B"
); // );
assert_eq!(&msg_b.headers.from[0], "Sender B <sender-b@localhost>"); // assert_eq!(&msg_b.headers.from[0], "Sender B <sender-b@localhost>");
// TODO: search messages // // TODO: search messages
// TODO: read message (+ \Seen flag) // // TODO: read message (+ \Seen flag)
// TODO: list message attachments // // TODO: list message attachments
// TODO: add/set/remove flags // // TODO: add/set/remove flags
// Logout // // Logout
imap_conn.logout(); // imap_conn.logout();
} // }