clean process and account modules (#340)

This commit is contained in:
Clément DOUIN 2022-06-27 01:13:55 +02:00
parent a5c4fdaac6
commit c0e002ea1b
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
27 changed files with 251 additions and 216 deletions

View file

@ -3,7 +3,7 @@
//! This module gathers all account actions triggered by the CLI.
use anyhow::Result;
use himalaya_lib::account::{AccountConfig, DeserializedConfig};
use himalaya_lib::account::{Account, DeserializedConfig};
use log::{info, trace};
use crate::{
@ -15,7 +15,7 @@ use crate::{
pub fn list<'a, P: PrinterService>(
max_width: Option<usize>,
config: &DeserializedConfig,
account_config: &AccountConfig,
account_config: &Account,
printer: &mut P,
) -> Result<()> {
info!(">> account list handler");
@ -38,7 +38,7 @@ pub fn list<'a, P: PrinterService>(
#[cfg(test)]
mod tests {
use himalaya_lib::account::{
AccountConfig, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig,
Account, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig,
};
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
use termcolor::ColorSpec;
@ -122,7 +122,7 @@ mod tests {
..DeserializedConfig::default()
};
let account_config = AccountConfig::default();
let account_config = Account::default();
let mut printer = PrinterServiceTest::default();
assert!(list(None, &config, &account_config, &mut printer).is_ok());

View file

@ -1,6 +1,6 @@
use anyhow::{Context, Result};
use himalaya_lib::{
account::{AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER},
account::{Account, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER},
backend::Backend,
};
use std::{convert::TryFrom, env};
@ -58,7 +58,7 @@ fn main() -> Result<()> {
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let config = DeserializedConfig::from_opt_path(None)?;
let (account_config, backend_config) =
AccountConfig::from_config_and_opt_account_name(&config, None)?;
Account::from_config_and_opt_account_name(&config, None)?;
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
let url = Url::parse(&raw_args[1])?;
let mut smtp = LettreService::from(&account_config);
@ -114,7 +114,7 @@ fn main() -> Result<()> {
// Init entities and services.
let config = DeserializedConfig::from_opt_path(m.value_of("config"))?;
let (account_config, backend_config) =
AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?;
Account::from_config_and_opt_account_name(&config, m.value_of("account"))?;
let mbox = m
.value_of("mbox-source")
.or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str()))

View file

@ -3,7 +3,7 @@
//! This module gathers all mailbox actions triggered by the CLI.
use anyhow::Result;
use himalaya_lib::{account::AccountConfig, backend::Backend};
use himalaya_lib::{account::Account, backend::Backend};
use log::{info, trace};
use crate::output::{PrintTableOpts, PrinterService};
@ -11,7 +11,7 @@ use crate::output::{PrintTableOpts, PrinterService};
/// Lists all mailboxes.
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
max_width: Option<usize>,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
@ -170,7 +170,7 @@ mod tests {
}
}
let config = AccountConfig::default();
let config = Account::default();
let mut printer = PrinterServiceTest::default();
let mut backend = TestBackend {};
let backend = Box::new(&mut backend);

View file

@ -5,7 +5,7 @@
use anyhow::{Context, Result};
use atty::Stream;
use himalaya_lib::{
account::{AccountConfig, DEFAULT_SENT_FOLDER},
account::{Account, DEFAULT_SENT_FOLDER},
backend::Backend,
msg::{Msg, Part, Parts, TextPlainPart, TplOverride},
};
@ -28,7 +28,7 @@ use crate::{
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
@ -92,7 +92,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
attachments_paths: Vec<&str>,
encrypt: bool,
mbox: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
@ -112,7 +112,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
imap: Box<&'a mut B>,
) -> Result<()> {
@ -134,7 +134,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
url: &Url,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
@ -212,7 +212,7 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
raw: bool,
headers: Vec<&str>,
mbox: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
@ -233,7 +233,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
attachments_paths: Vec<&str>,
encrypt: bool,
mbox: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
@ -285,7 +285,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
@ -310,7 +310,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
@ -330,7 +330,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
/// Send a raw message.
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
raw_msg: &str,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&mut B>,
smtp: &mut S,
@ -371,7 +371,7 @@ pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
tpl: TplOverride,
attachments_paths: Vec<&str>,
encrypt: bool,
config: &AccountConfig,
config: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,

View file

@ -5,7 +5,7 @@
use anyhow::Result;
use atty::Stream;
use himalaya_lib::{
account::AccountConfig,
account::Account,
backend::Backend,
msg::{Msg, TplOverride},
};
@ -16,7 +16,7 @@ use crate::{output::PrinterService, smtp::SmtpService};
/// Generate a new message template.
pub fn new<'a, P: PrinterService>(
opts: TplOverride<'a>,
account: &'a AccountConfig,
account: &'a Account,
printer: &'a mut P,
) -> Result<()> {
let tpl = Msg::default().to_tpl(opts, account)?;
@ -29,7 +29,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
all: bool,
opts: TplOverride<'a>,
mbox: &str,
config: &'a AccountConfig,
config: &'a Account,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
@ -45,7 +45,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
opts: TplOverride<'a>,
mbox: &str,
config: &'a AccountConfig,
config: &'a Account,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
@ -59,7 +59,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
/// Saves a message based on a template.
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
mbox: &str,
config: &AccountConfig,
config: &Account,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut P,
@ -84,7 +84,7 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
/// Sends a message based on a template.
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
mbox: &str,
account: &AccountConfig,
account: &Account,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut P,

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use himalaya_lib::account::Format;
use himalaya_lib::account::TextPlainFormat;
use std::io;
use termcolor::{self, StandardStream};
@ -12,6 +12,6 @@ pub trait PrintTable {
}
pub struct PrintTableOpts<'a> {
pub format: &'a Format,
pub format: &'a TextPlainFormat,
pub max_width: Option<usize>,
}

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Result};
use himalaya_lib::{account::AccountConfig, msg::Msg};
use himalaya_lib::{account::Account, msg::Msg};
use lettre::{
self,
transport::smtp::{
@ -13,11 +13,11 @@ use std::convert::TryInto;
use crate::output::pipe_cmd;
pub trait SmtpService {
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
fn send(&mut self, account: &Account, msg: &Msg) -> Result<Vec<u8>>;
}
pub struct LettreService<'a> {
account: &'a AccountConfig,
account: &'a Account,
transport: Option<SmtpTransport>,
}
@ -56,7 +56,7 @@ impl LettreService<'_> {
}
impl SmtpService for LettreService<'_> {
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>> {
fn send(&mut self, account: &Account, msg: &Msg) -> Result<Vec<u8>> {
let mut raw_msg = msg.into_sendable_msg(account)?.formatted();
let envelope: lettre::address::Envelope =
@ -76,8 +76,8 @@ impl SmtpService for LettreService<'_> {
}
}
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
fn from(account: &'a AccountConfig) -> Self {
impl<'a> From<&'a Account> for LettreService<'a> {
fn from(account: &'a Account) -> Self {
Self {
account,
transport: None,

View file

@ -1,6 +1,6 @@
use anyhow::{Context, Result};
use himalaya_lib::{
account::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER},
account::{Account, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER},
backend::Backend,
msg::{local_draft_path, remove_local_draft, Msg, TplOverride},
};
@ -39,7 +39,7 @@ pub fn open_with_draft() -> Result<String> {
open_with_tpl(tpl)
}
fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &AccountConfig) -> Result<Msg> {
fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &Account) -> Result<Msg> {
let tpl = msg.to_tpl(tpl, account)?;
let tpl = open_with_tpl(tpl)?;
Msg::from_tpl(&tpl).context("cannot parse message from template")
@ -48,7 +48,7 @@ fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &AccountConfig) -
pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
mut msg: Msg,
tpl: TplOverride,
account: &AccountConfig,
account: &Account,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,

View file

@ -5,7 +5,7 @@
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use anyhow::{Context, Result};
use himalaya_lib::account::Format;
use himalaya_lib::account::TextPlainFormat;
use log::trace;
use termcolor::{Color, ColorSpec};
use terminal_size;
@ -169,11 +169,11 @@ where
/// Writes the table to the writer.
fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
let is_format_flowed = matches!(opts.format, Format::Flowed);
let is_format_flowed = matches!(opts.format, TextPlainFormat::Flowed);
let max_width = match opts.format {
Format::Fixed(width) => opts.max_width.unwrap_or(*width),
Format::Flowed => 0,
Format::Auto => opts
TextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width),
TextPlainFormat::Flowed => 0,
TextPlainFormat::Auto => opts
.max_width
.or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize))
.unwrap_or(DEFAULT_TERM_WIDTH),

View file

@ -1,52 +1,78 @@
//! Account config module.
//!
//! This module contains the representation of the user account.
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, info, trace};
use mailparse::MailAddr;
use serde::Deserialize;
use shellexpand;
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf, result};
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf};
use thiserror::Error;
use crate::process;
use crate::process::{self, ProcessError};
use super::*;
#[derive(Error, Debug)]
pub enum Error {
#[error("cannot run encrypt file command")]
RunEncryptFileCmdError(#[source] process::Error),
#[error("cannot find pgp encrypt file command from config")]
FindPgpEncryptFileCmdError,
#[error("cannot find pgp decrypt file command from config")]
FindPgpDecryptFileCmdError,
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";
#[derive(Debug, Error)]
pub enum AccountError {
#[error("cannot encrypt file using pgp")]
EncryptFileError(#[source] ProcessError),
#[error("cannot find encrypt file command from config file")]
EncryptFileMissingCmdError,
#[error("cannot decrypt file using pgp")]
DecryptFileError(#[source] ProcessError),
#[error("cannot find decrypt file command from config file")]
DecryptFileMissingCmdError,
#[error("cannot get smtp password")]
GetSmtpPasswdError(#[source] ProcessError),
#[error("cannot get smtp password: password is empty")]
GetSmtpPasswdEmptyError,
#[cfg(feature = "imap-backend")]
#[error("cannot get imap password")]
GetImapPasswdError(#[source] ProcessError),
#[cfg(feature = "imap-backend")]
#[error("cannot get imap password: password is empty")]
GetImapPasswdEmptyError,
#[error("cannot find default account")]
FindDefaultAccountError,
#[error("cannot find account \"{0}\"")]
#[error("cannot find account {0}")]
FindAccountError(String),
#[error("cannot shell expand")]
ShellExpandError(#[from] shellexpand::LookupError<env::VarError>),
#[error("cannot parse account address")]
ParseAccountAddressError(#[from] mailparse::MailParseError),
#[error("cannot find account address from \"{0}\"")]
FindAccountAddressError(String),
#[error("cannot parse download file name from \"{0}\"")]
ParseDownloadFileNameError(PathBuf),
#[error("cannot find password")]
FindPasswordError,
#[error("cannot get smtp password")]
GetSmtpPasswdError(#[source] process::Error),
#[error("cannot get imap password")]
GetImapPasswdError(#[source] process::Error),
#[error("cannot decrypt pgp file")]
DecryptPgpFileError(#[source] process::Error),
#[error("cannot run notify command")]
RunNotifyCmdError(#[source] process::Error),
}
#[error("cannot parse account address {0}")]
ParseAccountAddrError(#[source] mailparse::MailParseError, String),
#[error("cannot find account address in {0}")]
ParseAccountAddrNotFoundError(String),
pub type Result<T> = result::Result<T, Error>;
#[cfg(feature = "maildir-backend")]
#[error("cannot expand maildir path")]
ExpandMaildirPathError(#[source] shellexpand::LookupError<env::VarError>),
#[cfg(feature = "notmuch-backend")]
#[error("cannot expand notmuch path")]
ExpandNotmuchDatabasePathError(#[source] shellexpand::LookupError<env::VarError>),
#[error("cannot expand mailbox alias {1}")]
ExpandMboxAliasError(#[source] shellexpand::LookupError<env::VarError>, String),
#[error("cannot parse download file name from {0}")]
ParseDownloadFileNameError(PathBuf),
#[error("cannot start the notify mode")]
StartNotifyModeError(#[source] ProcessError),
}
/// Represents the user account.
#[derive(Debug, Default, Clone)]
pub struct AccountConfig {
pub struct Account {
/// Represents the name of the user account.
pub name: String,
/// Makes this account the default one.
@ -69,7 +95,7 @@ pub struct AccountConfig {
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,
pub format: TextPlainFormat,
/// Overrides the default headers displayed at the top of
/// the read message.
pub read_headers: Vec<String>,
@ -99,13 +125,13 @@ pub struct AccountConfig {
pub pgp_decrypt_cmd: Option<String>,
}
impl<'a> AccountConfig {
impl<'a> Account {
/// 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)> {
) -> Result<(Account, BackendConfig), AccountError> {
info!("begin: parsing account and backend configs from config and account name");
debug!("account name: {:?}", account_name.unwrap_or("default"));
@ -126,12 +152,12 @@ impl<'a> AccountConfig {
}
})
.map(|(name, account)| (name.to_owned(), account))
.ok_or_else(|| Error::FindDefaultAccountError),
.ok_or_else(|| AccountError::FindDefaultAccountError),
Some(name) => config
.accounts
.get(name)
.map(|account| (name.to_owned(), account))
.ok_or_else(|| Error::FindAccountError(name.to_owned())),
.ok_or_else(|| AccountError::FindAccountError(name.to_owned())),
}?;
let base_account = account.to_base();
@ -175,7 +201,7 @@ impl<'a> AccountConfig {
.or_else(|| sig.map(|sig| sig.to_owned()))
.map(|sig| format!("{}{}", sig_delim, sig.trim_end()));
let account_config = AccountConfig {
let account_config = Account {
name,
display_name: base_account
.name
@ -234,13 +260,17 @@ impl<'a> AccountConfig {
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => {
BackendConfig::Maildir(MaildirBackendConfig {
maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(),
maildir_dir: shellexpand::full(&config.maildir_dir)
.map_err(AccountError::ExpandMaildirPathError)?
.to_string()
.into(),
})
}
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => {
BackendConfig::Notmuch(NotmuchBackendConfig {
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)?
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)
.map_err(AccountError::ExpandNotmuchDatabasePathError)?
.to_string()
.into(),
})
@ -253,7 +283,7 @@ impl<'a> AccountConfig {
}
/// Builds the full RFC822 compliant address of the user account.
pub fn address(&self) -> Result<MailAddr> {
pub fn address(&self) -> Result<MailAddr, AccountError> {
let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c));
let addr = if self.display_name.is_empty() {
self.email.clone()
@ -264,19 +294,21 @@ impl<'a> AccountConfig {
format!("{} <{}>", self.display_name, self.email)
};
Ok(mailparse::addrparse(&addr)?
Ok(mailparse::addrparse(&addr)
.map_err(|err| AccountError::ParseAccountAddrError(err, addr.to_owned()))?
.first()
.ok_or_else(|| Error::FindAccountAddressError(addr.into()))?
.ok_or_else(|| AccountError::ParseAccountAddrNotFoundError(addr.to_owned()))?
.clone())
}
/// Builds the user account SMTP credentials.
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = process::run_cmd(&self.smtp_passwd_cmd).map_err(Error::GetSmtpPasswdError)?;
pub fn smtp_creds(&self) -> Result<SmtpCredentials, AccountError> {
let passwd =
process::run(&self.smtp_passwd_cmd).map_err(AccountError::GetSmtpPasswdError)?;
let passwd = passwd
.lines()
.next()
.ok_or_else(|| Error::FindPasswordError)?;
.ok_or_else(|| AccountError::GetSmtpPasswdEmptyError)?;
Ok(SmtpCredentials::new(
self.smtp_login.to_owned(),
@ -285,27 +317,30 @@ impl<'a> AccountConfig {
}
/// Encrypts a file.
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<String> {
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<String, AccountError> {
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
Ok(process::run_cmd(&encrypt_file_cmd).map_err(Error::RunEncryptFileCmdError)?)
Ok(process::run(&encrypt_file_cmd).map_err(AccountError::EncryptFileError)?)
} else {
Err(Error::FindPgpEncryptFileCmdError)
Err(AccountError::EncryptFileMissingCmdError)
}
}
/// Decrypts a file.
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<String> {
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<String, AccountError> {
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
let decrypt_file_cmd = format!("{} {:?}", cmd, path);
Ok(process::run_cmd(&decrypt_file_cmd).map_err(Error::DecryptPgpFileError)?)
Ok(process::run(&decrypt_file_cmd).map_err(AccountError::DecryptFileError)?)
} else {
Err(Error::FindPgpDecryptFileCmdError)
Err(AccountError::DecryptFileMissingCmdError)
}
}
/// Gets the download path from a file name.
pub fn get_download_file_path<S: AsRef<str>>(&self, file_name: S) -> Result<PathBuf> {
pub fn get_download_file_path<S: AsRef<str>>(
&self,
file_name: S,
) -> Result<PathBuf, AccountError> {
let file_path = self.downloads_dir.join(file_name.as_ref());
self.get_unique_download_file_path(&file_path, |path, _count| path.is_file())
}
@ -316,7 +351,7 @@ impl<'a> AccountConfig {
&self,
original_file_path: &PathBuf,
is_file: impl Fn(&PathBuf, u8) -> bool,
) -> Result<PathBuf> {
) -> Result<PathBuf, AccountError> {
let mut count = 0;
let file_ext = original_file_path
.extension()
@ -332,7 +367,9 @@ impl<'a> AccountConfig {
.file_stem()
.and_then(OsStr::to_str)
.map(|fstem| format!("{}_{}{}", fstem, count, file_ext))
.ok_or_else(|| Error::ParseDownloadFileNameError(file_path.to_owned()))?,
.ok_or_else(|| {
AccountError::ParseDownloadFileNameError(file_path.to_owned())
})?,
));
}
@ -340,7 +377,7 @@ impl<'a> AccountConfig {
}
/// Runs the notify command.
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<(), AccountError> {
let subject = subject.as_ref();
let sender = sender.as_ref();
@ -351,19 +388,21 @@ impl<'a> AccountConfig {
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd);
process::run_cmd(&cmd).map_err(Error::RunNotifyCmdError)?;
process::run(&cmd).map_err(AccountError::StartNotifyModeError)?;
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> {
pub fn get_mbox_alias(&self, mbox: &str) -> Result<String, AccountError> {
let mbox = self
.mailboxes
.get(&mbox.trim().to_lowercase())
.map(|s| s.as_str())
.unwrap_or(mbox);
let mbox = shellexpand::full(mbox).map(String::from)?;
let mbox = shellexpand::full(mbox)
.map(String::from)
.map_err(|err| AccountError::ExpandMboxAliasError(err, mbox.to_owned()))?;
Ok(mbox)
}
}
@ -400,12 +439,13 @@ pub struct ImapBackendConfig {
#[cfg(feature = "imap-backend")]
impl ImapBackendConfig {
/// Gets the IMAP password of the user account.
pub fn imap_passwd(&self) -> Result<String> {
let passwd = process::run_cmd(&self.imap_passwd_cmd).map_err(Error::GetImapPasswdError)?;
pub fn imap_passwd(&self) -> Result<String, AccountError> {
let passwd =
process::run(&self.imap_passwd_cmd).map_err(AccountError::GetImapPasswdError)?;
let passwd = passwd
.lines()
.next()
.ok_or_else(|| Error::FindPasswordError)?;
.ok_or_else(|| AccountError::GetImapPasswdEmptyError)?;
Ok(passwd.to_string())
}
}
@ -426,13 +466,39 @@ pub struct NotmuchBackendConfig {
pub notmuch_database_dir: PathBuf,
}
/// Represents the text/plain format as defined in the [RFC2646].
///
/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(tag = "type", content = "width", rename_all = "lowercase")]
pub enum TextPlainFormat {
// 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 TextPlainFormat {
fn default() -> Self {
Self::Auto
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Hooks {
pub pre_send: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_get_unique_download_file_path() {
let account = AccountConfig::default();
let account = Account::default();
let path = PathBuf::from("downloads/file.ext");
// When file path is unique

View file

@ -1,7 +1,12 @@
//! Deserialized account config module.
//!
//! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file.
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use crate::account::{Format, Hooks};
use super::*;
pub trait ToDeserializedBaseAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig;
@ -53,9 +58,8 @@ macro_rules! make_account_config {
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 text/plain format.
pub format: Option<TextPlainFormat>,
/// Represents the default headers displayed at the top of
/// the read message.
#[serde(default)]

View file

@ -1,17 +1,15 @@
//! Deserialized config module.
//!
//! This module contains the raw deserialized representation of the
//! user configuration file.
use log::{debug, trace};
use serde::Deserialize;
use std::{collections::HashMap, env, fs, io, path::PathBuf, result};
use thiserror::Error;
use toml;
use crate::account::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";
use super::*;
#[derive(Error, Debug)]
pub enum DeserializeConfigError {

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

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>,
}

View file

@ -1,16 +1,12 @@
pub mod deserialized_config;
pub use deserialized_config::*;
//! Account module.
//!
//! This module contains everything related to the user configuration.
pub mod deserialized_account_config;
pub use deserialized_account_config::*;
// pub mod account_handlers;
pub mod account_config;
mod account_config;
pub use account_config::*;
pub mod format;
pub use format::*;
mod deserialized_config;
pub use deserialized_config::*;
pub mod hooks;
pub use hooks::*;
mod deserialized_account_config;
pub use deserialized_account_config::*;

View file

@ -27,7 +27,7 @@ pub enum Error {
ImapError(#[from] super::imap::Error),
#[error(transparent)]
AccountError(#[from] account::Error),
AccountError(#[from] account::AccountError),
#[error(transparent)]
MsgError(#[from] msg::Error),

View file

@ -78,7 +78,7 @@ pub enum Error {
LogoutError(#[source] imap::Error),
#[error(transparent)]
AccountError(#[from] account::Error),
AccountError(#[from] account::AccountError),
#[error(transparent)]
MsgError(#[from] msg::Error),
}

View file

@ -8,26 +8,26 @@ use native_tls::{TlsConnector, TlsStream};
use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread};
use crate::{
account::{AccountConfig, ImapBackendConfig},
account::{Account, ImapBackendConfig},
backend::{
backend::Result, from_imap_fetch, from_imap_fetches,
imap::msg_sort_criterion::SortCriteria, imap::Error, into_imap_flags, Backend,
},
mbox::{Mbox, Mboxes},
msg::{Envelopes, Flags, Msg},
process::run_cmd,
process,
};
type ImapSess = imap::Session<TlsStream<TcpStream>>;
pub struct ImapBackend<'a> {
account_config: &'a AccountConfig,
account_config: &'a Account,
imap_config: &'a ImapBackendConfig,
sess: Option<ImapSess>,
}
impl<'a> ImapBackend<'a> {
pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self {
pub fn new(account_config: &'a Account, imap_config: &'a ImapBackendConfig) -> Self {
Self {
account_config,
imap_config,
@ -187,7 +187,7 @@ impl<'a> ImapBackend<'a> {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
let res = process::run(cmd);
debug!("{:?}", res);
})
});

View file

@ -7,7 +7,7 @@ use log::{debug, info, trace};
use std::{env, ffi::OsStr, fs, path::PathBuf};
use crate::{
account::{AccountConfig, MaildirBackendConfig},
account::{Account, MaildirBackendConfig},
backend::{backend::Result, maildir_envelopes, maildir_flags, Backend, IdMapper},
mbox::{Mbox, Mboxes},
msg::{Envelopes, Flags, Msg},
@ -17,15 +17,12 @@ use super::MaildirError;
/// Represents the maildir backend.
pub struct MaildirBackend<'a> {
account_config: &'a AccountConfig,
account_config: &'a Account,
mdir: maildir::Maildir,
}
impl<'a> MaildirBackend<'a> {
pub fn new(
account_config: &'a AccountConfig,
maildir_config: &'a MaildirBackendConfig,
) -> Self {
pub fn new(account_config: &'a Account, maildir_config: &'a MaildirBackendConfig) -> Self {
Self {
account_config,
mdir: maildir_config.maildir_dir.clone().into(),

View file

@ -2,7 +2,7 @@ use log::{debug, info, trace};
use std::fs;
use crate::{
account::{AccountConfig, NotmuchBackendConfig},
account::{Account, NotmuchBackendConfig},
backend::{
backend::Result, notmuch_envelopes, Backend, IdMapper, MaildirBackend, NotmuchError,
},
@ -12,7 +12,7 @@ use crate::{
/// Represents the Notmuch backend.
pub struct NotmuchBackend<'a> {
account_config: &'a AccountConfig,
account_config: &'a Account,
notmuch_config: &'a NotmuchBackendConfig,
pub mdir: &'a mut MaildirBackend<'a>,
db: notmuch::Database,
@ -20,7 +20,7 @@ pub struct NotmuchBackend<'a> {
impl<'a> NotmuchBackend<'a> {
pub fn new(
account_config: &'a AccountConfig,
account_config: &'a Account,
notmuch_config: &'a NotmuchBackendConfig,
mdir: &'a mut MaildirBackend<'a>,
) -> Result<NotmuchBackend<'a>> {

View file

@ -34,7 +34,7 @@ pub enum Error {
ParseAddressError(#[from] lettre::address::AddressError),
#[error(transparent)]
AccountError(#[from] account::Error),
AccountError(#[from] account::AccountError),
#[error("cannot get content type of multipart")]
GetMultipartContentTypeError,
@ -47,7 +47,7 @@ pub enum Error {
#[error("cannot write encrypted part to temporary file")]
WriteEncryptedPartBodyError(#[source] io::Error),
#[error("cannot write encrypted part to temporary file")]
DecryptPartError(#[source] account::Error),
DecryptPartError(#[source] account::AccountError),
#[error("cannot delete local draft: {1}")]
DeleteLocalDraftError(#[source] io::Error, path::PathBuf),

View file

@ -17,7 +17,7 @@ use tree_magic;
use uuid::Uuid;
use crate::{
account::{AccountConfig, DEFAULT_SIG_DELIM},
account::{Account, DEFAULT_SIG_DELIM},
msg::{
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, Addr,
Addrs, BinaryPart, Error, Part, Parts, Result, TextPlainPart, TplOverride,
@ -166,7 +166,7 @@ impl Msg {
}
}
pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result<Self> {
pub fn into_reply(mut self, all: bool, account: &Account) -> Result<Self> {
let account_addr = account.address()?;
// In-Reply-To
@ -264,7 +264,7 @@ impl Msg {
Ok(self)
}
pub fn into_forward(mut self, account: &AccountConfig) -> Result<Self> {
pub fn into_forward(mut self, account: &Account) -> Result<Self> {
let account_addr = account.address()?;
let prev_subject = self.subject.to_owned();
@ -380,7 +380,7 @@ impl Msg {
}
}
pub fn to_tpl(&self, opts: TplOverride, account: &AccountConfig) -> Result<String> {
pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> Result<String> {
let account_addr: Addrs = vec![account.address()?].into();
let mut tpl = String::default();
@ -463,10 +463,10 @@ impl Msg {
let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).map_err(Error::ParseTplError)?;
info!("end: building message from template");
Self::from_parsed_mail(parsed_mail, &AccountConfig::default())
Self::from_parsed_mail(parsed_mail, &Account::default())
}
pub fn into_sendable_msg(&self, account: &AccountConfig) -> Result<lettre::Message> {
pub fn into_sendable_msg(&self, account: &Account) -> Result<lettre::Message> {
let mut msg_builder = lettre::Message::builder()
.message_id(self.message_id.to_owned())
.subject(self.subject.to_owned());
@ -551,7 +551,7 @@ impl Msg {
pub fn from_parsed_mail(
parsed_mail: mailparse::ParsedMail<'_>,
config: &AccountConfig,
config: &Account,
) -> Result<Self> {
trace!(">> build message from parsed mail");
trace!("parsed mail: {:?}", parsed_mail);
@ -623,7 +623,7 @@ impl Msg {
&self,
text_mime: &str,
headers: Vec<&str>,
config: &AccountConfig,
config: &Account,
) -> Result<String> {
let mut all_headers = vec![];
for h in config.read_headers.iter() {
@ -750,10 +750,10 @@ mod tests {
#[test]
fn test_into_reply() {
let config = AccountConfig {
let config = Account {
display_name: "Test".into(),
email: "test-account@local".into(),
..AccountConfig::default()
..Account::default()
};
// Checks that:
@ -889,7 +889,7 @@ mod tests {
#[test]
fn test_to_readable() {
let config = AccountConfig::default();
let config = Account::default();
let msg = Msg {
parts: Parts(vec![Part::TextPlain(TextPlainPart {
content: String::from("hello, world!"),
@ -952,14 +952,14 @@ mod tests {
.unwrap()
);
let config = AccountConfig {
let config = Account {
read_headers: vec![
"CusTOM-heaDER".into(),
"Subject".into(),
"from".into(),
"cc".into(),
],
..AccountConfig::default()
..Account::default()
};
// header present but empty in msg headers, empty config
assert_eq!(

View file

@ -6,7 +6,7 @@ use std::{
};
use uuid::Uuid;
use crate::{account::AccountConfig, msg};
use crate::{account::Account, msg};
#[derive(Debug, Clone, Default, Serialize)]
pub struct TextPlainPart {
@ -50,7 +50,7 @@ impl Parts {
}
pub fn from_parsed_mail<'a>(
account: &'a AccountConfig,
account: &'a Account,
part: &'a mailparse::ParsedMail<'a>,
) -> msg::Result<Self> {
let mut parts = vec![];
@ -80,7 +80,7 @@ impl DerefMut for Parts {
}
fn build_parts_map_rec(
account: &AccountConfig,
account: &Account,
parsed_mail: &mailparse::ParsedMail,
parts: &mut Vec<Part>,
) -> msg::Result<()> {
@ -137,7 +137,7 @@ fn build_parts_map_rec(
Ok(())
}
fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> msg::Result<String> {
fn decrypt_part(account: &Account, msg: &mailparse::ParsedMail) -> msg::Result<String> {
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
let msg_body = msg
.get_body()

View file

@ -1,30 +1,34 @@
//! Process module.
//!
//! This module contains cross platform helpers around the
//! `std::process` crate.
use log::{debug, trace};
use std::{io, process, result, string};
use std::{io, process::Command, string};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("cannot run command: {1}")]
#[derive(Debug, Error)]
pub enum ProcessError {
#[error("cannot run command {1:?}")]
RunCmdError(#[source] io::Error, String),
#[error("cannot parse command output")]
ParseCmdOutputError(#[source] string::FromUtf8Error),
}
pub type Result<T> = result::Result<T, Error>;
pub fn run_cmd(cmd: &str) -> Result<String> {
trace!(">> run command");
pub fn run(cmd: &str) -> Result<String, ProcessError> {
debug!(">> run command");
debug!("command: {}", cmd);
let output = if cfg!(target_os = "windows") {
process::Command::new("cmd").args(&["/C", cmd]).output()
Command::new("cmd").args(&["/C", cmd]).output()
} else {
process::Command::new("sh").arg("-c").arg(cmd).output()
Command::new("sh").arg("-c").arg(cmd).output()
};
let output = output.map_err(|err| Error::RunCmdError(err, cmd.to_string()))?;
let output = String::from_utf8(output.stdout).map_err(Error::ParseCmdOutputError)?;
let output = output.map_err(|err| ProcessError::RunCmdError(err, cmd.to_string()))?;
let output = String::from_utf8(output.stdout).map_err(ProcessError::ParseCmdOutputError)?;
debug!("command output: {}", output);
trace!("<< run command");
trace!("command output: {}", output);
debug!("<< run command");
Ok(output)
}

View file

@ -1,6 +1,6 @@
#[cfg(feature = "imap-backend")]
use himalaya_lib::{
account::{AccountConfig, ImapBackendConfig},
account::{Account, ImapBackendConfig},
backend::{Backend, ImapBackend},
};
@ -8,14 +8,14 @@ use himalaya_lib::{
#[test]
fn test_imap_backend() {
// configure accounts
let account_config = AccountConfig {
let account_config = Account {
smtp_host: "localhost".into(),
smtp_port: 3465,
smtp_starttls: false,
smtp_insecure: true,
smtp_login: "inbox@localhost".into(),
smtp_passwd_cmd: "echo 'password'".into(),
..AccountConfig::default()
..Account::default()
};
let imap_config = ImapBackendConfig {
imap_host: "localhost".into(),

View file

@ -2,7 +2,7 @@ use maildir::Maildir;
use std::{collections::HashMap, env, fs, iter::FromIterator};
use himalaya_lib::{
account::{AccountConfig, MaildirBackendConfig},
account::{Account, MaildirBackendConfig},
backend::{Backend, MaildirBackend},
msg::Flag,
};
@ -19,9 +19,9 @@ fn test_maildir_backend() {
mdir_sub.create_dirs().unwrap();
// configure accounts
let account_config = AccountConfig {
let account_config = Account {
mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]),
..AccountConfig::default()
..Account::default()
};
let mdir_config = MaildirBackendConfig {
maildir_dir: mdir.path().to_owned(),

View file

@ -3,7 +3,7 @@ use std::{collections::HashMap, env, fs, iter::FromIterator};
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::{
account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig},
account::{Account, MaildirBackendConfig, NotmuchBackendConfig},
backend::{Backend, MaildirBackend, NotmuchBackend},
};