set up multi account

This commit is contained in:
Clément DOUIN 2021-01-16 22:58:28 +01:00
parent 60af11bd47
commit ff724cb5be
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
6 changed files with 117 additions and 55 deletions

View file

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Merge `Email` with `Msg` [#21] - Merge `Email` with `Msg` [#21]
- List command with pagination [#19] - List command with pagination [#19]
- Icon in table when attachment is present [#16] - Icon in table when attachment is present [#16]
- Multi-account [#17]
[unreleased]: https://github.com/soywod/himalaya [unreleased]: https://github.com/soywod/himalaya
@ -41,5 +42,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#14]: https://github.com/soywod/himalaya/issues/14 [#14]: https://github.com/soywod/himalaya/issues/14
[#15]: https://github.com/soywod/himalaya/issues/15 [#15]: https://github.com/soywod/himalaya/issues/15
[#16]: https://github.com/soywod/himalaya/issues/16 [#16]: https://github.com/soywod/himalaya/issues/16
[#17]: https://github.com/soywod/himalaya/issues/17
[#19]: https://github.com/soywod/himalaya/issues/19 [#19]: https://github.com/soywod/himalaya/issues/19
[#21]: https://github.com/soywod/himalaya/issues/21 [#21]: https://github.com/soywod/himalaya/issues/21

View file

@ -1,6 +1,7 @@
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
collections::HashMap,
env, fmt, env, fmt,
fs::File, fs::File,
io::{self, Read}, io::{self, Read},
@ -15,8 +16,11 @@ use toml;
pub enum Error { pub enum Error {
IoError(io::Error), IoError(io::Error),
ParseTomlError(toml::de::Error), ParseTomlError(toml::de::Error),
ParseTomlAccountsError,
GetEnvVarError(env::VarError), GetEnvVarError(env::VarError),
GetPathNotFoundError, GetPathNotFoundError,
GetAccountNotFoundError(String),
GetAccountDefaultNotFoundError,
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -25,8 +29,11 @@ impl fmt::Display for Error {
match self { match self {
Error::IoError(err) => err.fmt(f), Error::IoError(err) => err.fmt(f),
Error::ParseTomlError(err) => err.fmt(f), Error::ParseTomlError(err) => err.fmt(f),
Error::ParseTomlAccountsError => write!(f, "no account found"),
Error::GetEnvVarError(err) => err.fmt(f), Error::GetEnvVarError(err) => err.fmt(f),
Error::GetPathNotFoundError => write!(f, "path not found"), Error::GetPathNotFoundError => write!(f, "path not found"),
Error::GetAccountNotFoundError(account) => write!(f, "account {} not found", account),
Error::GetAccountDefaultNotFoundError => write!(f, "no default account found"),
} }
} }
} }
@ -53,33 +60,48 @@ impl From<env::VarError> for Error {
type Result<T> = result::Result<T, Error>; type Result<T> = result::Result<T, Error>;
// Config // Account
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ServerInfo { pub struct Account {
pub host: String, // Override
pub port: u16, pub name: Option<String>,
pub login: String, pub downloads_dir: Option<PathBuf>,
pub password: String,
// Specific
pub default: Option<bool>,
pub email: String,
pub imap_host: String,
pub imap_port: u16,
pub imap_login: String,
pub imap_password: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_login: String,
pub smtp_password: String,
} }
impl ServerInfo { impl Account {
pub fn get_addr(&self) -> (&str, u16) { pub fn imap_addr(&self) -> (&str, u16) {
(&self.host, self.port) (&self.imap_host, self.imap_port)
} }
pub fn to_smtp_creds(&self) -> Credentials { pub fn smtp_creds(&self) -> SmtpCredentials {
Credentials::new(self.login.to_owned(), self.password.to_owned()) SmtpCredentials::new(self.smtp_login.to_owned(), self.smtp_password.to_owned())
} }
} }
// Config
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub name: String, pub name: String,
pub email: String,
pub downloads_dir: Option<PathBuf>, pub downloads_dir: Option<PathBuf>,
pub imap: ServerInfo,
pub smtp: ServerInfo, #[serde(flatten)]
pub accounts: HashMap<String, Account>,
} }
impl Config { impl Config {
@ -124,14 +146,35 @@ impl Config {
Ok(toml::from_slice(&content)?) Ok(toml::from_slice(&content)?)
} }
pub fn email_full(&self) -> String { pub fn get_account(&self, name: Option<&str>) -> Result<&Account> {
format!("{} <{}>", self.name, self.email) match name {
Some(name) => self
.accounts
.get(name)
.ok_or_else(|| Error::GetAccountNotFoundError(name.to_owned())),
None => self
.accounts
.iter()
.find(|(_, account)| account.default.unwrap_or(false))
.map(|(_, account)| account)
.ok_or_else(|| Error::GetAccountDefaultNotFoundError),
}
} }
pub fn downloads_filepath(&self, filename: &str) -> PathBuf { pub fn downloads_filepath(&self, account: &Account, filename: &str) -> PathBuf {
let temp_dir = env::temp_dir(); let temp_dir = env::temp_dir();
let mut full_path = self.downloads_dir.as_ref().unwrap_or(&temp_dir).to_owned(); let mut full_path = account
.downloads_dir
.as_ref()
.unwrap_or(self.downloads_dir.as_ref().unwrap_or(&temp_dir))
.to_owned();
full_path.push(filename); full_path.push(filename);
full_path full_path
} }
pub fn address(&self, account: &Account) -> String {
let name = account.name.as_ref().unwrap_or(&self.name);
format!("{} <{}>", name, account.email)
}
} }

View file

@ -2,7 +2,7 @@ use imap;
use native_tls::{self, TlsConnector, TlsStream}; use native_tls::{self, TlsConnector, TlsStream};
use std::{fmt, net::TcpStream, result}; use std::{fmt, net::TcpStream, result};
use crate::config; use crate::config::Account;
use crate::mbox::Mbox; use crate::mbox::Mbox;
use crate::msg::Msg; use crate::msg::Msg;
@ -64,19 +64,19 @@ type Result<T> = result::Result<T, Error>;
#[derive(Debug)] #[derive(Debug)]
pub struct ImapConnector<'a> { pub struct ImapConnector<'a> {
pub config: &'a config::ServerInfo, pub account: &'a Account,
pub sess: imap::Session<TlsStream<TcpStream>>, pub sess: imap::Session<TlsStream<TcpStream>>,
} }
impl<'a> ImapConnector<'a> { impl<'a> ImapConnector<'a> {
pub fn new(config: &'a config::ServerInfo) -> Result<Self> { pub fn new(account: &'a Account) -> Result<Self> {
let tls = TlsConnector::new()?; let tls = TlsConnector::new()?;
let client = imap::connect(config.get_addr(), &config.host, &tls)?; let client = imap::connect(account.imap_addr(), &account.imap_host, &tls)?;
let sess = client let sess = client
.login(&config.login, &config.password) .login(&account.imap_login, &account.imap_password)
.map_err(|res| res.0)?; .map_err(|res| res.0)?;
Ok(Self { config, sess }) Ok(Self { account, sess })
} }
pub fn close(&mut self) { pub fn close(&mut self) {

View file

@ -117,6 +117,13 @@ fn run() -> Result<()> {
.about("📫 Minimalist CLI email client") .about("📫 Minimalist CLI email client")
.author("soywod <clement.douin@posteo.net>") .author("soywod <clement.douin@posteo.net>")
.setting(AppSettings::ArgRequiredElseHelp) .setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::with_name("account")
.long("account")
.short("a")
.help("Name of the config file to use")
.value_name("STRING"),
)
.subcommand( .subcommand(
SubCommand::with_name("mailboxes") SubCommand::with_name("mailboxes")
.aliases(&["mboxes", "mb", "m"]) .aliases(&["mboxes", "mb", "m"])
@ -191,9 +198,12 @@ fn run() -> Result<()> {
) )
.get_matches(); .get_matches();
let account_name = matches.value_of("account");
if let Some(_) = matches.subcommand_matches("mailboxes") { if let Some(_) = matches.subcommand_matches("mailboxes") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mboxes = imap_conn.list_mboxes()?; let mboxes = imap_conn.list_mboxes()?;
println!("{}", mboxes.to_table()); println!("{}", mboxes.to_table());
@ -203,7 +213,8 @@ fn run() -> Result<()> {
if let Some(matches) = matches.subcommand_matches("list") { if let Some(matches) = matches.subcommand_matches("list") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let page_size: u32 = matches let page_size: u32 = matches
@ -225,7 +236,8 @@ fn run() -> Result<()> {
if let Some(matches) = matches.subcommand_matches("search") { if let Some(matches) = matches.subcommand_matches("search") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let page_size: usize = matches let page_size: usize = matches
@ -271,7 +283,8 @@ fn run() -> Result<()> {
if let Some(matches) = matches.subcommand_matches("read") { if let Some(matches) = matches.subcommand_matches("read") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
@ -286,7 +299,8 @@ fn run() -> Result<()> {
if let Some(matches) = matches.subcommand_matches("attachments") { if let Some(matches) = matches.subcommand_matches("attachments") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
@ -299,7 +313,7 @@ fn run() -> Result<()> {
} else { } else {
println!("{} attachment(s) found for message {}", parts.len(), uid); println!("{} attachment(s) found for message {}", parts.len(), uid);
parts.iter().for_each(|(filename, bytes)| { parts.iter().for_each(|(filename, bytes)| {
let filepath = config.downloads_filepath(&filename); let filepath = config.downloads_filepath(&account, &filename);
println!("Downloading {}", filename); println!("Downloading {}", filename);
fs::write(filepath, bytes).unwrap() fs::write(filepath, bytes).unwrap()
}); });
@ -311,16 +325,17 @@ fn run() -> Result<()> {
if let Some(_) = matches.subcommand_matches("write") { if let Some(_) = matches.subcommand_matches("write") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let tpl = Msg::build_new_tpl(&config)?; let tpl = Msg::build_new_tpl(&config, &account)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content); let msg = Msg::from(content);
input::ask_for_confirmation("Send the message?")?; input::ask_for_confirmation("Send the message?")?;
println!("Sending …"); println!("Sending …");
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; smtp::send(&account, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!"); println!("Done!");
@ -329,16 +344,17 @@ fn run() -> Result<()> {
if let Some(matches) = matches.subcommand_matches("reply") { if let Some(matches) = matches.subcommand_matches("reply") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let tpl = if matches.is_present("reply-all") { let tpl = if matches.is_present("reply-all") {
msg.build_reply_all_tpl(&config)? msg.build_reply_all_tpl(&config, &account)?
} else { } else {
msg.build_reply_tpl(&config)? msg.build_reply_tpl(&config, &account)?
}; };
let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
@ -347,7 +363,7 @@ fn run() -> Result<()> {
input::ask_for_confirmation("Send the message?")?; input::ask_for_confirmation("Send the message?")?;
println!("Sending …"); println!("Sending …");
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; smtp::send(&account, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!"); println!("Done!");
@ -356,20 +372,21 @@ fn run() -> Result<()> {
if let Some(matches) = matches.subcommand_matches("forward") { if let Some(matches) = matches.subcommand_matches("forward") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let tpl = msg.build_forward_tpl(&config)?; let tpl = msg.build_forward_tpl(&config, &account)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content); let msg = Msg::from(content);
input::ask_for_confirmation("Send the message?")?; input::ask_for_confirmation("Send the message?")?;
println!("Sending …"); println!("Sending …");
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; smtp::send(&account, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!"); println!("Done!");

View file

@ -2,8 +2,8 @@ use lettre;
use mailparse::{self, MailHeaderMap}; use mailparse::{self, MailHeaderMap};
use std::{fmt, result}; use std::{fmt, result};
use crate::config::{Account, Config};
use crate::table::{self, DisplayRow, DisplayTable}; use crate::table::{self, DisplayRow, DisplayTable};
use crate::Config;
// Error wrapper // Error wrapper
@ -208,11 +208,11 @@ impl<'a> Msg {
Ok(parts) Ok(parts)
} }
pub fn build_new_tpl(config: &Config) -> Result<String> { pub fn build_new_tpl(config: &Config, account: &Account) -> Result<String> {
let mut tpl = vec![]; let mut tpl = vec![];
// "From" header // "From" header
tpl.push(format!("From: {}", config.email_full())); tpl.push(format!("From: {}", config.address(account)));
// "To" header // "To" header
tpl.push("To: ".to_string()); tpl.push("To: ".to_string());
@ -223,13 +223,13 @@ impl<'a> Msg {
Ok(tpl.join("\r\n")) Ok(tpl.join("\r\n"))
} }
pub fn build_reply_tpl(&self, config: &Config) -> Result<String> { pub fn build_reply_tpl(&self, config: &Config, account: &Account) -> Result<String> {
let msg = &self.parse()?; let msg = &self.parse()?;
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
// "From" header // "From" header
tpl.push(format!("From: {}", config.email_full())); tpl.push(format!("From: {}", config.address(account)));
// "In-Reply-To" header // "In-Reply-To" header
if let Some(msg_id) = headers.get_first_value("message-id") { if let Some(msg_id) = headers.get_first_value("message-id") {
@ -263,13 +263,13 @@ impl<'a> Msg {
Ok(tpl.join("\r\n")) Ok(tpl.join("\r\n"))
} }
pub fn build_reply_all_tpl(&self, config: &Config) -> Result<String> { pub fn build_reply_all_tpl(&self, config: &Config, account: &Account) -> Result<String> {
let msg = &self.parse()?; let msg = &self.parse()?;
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
// "From" header // "From" header
tpl.push(format!("From: {}", config.email_full())); tpl.push(format!("From: {}", config.address(account)));
// "In-Reply-To" header // "In-Reply-To" header
if let Some(msg_id) = headers.get_first_value("message-id") { if let Some(msg_id) = headers.get_first_value("message-id") {
@ -278,7 +278,7 @@ impl<'a> Msg {
// "To" header // "To" header
// All addresses coming from original "To" … // All addresses coming from original "To" …
let email: lettre::Address = config.email.parse().unwrap(); let email: lettre::Address = account.email.parse().unwrap();
let to = headers let to = headers
.get_all_values("to") .get_all_values("to")
.iter() .iter()
@ -345,13 +345,13 @@ impl<'a> Msg {
Ok(tpl.join("\r\n")) Ok(tpl.join("\r\n"))
} }
pub fn build_forward_tpl(&self, config: &Config) -> Result<String> { pub fn build_forward_tpl(&self, config: &Config, account: &Account) -> Result<String> {
let msg = &self.parse()?; let msg = &self.parse()?;
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
// "From" header // "From" header
tpl.push(format!("From: {}", config.email_full())); tpl.push(format!("From: {}", config.address(account)));
// "To" header // "To" header
tpl.push("To: ".to_string()); tpl.push("To: ".to_string());

View file

@ -1,7 +1,7 @@
use lettre; use lettre;
use std::{fmt, result}; use std::{fmt, result};
use crate::config; use crate::config::Account;
// Error wrapper // Error wrapper
@ -31,11 +31,11 @@ type Result<T> = result::Result<T, Error>;
// Utils // Utils
pub fn send(config: &config::ServerInfo, msg: &lettre::Message) -> Result<()> { pub fn send(account: &Account, msg: &lettre::Message) -> Result<()> {
use lettre::Transport; use lettre::Transport;
lettre::transport::smtp::SmtpTransport::relay(&config.host)? lettre::transport::smtp::SmtpTransport::relay(&account.smtp_host)?
.credentials(config.to_smtp_creds()) .credentials(account.smtp_creds())
.build() .build()
.send(msg) .send(msg)
.map(|_| Ok(()))? .map(|_| Ok(()))?