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

This commit is contained in:
Clément DOUIN 2022-05-28 16:59:12 +02:00
parent 0e98def513
commit 3f5feed0ff
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
27 changed files with 219 additions and 103 deletions

35
Cargo.lock generated
View file

@ -441,6 +441,7 @@ dependencies = [
"convert_case", "convert_case",
"env_logger", "env_logger",
"erased-serde", "erased-serde",
"himalaya-lib",
"html-escape", "html-escape",
"imap", "imap",
"imap-proto", "imap-proto",
@ -468,6 +469,20 @@ dependencies = [
[[package]] [[package]]
name = "himalaya-lib" name = "himalaya-lib"
version = "0.1.0" version = "0.1.0"
dependencies = [
"imap",
"imap-proto",
"lettre",
"log",
"maildir",
"mailparse",
"md5",
"notmuch",
"serde",
"shellexpand",
"thiserror",
"toml",
]
[[package]] [[package]]
name = "hostname" name = "hostname"
@ -1365,6 +1380,26 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "thiserror"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.44" version = "0.1.44"

View file

@ -31,6 +31,7 @@ clap = { version = "2.33.3", default-features = false, features = ["suggestions"
convert_case = "0.5.0" convert_case = "0.5.0"
env_logger = "0.8.3" env_logger = "0.8.3"
erased-serde = "0.3.18" erased-serde = "0.3.18"
himalaya-lib = { path = "../lib" }
html-escape = "0.2.9" html-escape = "0.2.9"
lettre = { version = "0.10.0-rc.1", features = ["serde"] } lettre = { version = "0.10.0-rc.1", features = ["serde"] }
log = "0.4.14" log = "0.4.14"

View file

@ -3,6 +3,7 @@
//! This module contains the definition of the IMAP backend. //! This module contains the definition of the IMAP backend.
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::{AccountConfig, ImapBackendConfig};
use log::{debug, log_enabled, trace, Level}; use log::{debug, log_enabled, trace, Level};
use native_tls::{TlsConnector, TlsStream}; use native_tls::{TlsConnector, TlsStream};
use std::{ use std::{
@ -16,7 +17,6 @@ use crate::{
backends::{ backends::{
imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes, imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes,
}, },
config::{AccountConfig, ImapBackendConfig},
mbox::Mboxes, mbox::Mboxes,
msg::{Envelopes, Msg}, msg::{Envelopes, Msg},
output::run_cmd, output::run_cmd,

View file

@ -4,12 +4,12 @@
//! traits implementation. //! traits implementation.
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::{AccountConfig, MaildirBackendConfig};
use log::{debug, info, trace}; use log::{debug, info, trace};
use std::{convert::TryInto, env, fs, path::PathBuf}; use std::{convert::TryInto, env, fs, path::PathBuf};
use crate::{ use crate::{
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
config::{AccountConfig, MaildirBackendConfig},
mbox::Mboxes, mbox::Mboxes,
msg::{Envelopes, Msg}, msg::{Envelopes, Msg},
}; };

View file

@ -1,11 +1,11 @@
use std::{convert::TryInto, fs}; use std::{convert::TryInto, fs};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::{AccountConfig, NotmuchBackendConfig};
use log::{debug, info, trace}; use log::{debug, info, trace};
use crate::{ use crate::{
backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes},
config::{AccountConfig, NotmuchBackendConfig},
mbox::Mboxes, mbox::Mboxes,
msg::{Envelopes, Msg}, msg::{Envelopes, Msg},
}; };

View file

@ -12,8 +12,9 @@ use std::{
ops::Deref, ops::Deref,
}; };
use himalaya_lib::account::DeserializedAccountConfig;
use crate::{ use crate::{
config::DeserializedAccountConfig,
output::{PrintTable, PrintTableOpts, WriteColor}, output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table}, ui::{Cell, Row, Table},
}; };

View file

@ -3,10 +3,11 @@
//! This module gathers all account actions triggered by the CLI. //! This module gathers all account actions triggered by the CLI.
use anyhow::Result; use anyhow::Result;
use himalaya_lib::account::{AccountConfig, DeserializedConfig};
use log::{info, trace}; use log::{info, trace};
use crate::{ use crate::{
config::{AccountConfig, Accounts, DeserializedConfig}, config::Accounts,
output::{PrintTableOpts, PrinterService}, output::{PrintTableOpts, PrinterService},
}; };
@ -36,13 +37,13 @@ pub fn list<'a, P: PrinterService>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use himalaya_lib::account::{
AccountConfig, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig,
};
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator}; use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
use termcolor::ColorSpec; use termcolor::ColorSpec;
use crate::{ use crate::output::{Print, PrintTable, WriteColor};
config::{DeserializedAccountConfig, DeserializedImapAccountConfig},
output::{Print, PrintTable, WriteColor},
};
use super::*; use super::*;

View file

@ -107,12 +107,6 @@ pub mod smtp {
} }
pub mod config { pub mod config {
pub mod deserialized_config;
pub use deserialized_config::*;
pub mod deserialized_account_config;
pub use deserialized_account_config::*;
pub mod config_args; pub mod config_args;
pub mod account_args; pub mod account_args;
@ -120,15 +114,6 @@ pub mod config {
pub mod account; pub mod account;
pub use account::*; pub use account::*;
pub mod account_config;
pub use account_config::*;
pub mod format;
pub use format::*;
pub mod hooks;
pub use hooks::*;
} }
pub mod compl; pub mod compl;

View file

@ -1,14 +1,14 @@
use anyhow::Result; use anyhow::Result;
use himalaya_lib::account::{
AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER,
};
use std::{convert::TryFrom, env}; use std::{convert::TryFrom, env};
use url::Url; use url::Url;
use himalaya::{ use himalaya::{
backends::Backend, backends::Backend,
compl::{compl_args, compl_handlers}, compl::{compl_args, compl_handlers},
config::{ config::{account_args, account_handlers, config_args},
account_args, account_handlers, config_args, AccountConfig, BackendConfig,
DeserializedConfig, DEFAULT_INBOX_FOLDER,
},
mbox::{mbox_args, mbox_handlers}, mbox::{mbox_args, mbox_handlers},
msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers}, msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers},
output::{output_args, OutputFmt, StdoutPrinter}, output::{output_args, OutputFmt, StdoutPrinter},
@ -22,7 +22,9 @@ use himalaya::backends::{imap_args, imap_handlers, ImapBackend};
use himalaya::backends::MaildirBackend; use himalaya::backends::MaildirBackend;
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig}; use himalaya::backends::NotmuchBackend;
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::account::MaildirBackendConfig;
fn create_app<'a>() -> clap::App<'a, 'a> { fn create_app<'a>() -> clap::App<'a, 'a> {
let app = clap::App::new(env!("CARGO_PKG_NAME")) let app = clap::App::new(env!("CARGO_PKG_NAME"))

View file

@ -3,11 +3,11 @@
//! This module gathers all mailbox actions triggered by the CLI. //! This module gathers all mailbox actions triggered by the CLI.
use anyhow::Result; use anyhow::Result;
use himalaya_lib::account::AccountConfig;
use log::{info, trace}; use log::{info, trace};
use crate::{ use crate::{
backends::Backend, backends::Backend,
config::AccountConfig,
output::{PrintTableOpts, PrinterService}, output::{PrintTableOpts, PrinterService},
}; };

View file

@ -2,6 +2,9 @@ use ammonia;
use anyhow::{anyhow, Context, Error, Result}; use anyhow::{anyhow, Context, Error, Result};
use chrono::{DateTime, Local, TimeZone, Utc}; use chrono::{DateTime, Local, TimeZone, Utc};
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use himalaya_lib::account::{
AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM,
};
use html_escape; use html_escape;
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
use log::{info, trace, warn}; use log::{info, trace, warn};
@ -18,7 +21,6 @@ use uuid::Uuid;
use crate::{ use crate::{
backends::Backend, backends::Backend,
config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM},
msg::{ msg::{
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils, from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils,
Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride, Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride,

View file

@ -4,6 +4,7 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use atty::Stream; use atty::Stream;
use himalaya_lib::account::{AccountConfig, DEFAULT_SENT_FOLDER};
use log::{debug, info, trace}; use log::{debug, info, trace};
use mailparse::addrparse; use mailparse::addrparse;
use std::{ use std::{
@ -15,7 +16,6 @@ use url::Url;
use crate::{ use crate::{
backends::Backend, backends::Backend,
config::{AccountConfig, DEFAULT_SENT_FOLDER},
msg::{Msg, Part, Parts, TextPlainPart}, msg::{Msg, Part, Parts, TextPlainPart},
output::{PrintTableOpts, PrinterService}, output::{PrintTableOpts, PrinterService},
smtp::SmtpService, smtp::SmtpService,

View file

@ -1,4 +1,5 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use himalaya_lib::account::AccountConfig;
use mailparse::MailHeaderMap; use mailparse::MailHeaderMap;
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
@ -7,8 +8,6 @@ use std::{
}; };
use uuid::Uuid; use uuid::Uuid;
use crate::config::AccountConfig;
#[derive(Debug, Clone, Default, Serialize)] #[derive(Debug, Clone, Default, Serialize)]
pub struct TextPlainPart { pub struct TextPlainPart {
pub content: String, pub content: String,

View file

@ -4,11 +4,11 @@
use anyhow::Result; use anyhow::Result;
use atty::Stream; use atty::Stream;
use himalaya_lib::account::AccountConfig;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use crate::{ use crate::{
backends::Backend, backends::Backend,
config::AccountConfig,
msg::{Msg, TplOverride}, msg::{Msg, TplOverride},
output::PrinterService, output::PrinterService,
smtp::SmtpService, smtp::SmtpService,

View file

@ -1,9 +1,8 @@
use anyhow::Result; use anyhow::Result;
use himalaya_lib::account::Format;
use std::io; use std::io;
use termcolor::{self, StandardStream}; use termcolor::{self, StandardStream};
use crate::config::Format;
pub trait WriteColor: io::Write + termcolor::WriteColor {} pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {} impl WriteColor for StandardStream {}

View file

@ -1,4 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use himalaya_lib::account::AccountConfig;
use lettre::{ use lettre::{
self, self,
transport::smtp::{ transport::smtp::{
@ -9,7 +10,7 @@ use lettre::{
}; };
use std::convert::TryInto; use std::convert::TryInto;
use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd}; use crate::{msg::Msg, output::pipe_cmd};
pub trait SmtpService { pub trait SmtpService {
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>; fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
@ -62,7 +63,7 @@ impl SmtpService for LettreService<'_> {
if let Some(cmd) = account.hooks.pre_send.as_deref() { if let Some(cmd) = account.hooks.pre_send.as_deref() {
for cmd in cmd.split('|') { for cmd in cmd.split('|') {
raw_msg = pipe_cmd(cmd.trim(), &raw_msg) raw_msg = pipe_cmd(cmd.trim(), &raw_msg)
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))? .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?;
} }
let parsed_mail = mailparse::parse_mail(&raw_msg)?; let parsed_mail = mailparse::parse_mail(&raw_msg)?;
Msg::from_parsed_mail(parsed_mail, account)?.try_into() Msg::from_parsed_mail(parsed_mail, account)?.try_into()

View file

@ -5,15 +5,13 @@
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder //! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use himalaya_lib::account::Format;
use log::trace; use log::trace;
use termcolor::{Color, ColorSpec}; use termcolor::{Color, ColorSpec};
use terminal_size; use terminal_size;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::{ use crate::output::{Print, PrintTableOpts, WriteColor};
config::Format,
output::{Print, PrintTableOpts, WriteColor},
};
/// Defines the default terminal size. /// Defines the default terminal size.
/// This is used when the size cannot be determined by the `terminal_size` crate. /// This is used when the size cannot be determined by the `terminal_size` crate.

View file

@ -3,4 +3,24 @@ name = "himalaya-lib"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[features]
imap-backend = ["imap", "imap-proto"]
maildir-backend = ["maildir", "md5"]
notmuch-backend = ["notmuch", "maildir-backend"]
default = ["imap-backend", "maildir-backend"]
[dependencies] [dependencies]
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
log = "0.4.14"
mailparse = "0.13.6"
serde = { version = "1.0.118", features = ["derive"] }
shellexpand = "2.1.0"
thiserror = "1.0.31"
toml = "0.5.8"
# [optional]
imap = { version = "=3.0.0-alpha.4", optional = true }
imap-proto = { version = "0.14.3", optional = true }
maildir = { version = "0.6.1", optional = true }
md5 = { version = "0.7.0", optional = true }
notmuch = { version = "0.7.1", optional = true }

View file

@ -1,10 +1,35 @@
use anyhow::{anyhow, Context, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, info, trace}; use log::{debug, info, trace};
use mailparse::MailAddr; use mailparse::MailAddr;
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf}; use shellexpand;
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf, result};
use thiserror::Error;
use crate::{config::*, output::run_cmd}; use crate::process::{run_cmd, ProcessError};
use super::*;
#[derive(Error, Debug)]
pub enum AccountError {
#[error("cannot find default account")]
FindDefaultAccountError,
#[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(transparent)]
RunCmdError(#[from] ProcessError),
}
type Result<T> = result::Result<T, AccountError>;
/// Represents the user account. /// Represents the user account.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -62,7 +87,8 @@ pub struct AccountConfig {
} }
impl<'a> AccountConfig { impl<'a> AccountConfig {
/// tries to create an account from a config and an optional account name. /// Tries to create an account from a config and an optional
/// account name.
pub fn from_config_and_opt_account_name( pub fn from_config_and_opt_account_name(
config: &'a DeserializedConfig, config: &'a DeserializedConfig,
account_name: Option<&str>, account_name: Option<&str>,
@ -87,12 +113,12 @@ impl<'a> AccountConfig {
} }
}) })
.map(|(name, account)| (name.to_owned(), account)) .map(|(name, account)| (name.to_owned(), account))
.ok_or_else(|| anyhow!("cannot find default account")), .ok_or_else(|| AccountError::FindDefaultAccountError),
Some(name) => config Some(name) => config
.accounts .accounts
.get(name) .get(name)
.map(|account| (name.to_owned(), account)) .map(|account| (name.to_owned(), account))
.ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)), .ok_or_else(|| AccountError::FindAccountError(name.to_owned())),
}?; }?;
let base_account = account.to_base(); let base_account = account.to_base();
@ -225,20 +251,19 @@ impl<'a> AccountConfig {
format!("{} <{}>", self.display_name, self.email) format!("{} <{}>", self.display_name, self.email)
}; };
Ok(mailparse::addrparse(&addr) Ok(mailparse::addrparse(&addr)?
.context(format!(
"cannot parse account address {:?}",
self.display_name
))?
.first() .first()
.ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))? .ok_or_else(|| AccountError::FindAccountAddressError(addr.into()))?
.clone()) .clone())
} }
/// Builds the user account SMTP credentials. /// Builds the user account SMTP credentials.
pub fn smtp_creds(&self) -> Result<SmtpCredentials> { pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?; let passwd = run_cmd(&self.smtp_passwd_cmd)?;
let passwd = passwd.lines().next().context("cannot find password")?; let passwd = passwd
.lines()
.next()
.ok_or_else(|| AccountError::FindPasswordError)?;
Ok(SmtpCredentials::new( Ok(SmtpCredentials::new(
self.smtp_login.to_owned(), self.smtp_login.to_owned(),
@ -250,10 +275,7 @@ impl<'a> AccountConfig {
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> { pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() { if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path); let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
run_cmd(&encrypt_file_cmd).map(Some).context(format!( Ok(run_cmd(&encrypt_file_cmd).map(Some)?)
"cannot run pgp encrypt command {:?}",
encrypt_file_cmd
))
} else { } else {
Ok(None) Ok(None)
} }
@ -263,10 +285,7 @@ impl<'a> AccountConfig {
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> { pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() { if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
let decrypt_file_cmd = format!("{} {:?}", cmd, path); let decrypt_file_cmd = format!("{} {:?}", cmd, path);
run_cmd(&decrypt_file_cmd).map(Some).context(format!( Ok(run_cmd(&decrypt_file_cmd).map(Some)?)
"cannot run pgp decrypt command {:?}",
decrypt_file_cmd
))
} else { } else {
Ok(None) Ok(None)
} }
@ -276,13 +295,10 @@ impl<'a> AccountConfig {
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> {
let file_path = self.downloads_dir.join(file_name.as_ref()); let file_path = self.downloads_dir.join(file_name.as_ref());
self.get_unique_download_file_path(&file_path, |path, _count| path.is_file()) self.get_unique_download_file_path(&file_path, |path, _count| path.is_file())
.context(format!(
"cannot get download file path of {:?}",
file_name.as_ref()
))
} }
/// Gets the unique download path from a file name by adding suffixes in case of name conflicts. /// Gets the unique download path from a file name by adding
/// suffixes in case of name conflicts.
pub fn get_unique_download_file_path( pub fn get_unique_download_file_path(
&self, &self,
original_file_path: &PathBuf, original_file_path: &PathBuf,
@ -303,7 +319,9 @@ impl<'a> AccountConfig {
.file_stem() .file_stem()
.and_then(OsStr::to_str) .and_then(OsStr::to_str)
.map(|fstem| format!("{}_{}{}", fstem, count, file_ext)) .map(|fstem| format!("{}_{}{}", fstem, count, file_ext))
.ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?, .ok_or_else(|| {
AccountError::ParseDownloadFileNameError(file_path.to_owned())
})?,
)); ));
} }
@ -323,7 +341,7 @@ impl<'a> AccountConfig {
.unwrap_or(default_cmd); .unwrap_or(default_cmd);
debug!("run command: {}", cmd); debug!("run command: {}", cmd);
run_cmd(&cmd).context("cannot run notify cmd")?; run_cmd(&cmd)?;
Ok(()) Ok(())
} }
@ -335,9 +353,8 @@ impl<'a> AccountConfig {
.get(&mbox.trim().to_lowercase()) .get(&mbox.trim().to_lowercase())
.map(|s| s.as_str()) .map(|s| s.as_str())
.unwrap_or(mbox); .unwrap_or(mbox);
shellexpand::full(mbox) let mbox = shellexpand::full(mbox).map(String::from)?;
.map(String::from) Ok(mbox)
.with_context(|| format!("cannot expand mailbox path {:?}", mbox))
} }
} }
@ -374,8 +391,11 @@ pub struct ImapBackendConfig {
impl ImapBackendConfig { impl ImapBackendConfig {
/// Gets the IMAP password of the user account. /// Gets the IMAP password of the user account.
pub fn imap_passwd(&self) -> Result<String> { pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?; let passwd = run_cmd(&self.imap_passwd_cmd)?;
let passwd = passwd.lines().next().context("cannot find password")?; let passwd = passwd
.lines()
.next()
.ok_or_else(|| AccountError::FindPasswordError)?;
Ok(passwd.to_string()) Ok(passwd.to_string())
} }
} }

View file

@ -1,7 +1,7 @@
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use crate::config::{Format, Hooks}; use crate::account::{Format, Hooks};
pub trait ToDeserializedBaseAccountConfig { pub trait ToDeserializedBaseAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig; fn to_base(&self) -> DeserializedBaseAccountConfig;

View file

@ -1,10 +1,10 @@
use anyhow::{Context, Result}; use log::{debug, trace};
use log::{debug, info, trace};
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, env, fs, path::PathBuf}; use std::{collections::HashMap, env, fs, io, path::PathBuf, result};
use thiserror::Error;
use toml; use toml;
use crate::config::DeserializedAccountConfig; use crate::account::DeserializedAccountConfig;
pub const DEFAULT_PAGE_SIZE: usize = 10; pub const DEFAULT_PAGE_SIZE: usize = 10;
pub const DEFAULT_SIG_DELIM: &str = "-- \n"; pub const DEFAULT_SIG_DELIM: &str = "-- \n";
@ -13,6 +13,18 @@ pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
pub const DEFAULT_SENT_FOLDER: &str = "Sent"; pub const DEFAULT_SENT_FOLDER: &str = "Sent";
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts"; pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
#[derive(Error, Debug)]
pub enum DeserializeConfigError {
#[error("cannot read config file")]
ReadConfigFile(#[from] io::Error),
#[error("cannot parse config file")]
ParseConfigFile(#[from] toml::de::Error),
#[error("cannot read environment variable")]
ReadEnvVar(#[from] env::VarError),
}
type Result<T> = result::Result<T, DeserializeConfigError>;
/// Represents the user config file. /// Represents the user config file.
#[derive(Debug, Default, Clone, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@ -42,32 +54,35 @@ pub struct DeserializedConfig {
impl DeserializedConfig { impl DeserializedConfig {
/// Tries to create a config from an optional path. /// Tries to create a config from an optional path.
pub fn from_opt_path(path: Option<&str>) -> Result<Self> { pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
info!("begin: try to parse config from path"); trace!(">> parse config from path");
debug!("path: {:?}", path); debug!("path: {:?}", path);
let path = path.map(|s| s.into()).unwrap_or(Self::path()?); let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?; let content = fs::read_to_string(path)?;
let config = toml::from_str(&content).context("cannot parse config file")?; let config = toml::from_str(&content)?;
info!("end: try to parse config from path");
trace!("config: {:?}", config); trace!("config: {:?}", config);
trace!("<< parse config from path");
Ok(config) Ok(config)
} }
/// Tries to get the XDG config file path from XDG_CONFIG_HOME environment variable. /// Tries to get the XDG config file path from XDG_CONFIG_HOME
/// environment variable.
fn path_from_xdg() -> Result<PathBuf> { fn path_from_xdg() -> Result<PathBuf> {
let path = let path = env::var("XDG_CONFIG_HOME")?;
env::var("XDG_CONFIG_HOME").context("cannot find \"XDG_CONFIG_HOME\" env var")?;
let path = PathBuf::from(path).join("himalaya").join("config.toml"); let path = PathBuf::from(path).join("himalaya").join("config.toml");
Ok(path) Ok(path)
} }
/// Tries to get the XDG config file path from HOME environment variable. /// Tries to get the XDG config file path from HOME environment
/// variable.
fn path_from_xdg_alt() -> Result<PathBuf> { fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") { let home_var = if cfg!(target_family = "windows") {
"USERPROFILE" "USERPROFILE"
} else { } else {
"HOME" "HOME"
}; };
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?; let path = env::var(home_var)?;
let path = PathBuf::from(path) let path = PathBuf::from(path)
.join(".config") .join(".config")
.join("himalaya") .join("himalaya")
@ -75,14 +90,15 @@ impl DeserializedConfig {
Ok(path) Ok(path)
} }
/// Tries to get the .himalayarc config file path from HOME environment variable. /// Tries to get the .himalayarc config file path from HOME
/// environment variable.
fn path_from_home() -> Result<PathBuf> { fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") { let home_var = if cfg!(target_family = "windows") {
"USERPROFILE" "USERPROFILE"
} else { } else {
"HOME" "HOME"
}; };
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?; let path = env::var(home_var)?;
let path = PathBuf::from(path).join(".himalayarc"); let path = PathBuf::from(path).join(".himalayarc");
Ok(path) Ok(path)
} }
@ -92,6 +108,5 @@ impl DeserializedConfig {
Self::path_from_xdg() Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt()) .or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home()) .or_else(|_| Self::path_from_home())
.context("cannot find config path")
} }
} }

16
lib/src/account/mod.rs Normal file
View file

@ -0,0 +1,16 @@
pub mod deserialized_config;
pub use deserialized_config::*;
pub mod deserialized_account_config;
pub use deserialized_account_config::*;
// pub mod account_handlers;
pub mod account_config;
pub use account_config::*;
pub mod format;
pub use format::*;
pub mod hooks;
pub use hooks::*;

View file

@ -1,8 +1,2 @@
#[cfg(test)] pub mod account;
mod tests { mod process;
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}

25
lib/src/process.rs Normal file
View file

@ -0,0 +1,25 @@
use log::debug;
use std::{io, process::Command, result, string};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ProcessError {
#[error("cannot run command")]
RunCmdError(#[from] io::Error),
#[error("cannot parse command output")]
ParseCmdOutputError(#[from] string::FromUtf8Error),
}
type Result<T> = result::Result<T, ProcessError>;
pub fn run_cmd(cmd: &str) -> Result<String> {
debug!("running command: {}", cmd);
let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(&["/C", cmd]).output()
} else {
Command::new("sh").arg("-c").arg(cmd).output()
}?;
Ok(String::from_utf8(output.stdout)?)
}

View file

@ -1,11 +1,13 @@
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
use std::{collections::HashMap, env, fs, iter::FromIterator}; use std::{collections::HashMap, env, fs, iter::FromIterator};
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
use himalaya::{ use himalaya::{
backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes}, backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes},
config::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig},
}; };
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
#[test] #[test]