diff --git a/Cargo.lock b/Cargo.lock index 7ccbc8e..adda362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,6 +486,10 @@ dependencies = [ name = "himalaya-lib" version = "0.1.0" dependencies = [ + "ammonia", + "chrono", + "convert_case", + "html-escape", "imap", "imap-proto", "lettre", @@ -493,11 +497,16 @@ dependencies = [ "maildir", "mailparse", "md5", + "native-tls", "notmuch", + "regex", + "rfc2047-decoder", "serde", "shellexpand", "thiserror", "toml", + "tree_magic", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2f84d20..de6b2bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["lib", "cli"] \ No newline at end of file +members = ["lib", "cli"] diff --git a/cli/src/backends/maildir/maildir_flag.rs b/cli/src/backends/maildir/maildir_flag.rs deleted file mode 100644 index b868e8a..0000000 --- a/cli/src/backends/maildir/maildir_flag.rs +++ /dev/null @@ -1,13 +0,0 @@ -use himalaya_lib::msg::Flag; - -pub fn from_char(c: char) -> Flag { - match c { - 'R' => Flag::Answered, - 'S' => Flag::Seen, - 'T' => Flag::Deleted, - 'D' => Flag::Draft, - 'F' => Flag::Flagged, - 'P' => Flag::Custom(String::from("Passed")), - flag => Flag::Custom(flag.to_string()), - } -} diff --git a/cli/src/backends/maildir/maildir_flags.rs b/cli/src/backends/maildir/maildir_flags.rs deleted file mode 100644 index 538c90a..0000000 --- a/cli/src/backends/maildir/maildir_flags.rs +++ /dev/null @@ -1,7 +0,0 @@ -use himalaya_lib::msg::Flags; - -use super::maildir_flag; - -pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags { - entry.flags().chars().map(maildir_flag::from_char).collect() -} diff --git a/cli/src/backends/imap/imap_args.rs b/cli/src/imap/imap_args.rs similarity index 100% rename from cli/src/backends/imap/imap_args.rs rename to cli/src/imap/imap_args.rs diff --git a/cli/src/backends/imap/imap_envelopes.rs b/cli/src/imap/imap_envelopes.rs similarity index 82% rename from cli/src/backends/imap/imap_envelopes.rs rename to cli/src/imap/imap_envelopes.rs index d0e6591..8095b9b 100644 --- a/cli/src/backends/imap/imap_envelopes.rs +++ b/cli/src/imap/imap_envelopes.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; -use himalaya_lib::msg::Envelopes; - -use crate::backends::{imap::from_imap_fetch, ImapFetch}; +use himalaya_lib::{ + backend::{from_imap_fetch, ImapFetch}, + msg::Envelopes, +}; /// Represents the list of raw envelopes returned by the `imap` crate. pub type ImapFetches = imap::types::ZeroCopy>; diff --git a/cli/src/backends/imap/imap_handlers.rs b/cli/src/imap/imap_handlers.rs similarity index 58% rename from cli/src/backends/imap/imap_handlers.rs rename to cli/src/imap/imap_handlers.rs index 3805909..f5ab439 100644 --- a/cli/src/backends/imap/imap_handlers.rs +++ b/cli/src/imap/imap_handlers.rs @@ -2,14 +2,13 @@ //! //! This module gathers all IMAP handlers triggered by the CLI. -use anyhow::Result; - -use crate::backends::ImapBackend; +use anyhow::{Context, Result}; +use himalaya_lib::backend::ImapBackend; pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { - imap.notify(keepalive, mbox) + imap.notify(keepalive, mbox).context("cannot imap notify") } pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { - imap.watch(keepalive, mbox) + imap.watch(keepalive, mbox).context("cannot imap watch") } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 634a52f..9fefb19 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,6 +9,15 @@ pub mod mbox { pub mod mbox_handlers; } +#[cfg(feature = "imap-backend")] +pub mod imap { + pub mod imap_args; + pub mod imap_handlers; + + pub mod imap_envelopes; + pub use imap_envelopes::*; +} + pub mod msg { pub mod envelope; pub use envelope::*; @@ -19,95 +28,13 @@ pub mod msg { pub mod msg_args; pub mod msg_handlers; - pub mod msg_utils; pub mod flag_args; pub mod flag_handlers; pub mod tpl_args; - pub use tpl_args::TplOverride; pub mod tpl_handlers; - - pub mod msg_entity; - pub use msg_entity::*; - - pub mod parts_entity; - pub use parts_entity::*; - - pub mod addr_entity; - pub use addr_entity::*; -} - -pub mod backends { - pub mod backend; - pub use backend::*; - - pub mod id_mapper; - pub use id_mapper::*; - - #[cfg(feature = "imap-backend")] - pub mod imap { - pub mod imap_args; - - pub mod imap_backend; - pub use imap_backend::*; - - pub mod imap_handlers; - - pub mod imap_envelopes; - pub use imap_envelopes::*; - - pub mod imap_envelope; - pub use imap_envelope::*; - - pub mod imap_flags; - pub use imap_flags::*; - - pub mod imap_flag; - pub use imap_flag::*; - - pub mod msg_sort_criterion; - } - - #[cfg(feature = "imap-backend")] - pub use self::imap::*; - - #[cfg(feature = "maildir-backend")] - pub mod maildir { - pub mod maildir_backend; - pub use maildir_backend::*; - - pub mod maildir_envelopes; - pub use maildir_envelopes::*; - - pub mod maildir_envelope; - pub use maildir_envelope::*; - - pub mod maildir_flags; - pub use maildir_flags::*; - - pub mod maildir_flag; - pub use maildir_flag::*; - } - - #[cfg(feature = "maildir-backend")] - pub use self::maildir::*; - - #[cfg(feature = "notmuch-backend")] - pub mod notmuch { - pub mod notmuch_backend; - pub use notmuch_backend::*; - - pub mod notmuch_envelopes; - pub use notmuch_envelopes::*; - - pub mod notmuch_envelope; - pub use notmuch_envelope::*; - } - - #[cfg(feature = "notmuch-backend")] - pub use self::notmuch::*; } pub mod smtp { diff --git a/cli/src/main.rs b/cli/src/main.rs index 10c7382..b3f834c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,12 +1,12 @@ -use anyhow::Result; -use himalaya_lib::account::{ - AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER, +use anyhow::{Context, Result}; +use himalaya_lib::{ + account::{AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER}, + backend::Backend, }; use std::{convert::TryFrom, env}; use url::Url; use himalaya::{ - backends::Backend, compl::{compl_args, compl_handlers}, config::{account_args, account_handlers, config_args}, mbox::{mbox_args, mbox_handlers}, @@ -16,15 +16,16 @@ use himalaya::{ }; #[cfg(feature = "imap-backend")] -use himalaya::backends::{imap_args, imap_handlers, ImapBackend}; +use himalaya::imap::{imap_args, imap_handlers}; + +#[cfg(feature = "imap-backend")] +use himalaya_lib::backend::ImapBackend; #[cfg(feature = "maildir-backend")] -use himalaya::backends::MaildirBackend; +use himalaya_lib::backend::MaildirBackend; #[cfg(feature = "notmuch-backend")] -use himalaya::backends::NotmuchBackend; -#[cfg(feature = "notmuch-backend")] -use himalaya_lib::account::MaildirBackendConfig; +use himalaya_lib::{account::MaildirBackendConfig, backend::NotmuchBackend}; fn create_app<'a>() -> clap::App<'a, 'a> { let app = clap::App::new(env!("CARGO_PKG_NAME")) @@ -346,5 +347,5 @@ fn main() -> Result<()> { _ => (), } - backend.disconnect() + backend.disconnect().context("cannot disconnect") } diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index aed639f..c4d50ac 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -3,13 +3,10 @@ //! This module gathers all mailbox actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::account::AccountConfig; +use himalaya_lib::{account::AccountConfig, backend::Backend}; use log::{info, trace}; -use crate::{ - backends::Backend, - output::{PrintTableOpts, PrinterService}, -}; +use crate::output::{PrintTableOpts, PrinterService}; /// Lists all mailboxes. pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( @@ -34,16 +31,14 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( #[cfg(test)] mod tests { use himalaya_lib::{ + backend::{backend, Backend}, mbox::{Mbox, Mboxes}, - msg::Envelopes, + msg::{Envelopes, Msg}, }; use std::{fmt::Debug, io}; use termcolor::ColorSpec; - use crate::{ - msg::Msg, - output::{Print, PrintTable, WriteColor}, - }; + use crate::output::{Print, PrintTable, WriteColor}; use super::*; @@ -93,17 +88,17 @@ mod tests { &mut self, data: Box, opts: PrintTableOpts, - ) -> Result<()> { + ) -> anyhow::Result<()> { data.print_table(&mut self.writer, opts)?; Ok(()) } - fn print_str(&mut self, _data: T) -> Result<()> { + fn print_str(&mut self, _data: T) -> anyhow::Result<()> { unimplemented!() } fn print_struct( &mut self, _data: T, - ) -> Result<()> { + ) -> anyhow::Result<()> { unimplemented!() } fn is_json(&self) -> bool { @@ -114,10 +109,10 @@ mod tests { struct TestBackend; impl<'a> Backend<'a> for TestBackend { - fn add_mbox(&mut self, _: &str) -> Result<()> { + fn add_mbox(&mut self, _: &str) -> backend::Result<()> { unimplemented!(); } - fn get_mboxes(&mut self) -> Result { + fn get_mboxes(&mut self) -> backend::Result { Ok(Mboxes { mboxes: vec![ Mbox { @@ -133,10 +128,10 @@ mod tests { ], }) } - fn del_mbox(&mut self, _: &str) -> Result<()> { + fn del_mbox(&mut self, _: &str) -> backend::Result<()> { unimplemented!(); } - fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result { + fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> backend::Result { unimplemented!() } fn search_envelopes( @@ -146,31 +141,31 @@ mod tests { _: &str, _: usize, _: usize, - ) -> Result { + ) -> backend::Result { unimplemented!() } - fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result { + fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result { unimplemented!() } - fn get_msg(&mut self, _: &str, _: &str) -> Result { + fn get_msg(&mut self, _: &str, _: &str) -> backend::Result { unimplemented!() } - fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn move_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn move_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn del_msg(&mut self, _: &str, _: &str) -> Result<()> { + fn del_msg(&mut self, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn add_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn add_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn set_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn set_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn del_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn del_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } } diff --git a/cli/src/msg/flag_handlers.rs b/cli/src/msg/flag_handlers.rs index 33ed696..686912e 100644 --- a/cli/src/msg/flag_handlers.rs +++ b/cli/src/msg/flag_handlers.rs @@ -3,8 +3,9 @@ //! This module gathers all flag actions triggered by the CLI. use anyhow::Result; +use himalaya_lib::backend::Backend; -use crate::{backends::Backend, output::PrinterService}; +use crate::output::PrinterService; /// Adds flags to all messages matching the given sequence range. /// Flags are case-insensitive, and they do not need to be prefixed with `\`. diff --git a/cli/src/msg/msg_args.rs b/cli/src/msg/msg_args.rs index 169b05a..b5b7862 100644 --- a/cli/src/msg/msg_args.rs +++ b/cli/src/msg/msg_args.rs @@ -4,11 +4,15 @@ use anyhow::Result; use clap::{self, App, Arg, ArgMatches, SubCommand}; +use himalaya_lib::msg::TplOverride; use log::{debug, info, trace}; use crate::{ mbox::mbox_args, - msg::{flag_args, msg_args, tpl_args}, + msg::{ + flag_args, msg_args, + tpl_args::{self, from_args}, + }, ui::table_arg, }; @@ -42,7 +46,7 @@ pub enum Cmd<'a> { Search(Query, MaxTableWidth, Option, Page), Sort(Criteria, Query, MaxTableWidth, Option, Page), Send(RawMsg<'a>), - Write(tpl_args::TplOverride<'a>, AttachmentPaths<'a>, Encrypt), + Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt), Flag(Option>), Tpl(Option>), @@ -261,7 +265,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("attachments paths: {:?}", attachment_paths); let encrypt = m.is_present("encrypt"); debug!("encrypt: {}", encrypt); - let tpl = tpl_args::TplOverride::from(m); + let tpl = from_args(m); return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt))); } diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index 3011427..b18c2c4 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -4,7 +4,11 @@ use anyhow::{Context, Result}; use atty::Stream; -use himalaya_lib::account::{AccountConfig, DEFAULT_SENT_FOLDER}; +use himalaya_lib::{ + account::{AccountConfig, DEFAULT_SENT_FOLDER}, + backend::Backend, + msg::{Msg, Part, Parts, TextPlainPart, TplOverride}, +}; use log::{debug, info, trace}; use mailparse::addrparse; use std::{ @@ -15,14 +19,11 @@ use std::{ use url::Url; use crate::{ - backends::Backend, - msg::{Msg, Part, Parts, TextPlainPart}, output::{PrintTableOpts, PrinterService}, smtp::SmtpService, + ui::editor, }; -use super::tpl_args; - /// Downloads all message attachments to the user account downloads directory. pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, @@ -96,18 +97,12 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - backend + let msg = backend .get_msg(mbox, seq)? .into_forward(config)? .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor( - tpl_args::TplOverride::default(), - config, - printer, - backend, - smtp, - )?; + .encrypt(encrypt); + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; Ok(()) } @@ -191,13 +186,7 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( }; trace!("message: {:?}", msg); - msg.edit_with_editor( - tpl_args::TplOverride::default(), - config, - printer, - backend, - smtp, - )?; + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; Ok(()) } @@ -249,19 +238,14 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - backend + let msg = backend .get_msg(mbox, seq)? .into_reply(all, config)? .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor( - tpl_args::TplOverride::default(), - config, - printer, - backend, - smtp, - )? - .add_flags(mbox, seq, "replied") + .encrypt(encrypt); + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)? + .add_flags(mbox, seq, "replied")?; + Ok(()) } /// Saves a raw message to the targetted mailbox. @@ -384,7 +368,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( /// Compose a new message. pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( - tpl: tpl_args::TplOverride, + tpl: TplOverride, attachments_paths: Vec<&str>, encrypt: bool, config: &AccountConfig, @@ -392,9 +376,9 @@ pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - Msg::default() + let msg = Msg::default() .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor(tpl, config, printer, backend, smtp)?; + .encrypt(encrypt); + editor::edit_msg_with_editor(msg, tpl, config, printer, backend, smtp)?; Ok(()) } diff --git a/cli/src/msg/msg_utils.rs b/cli/src/msg/msg_utils.rs deleted file mode 100644 index b5ccacf..0000000 --- a/cli/src/msg/msg_utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -use anyhow::{Context, Result}; -use log::{debug, trace}; -use std::{env, fs, path::PathBuf}; - -pub fn local_draft_path() -> PathBuf { - let path = env::temp_dir().join("himalaya-draft.eml"); - trace!("local draft path: {:?}", path); - path -} - -pub fn remove_local_draft() -> Result<()> { - let path = local_draft_path(); - debug!("remove draft path at {:?}", path); - fs::remove_file(&path).context(format!("cannot remove local draft at {:?}", path)) -} diff --git a/cli/src/msg/tpl_args.rs b/cli/src/msg/tpl_args.rs index 90a9356..5436ad0 100644 --- a/cli/src/msg/tpl_args.rs +++ b/cli/src/msg/tpl_args.rs @@ -4,6 +4,7 @@ use anyhow::Result; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; +use himalaya_lib::msg::TplOverride; use log::{debug, info, trace}; use crate::msg::msg_args; @@ -13,30 +14,16 @@ type ReplyAll = bool; type AttachmentPaths<'a> = Vec<&'a str>; type Tpl<'a> = &'a str; -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct TplOverride<'a> { - pub subject: Option<&'a str>, - pub from: Option>, - pub to: Option>, - pub cc: Option>, - pub bcc: Option>, - pub headers: Option>, - pub body: Option<&'a str>, - pub sig: Option<&'a str>, -} - -impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> { - fn from(matches: &'a ArgMatches<'a>) -> Self { - Self { - subject: matches.value_of("subject"), - from: matches.values_of("from").map(|v| v.collect()), - to: matches.values_of("to").map(|v| v.collect()), - cc: matches.values_of("cc").map(|v| v.collect()), - bcc: matches.values_of("bcc").map(|v| v.collect()), - headers: matches.values_of("headers").map(|v| v.collect()), - body: matches.value_of("body"), - sig: matches.value_of("signature"), - } +pub fn from_args<'a>(matches: &'a ArgMatches<'a>) -> TplOverride { + TplOverride { + subject: matches.value_of("subject"), + from: matches.values_of("from").map(|v| v.collect()), + to: matches.values_of("to").map(|v| v.collect()), + cc: matches.values_of("cc").map(|v| v.collect()), + bcc: matches.values_of("bcc").map(|v| v.collect()), + headers: matches.values_of("headers").map(|v| v.collect()), + body: matches.value_of("body"), + sig: matches.value_of("signature"), } } @@ -56,7 +43,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { if let Some(m) = m.subcommand_matches("new") { info!("new subcommand matched"); - let tpl = TplOverride::from(m); + let tpl = from_args(m); trace!("template override: {:?}", tpl); return Ok(Some(Cmd::New(tpl))); } @@ -67,7 +54,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("sequence: {}", seq); let all = m.is_present("reply-all"); debug!("reply all: {}", all); - let tpl = TplOverride::from(m); + let tpl = from_args(m); trace!("template override: {:?}", tpl); return Ok(Some(Cmd::Reply(seq, all, tpl))); } @@ -76,7 +63,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { info!("forward subcommand matched"); let seq = m.value_of("seq").unwrap(); debug!("sequence: {}", seq); - let tpl = TplOverride::from(m); + let tpl = from_args(m); trace!("template args: {:?}", tpl); return Ok(Some(Cmd::Forward(seq, tpl))); } diff --git a/cli/src/msg/tpl_handlers.rs b/cli/src/msg/tpl_handlers.rs index 16953b1..8d8ce14 100644 --- a/cli/src/msg/tpl_handlers.rs +++ b/cli/src/msg/tpl_handlers.rs @@ -4,15 +4,14 @@ use anyhow::Result; use atty::Stream; -use himalaya_lib::account::AccountConfig; +use himalaya_lib::{ + account::AccountConfig, + backend::Backend, + msg::{Msg, TplOverride}, +}; use std::io::{self, BufRead}; -use crate::{ - backends::Backend, - msg::{Msg, TplOverride}, - output::PrinterService, - smtp::SmtpService, -}; +use crate::{output::PrinterService, smtp::SmtpService}; /// Generate a new message template. pub fn new<'a, P: PrinterService>( diff --git a/cli/src/smtp/smtp_service.rs b/cli/src/smtp/smtp_service.rs index e417afb..c5c788d 100644 --- a/cli/src/smtp/smtp_service.rs +++ b/cli/src/smtp/smtp_service.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use himalaya_lib::account::AccountConfig; +use himalaya_lib::{account::AccountConfig, msg::Msg}; use lettre::{ self, transport::smtp::{ @@ -10,7 +10,7 @@ use lettre::{ }; use std::convert::TryInto; -use crate::{msg::Msg, output::pipe_cmd}; +use crate::output::pipe_cmd; pub trait SmtpService { fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result>; diff --git a/cli/src/ui/editor.rs b/cli/src/ui/editor.rs index 9e7a5c2..f940cef 100644 --- a/cli/src/ui/editor.rs +++ b/cli/src/ui/editor.rs @@ -1,11 +1,20 @@ use anyhow::{Context, Result}; -use log::debug; +use himalaya_lib::{ + account::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER}, + backend::Backend, + msg::{local_draft_path, remove_local_draft, Msg, TplOverride}, +}; +use log::{debug, info}; use std::{env, fs, process::Command}; -use crate::msg::msg_utils; +use crate::{ + output::PrinterService, + smtp::SmtpService, + ui::choice::{self, PostEditChoice, PreEditChoice}, +}; pub fn open_with_tpl(tpl: String) -> Result { - let path = msg_utils::local_draft_path(); + let path = local_draft_path(); debug!("create draft"); fs::write(&path, tpl.as_bytes()).context(format!("cannot write local draft at {:?}", path))?; @@ -24,8 +33,100 @@ pub fn open_with_tpl(tpl: String) -> Result { } pub fn open_with_draft() -> Result { - let path = msg_utils::local_draft_path(); + let path = local_draft_path(); let tpl = fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; open_with_tpl(tpl) } + +fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &AccountConfig) -> Result { + let tpl = msg.to_tpl(tpl, account)?; + let tpl = open_with_tpl(tpl)?; + Msg::from_tpl(&tpl).context("cannot parse message from template") +} + +pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( + mut msg: Msg, + tpl: TplOverride, + account: &AccountConfig, + printer: &mut P, + backend: Box<&'a mut B>, + smtp: &mut S, +) -> Result> { + info!("start editing with editor"); + + let draft = local_draft_path(); + if draft.exists() { + loop { + match choice::pre_edit() { + Ok(choice) => match choice { + PreEditChoice::Edit => { + let tpl = open_with_draft()?; + msg.merge_with(Msg::from_tpl(&tpl)?); + break; + } + PreEditChoice::Discard => { + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + break; + } + PreEditChoice::Quit => return Ok(backend), + }, + Err(err) => { + println!("{}", err); + continue; + } + } + } + } else { + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + } + + loop { + match choice::post_edit() { + Ok(PostEditChoice::Send) => { + printer.print_str("Sending message…")?; + let sent_msg = smtp.send(account, &msg)?; + let sent_folder = account + .mailboxes + .get("sent") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_SENT_FOLDER); + printer.print_str(format!("Adding message to the {:?} folder…", sent_folder))?; + backend.add_msg(&sent_folder, &sent_msg, "seen")?; + remove_local_draft()?; + printer.print_struct("Done!")?; + break; + } + Ok(PostEditChoice::Edit) => { + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + continue; + } + Ok(PostEditChoice::LocalDraft) => { + printer.print_struct("Message successfully saved locally")?; + break; + } + Ok(PostEditChoice::RemoteDraft) => { + let tpl = msg.to_tpl(TplOverride::default(), account)?; + let draft_folder = account + .mailboxes + .get("draft") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_DRAFT_FOLDER); + backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?; + remove_local_draft()?; + printer.print_struct(format!("Message successfully saved to {}", draft_folder))?; + break; + } + Ok(PostEditChoice::Discard) => { + remove_local_draft()?; + break; + } + Err(err) => { + println!("{}", err); + continue; + } + } + } + + Ok(backend) +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 68f0916..2549a13 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -10,13 +10,22 @@ notmuch-backend = ["notmuch", "maildir-backend"] default = ["imap-backend", "maildir-backend"] [dependencies] +ammonia = "3.1.2" +chrono = "0.4.19" +convert_case = "0.5.0" +html-escape = "0.2.9" lettre = { version = "0.10.0-rc.7", features = ["serde"] } log = "0.4.14" mailparse = "0.13.6" +native-tls = "0.2.8" +regex = "1.5.4" +rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } shellexpand = "2.1.0" thiserror = "1.0.31" toml = "0.5.8" +tree_magic = "0.2.3" +uuid = { version = "0.8", features = ["v4"] } # [optional] imap = { version = "=3.0.0-alpha.4", optional = true } diff --git a/lib/src/account/account_config.rs b/lib/src/account/account_config.rs index 5e7b951..7229bb9 100644 --- a/lib/src/account/account_config.rs +++ b/lib/src/account/account_config.rs @@ -5,12 +5,19 @@ use shellexpand; use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf, result}; use thiserror::Error; -use crate::process::{run_cmd, ProcessError}; +use crate::process; use super::*; #[derive(Error, Debug)] -pub enum AccountError { +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, + #[error("cannot find default account")] FindDefaultAccountError, #[error("cannot find account \"{0}\"")] @@ -25,11 +32,17 @@ pub enum AccountError { ParseDownloadFileNameError(PathBuf), #[error("cannot find password")] FindPasswordError, - #[error(transparent)] - RunCmdError(#[from] ProcessError), + #[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), } -type Result = result::Result; +pub type Result = result::Result; /// Represents the user account. #[derive(Debug, Default, Clone)] @@ -113,12 +126,12 @@ impl<'a> AccountConfig { } }) .map(|(name, account)| (name.to_owned(), account)) - .ok_or_else(|| AccountError::FindDefaultAccountError), + .ok_or_else(|| Error::FindDefaultAccountError), Some(name) => config .accounts .get(name) .map(|account| (name.to_owned(), account)) - .ok_or_else(|| AccountError::FindAccountError(name.to_owned())), + .ok_or_else(|| Error::FindAccountError(name.to_owned())), }?; let base_account = account.to_base(); @@ -253,17 +266,17 @@ impl<'a> AccountConfig { Ok(mailparse::addrparse(&addr)? .first() - .ok_or_else(|| AccountError::FindAccountAddressError(addr.into()))? + .ok_or_else(|| Error::FindAccountAddressError(addr.into()))? .clone()) } /// Builds the user account SMTP credentials. pub fn smtp_creds(&self) -> Result { - let passwd = run_cmd(&self.smtp_passwd_cmd)?; + let passwd = process::run_cmd(&self.smtp_passwd_cmd).map_err(Error::GetSmtpPasswdError)?; let passwd = passwd .lines() .next() - .ok_or_else(|| AccountError::FindPasswordError)?; + .ok_or_else(|| Error::FindPasswordError)?; Ok(SmtpCredentials::new( self.smtp_login.to_owned(), @@ -272,22 +285,22 @@ impl<'a> AccountConfig { } /// Encrypts a file. - pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result> { + pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result { if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() { let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path); - Ok(run_cmd(&encrypt_file_cmd).map(Some)?) + Ok(process::run_cmd(&encrypt_file_cmd).map_err(Error::RunEncryptFileCmdError)?) } else { - Ok(None) + Err(Error::FindPgpEncryptFileCmdError) } } /// Decrypts a file. - pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result> { + pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result { if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() { let decrypt_file_cmd = format!("{} {:?}", cmd, path); - Ok(run_cmd(&decrypt_file_cmd).map(Some)?) + Ok(process::run_cmd(&decrypt_file_cmd).map_err(Error::DecryptPgpFileError)?) } else { - Ok(None) + Err(Error::FindPgpDecryptFileCmdError) } } @@ -319,9 +332,7 @@ impl<'a> AccountConfig { .file_stem() .and_then(OsStr::to_str) .map(|fstem| format!("{}_{}{}", fstem, count, file_ext)) - .ok_or_else(|| { - AccountError::ParseDownloadFileNameError(file_path.to_owned()) - })?, + .ok_or_else(|| Error::ParseDownloadFileNameError(file_path.to_owned()))?, )); } @@ -340,8 +351,7 @@ impl<'a> AccountConfig { .map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender)) .unwrap_or(default_cmd); - debug!("run command: {}", cmd); - run_cmd(&cmd)?; + process::run_cmd(&cmd).map_err(Error::RunNotifyCmdError)?; Ok(()) } @@ -391,11 +401,11 @@ pub struct ImapBackendConfig { impl ImapBackendConfig { /// Gets the IMAP password of the user account. pub fn imap_passwd(&self) -> Result { - let passwd = run_cmd(&self.imap_passwd_cmd)?; + let passwd = process::run_cmd(&self.imap_passwd_cmd).map_err(Error::GetImapPasswdError)?; let passwd = passwd .lines() .next() - .ok_or_else(|| AccountError::FindPasswordError)?; + .ok_or_else(|| Error::FindPasswordError)?; Ok(passwd.to_string()) } } diff --git a/cli/src/backends/backend.rs b/lib/src/backend/backend.rs similarity index 61% rename from cli/src/backends/backend.rs rename to lib/src/backend/backend.rs index 0162d05..2030347 100644 --- a/cli/src/backends/backend.rs +++ b/lib/src/backend/backend.rs @@ -3,10 +3,48 @@ //! This module exposes the backend trait, which can be used to create //! custom backend implementations. -use anyhow::Result; -use himalaya_lib::{mbox::Mboxes, msg::Envelopes}; +use std::result; -use crate::msg::Msg; +use thiserror::Error; + +use crate::{ + account, + mbox::Mboxes, + msg::{self, Envelopes, Msg}, +}; + +use super::id_mapper; + +#[cfg(feature = "maildir-backend")] +use super::MaildirError; + +#[cfg(feature = "notmuch-backend")] +use super::NotmuchError; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + ImapError(#[from] super::imap::Error), + + #[error(transparent)] + AccountError(#[from] account::Error), + + #[error(transparent)] + MsgError(#[from] msg::Error), + + #[error(transparent)] + IdMapperError(#[from] id_mapper::Error), + + #[cfg(feature = "maildir-backend")] + #[error(transparent)] + MaildirError(#[from] MaildirError), + + #[cfg(feature = "notmuch-backend")] + #[error(transparent)] + NotmuchError(#[from] NotmuchError), +} + +pub type Result = result::Result; pub trait Backend<'a> { fn connect(&mut self) -> Result<()> { diff --git a/cli/src/backends/id_mapper.rs b/lib/src/backend/id_mapper.rs similarity index 56% rename from cli/src/backends/id_mapper.rs rename to lib/src/backend/id_mapper.rs index 09a5422..d5ab5df 100644 --- a/cli/src/backends/id_mapper.rs +++ b/lib/src/backend/id_mapper.rs @@ -1,43 +1,56 @@ -use anyhow::{anyhow, Context, Result}; use std::{ - collections::HashMap, - fs::OpenOptions, - io::{BufRead, BufReader, Write}, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, + collections, fs, + io::{self, prelude::*}, + ops, path, result, }; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("cannot parse id mapper cache line {0}")] + ParseLineError(String), + #[error("cannot find message id from short hash {0}")] + FindFromShortHashError(String), + #[error("the short hash {0} matches more than one hash: {1}")] + MatchShortHashError(String, String), + + #[error("cannot open id mapper file: {1}")] + OpenHashMapFileError(#[source] io::Error, path::PathBuf), + #[error("cannot write id mapper file: {1}")] + WriteHashMapFileError(#[source] io::Error, path::PathBuf), + #[error("cannot read line from id mapper file")] + ReadHashMapFileLineError(#[source] io::Error), +} + +type Result = result::Result; #[derive(Debug, Default)] pub struct IdMapper { - path: PathBuf, - map: HashMap, + path: path::PathBuf, + map: collections::HashMap, short_hash_len: usize, } impl IdMapper { - pub fn new(dir: &Path) -> Result { + pub fn new(dir: &path::Path) -> Result { let mut mapper = Self::default(); mapper.path = dir.join(".himalaya-id-map"); - let file = OpenOptions::new() + let file = fs::OpenOptions::new() .read(true) .write(true) .create(true) .open(&mapper.path) - .context("cannot open id hash map file")?; - let reader = BufReader::new(file); + .map_err(|err| Error::OpenHashMapFileError(err, mapper.path.to_owned()))?; + let reader = io::BufReader::new(file); for line in reader.lines() { - let line = - line.context("cannot read line from maildir envelopes id mapper cache file")?; + let line = line.map_err(Error::ReadHashMapFileLineError)?; if mapper.short_hash_len == 0 { mapper.short_hash_len = 2.max(line.parse().unwrap_or(2)); } else { - let (hash, id) = line.split_once(' ').ok_or_else(|| { - anyhow!( - "cannot parse line {:?} from maildir envelopes id mapper cache file", - line - ) - })?; + let (hash, id) = line + .split_once(' ') + .ok_or_else(|| Error::ParseLineError(line.to_owned()))?; mapper.insert(hash.to_owned(), id.to_owned()); } } @@ -51,24 +64,16 @@ impl IdMapper { .filter(|hash| hash.starts_with(short_hash)) .collect(); if matching_hashes.len() == 0 { - Err(anyhow!( - "cannot find maildir message id from short hash {:?}", - short_hash, - )) + Err(Error::FindFromShortHashError(short_hash.to_owned())) } else if matching_hashes.len() > 1 { - Err(anyhow!( - "the short hash {:?} matches more than one hash: {}", - short_hash, + Err(Error::MatchShortHashError( + short_hash.to_owned(), matching_hashes .iter() .map(|s| s.to_string()) .collect::>() - .join(", ") - ) - .context(format!( - "cannot find maildir message id from short hash {:?}", - short_hash - ))) + .join(", "), + )) } else { Ok(self.get(matching_hashes[0]).unwrap().to_owned()) } @@ -98,28 +103,28 @@ impl IdMapper { self.short_hash_len = short_hash_len; - OpenOptions::new() + fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&self.path) - .context("cannot open maildir id hash map cache")? + .map_err(|err| Error::OpenHashMapFileError(err, self.path.to_owned()))? .write(format!("{}\n{}", short_hash_len, entries).as_bytes()) - .context("cannot write maildir id hash map cache")?; + .map_err(|err| Error::WriteHashMapFileError(err, self.path.to_owned()))?; Ok(short_hash_len) } } -impl Deref for IdMapper { - type Target = HashMap; +impl ops::Deref for IdMapper { + type Target = collections::HashMap; fn deref(&self) -> &Self::Target { &self.map } } -impl DerefMut for IdMapper { +impl ops::DerefMut for IdMapper { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.map } diff --git a/lib/src/backend/imap/error.rs b/lib/src/backend/imap/error.rs new file mode 100644 index 0000000..76454a3 --- /dev/null +++ b/lib/src/backend/imap/error.rs @@ -0,0 +1,86 @@ +use std::result; +use thiserror::Error; + +use crate::{ + account, + msg::{self, Flags}, +}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot get envelope of message {0}")] + GetEnvelopeError(u32), + #[error("cannot get sender of message {0}")] + GetSenderError(u32), + #[error("cannot get imap session")] + GetSessionError, + #[error("cannot retrieve message {0}'s uid")] + GetMsgUidError(u32), + #[error("cannot find message {0}")] + FindMsgError(String), + #[error("cannot parse sort criterion {0}")] + ParseSortCriterionError(String), + + #[error("cannot decode subject of message {1}")] + DecodeSubjectError(#[source] rfc2047_decoder::Error, u32), + #[error("cannot decode sender name of message {1}")] + DecodeSenderNameError(#[source] rfc2047_decoder::Error, u32), + #[error("cannot decode sender mailbox of message {1}")] + DecodeSenderMboxError(#[source] rfc2047_decoder::Error, u32), + #[error("cannot decode sender host of message {1}")] + DecodeSenderHostError(#[source] rfc2047_decoder::Error, u32), + + #[error("cannot create tls connector")] + CreateTlsConnectorError(#[source] native_tls::Error), + #[error("cannot connect to imap server")] + ConnectImapServerError(#[source] imap::Error), + #[error("cannot login to imap server")] + LoginImapServerError(#[source] imap::Error), + #[error("cannot search new messages")] + SearchNewMsgsError(#[source] imap::Error), + #[error("cannot examine mailbox {1}")] + ExamineMboxError(#[source] imap::Error, String), + #[error("cannot start the idle mode")] + StartIdleModeError(#[source] imap::Error), + #[error("cannot parse message {1}")] + ParseMsgError(#[source] mailparse::MailParseError, String), + #[error("cannot fetch new messages envelope")] + FetchNewMsgsEnvelopeError(#[source] imap::Error), + #[error("cannot get uid of message {0}")] + GetUidError(u32), + #[error("cannot create mailbox {1}")] + CreateMboxError(#[source] imap::Error, String), + #[error("cannot list mailboxes")] + ListMboxesError(#[source] imap::Error), + #[error("cannot delete mailbox {1}")] + DeleteMboxError(#[source] imap::Error, String), + #[error("cannot select mailbox {1}")] + SelectMboxError(#[source] imap::Error, String), + #[error("cannot fetch messages within range {1}")] + FetchMsgsByRangeError(#[source] imap::Error, String), + #[error("cannot fetch messages by sequence {1}")] + FetchMsgsBySeqError(#[source] imap::Error, String), + #[error("cannot append message to mailbox {1}")] + AppendMsgError(#[source] imap::Error, String), + #[error("cannot sort messages in mailbox {1} with query: {2}")] + SortMsgsError(#[source] imap::Error, String, String), + #[error("cannot search messages in mailbox {1} with query: {2}")] + SearchMsgsError(#[source] imap::Error, String, String), + #[error("cannot expunge mailbox {1}")] + ExpungeError(#[source] imap::Error, String), + #[error("cannot add flags {1} to message(s) {2}")] + AddFlagsError(#[source] imap::Error, Flags, String), + #[error("cannot set flags {1} to message(s) {2}")] + SetFlagsError(#[source] imap::Error, Flags, String), + #[error("cannot delete flags {1} to message(s) {2}")] + DelFlagsError(#[source] imap::Error, Flags, String), + #[error("cannot logout from imap server")] + LogoutError(#[source] imap::Error), + + #[error(transparent)] + AccountError(#[from] account::Error), + #[error(transparent)] + MsgError(#[from] msg::Error), +} + +pub type Result = result::Result; diff --git a/cli/src/backends/imap/imap_backend.rs b/lib/src/backend/imap/imap_backend.rs similarity index 78% rename from cli/src/backends/imap/imap_backend.rs rename to lib/src/backend/imap/imap_backend.rs index 71e9a32..d5bcee5 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/lib/src/backend/imap/imap_backend.rs @@ -2,24 +2,20 @@ //! //! This module contains the definition of the IMAP backend. -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::{ - account::{AccountConfig, ImapBackendConfig}, - mbox::{Mbox, Mboxes}, - msg::{Envelopes, Flags}, -}; use imap::types::NameAttribute; use log::{debug, log_enabled, trace, Level}; use native_tls::{TlsConnector, TlsStream}; use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread}; use crate::{ - backends::{ - from_imap_fetch, from_imap_fetches, imap::msg_sort_criterion::SortCriteria, - into_imap_flags, Backend, + account::{AccountConfig, ImapBackendConfig}, + backend::{ + backend::Result, from_imap_fetch, from_imap_fetches, + imap::msg_sort_criterion::SortCriteria, imap::Error, into_imap_flags, Backend, }, - msg::Msg, - output::run_cmd, + mbox::{Mbox, Mboxes}, + msg::{Envelopes, Flags, Msg}, + process::run_cmd, }; type ImapSess = imap::Session>; @@ -47,7 +43,7 @@ impl<'a> ImapBackend<'a> { .danger_accept_invalid_certs(self.imap_config.imap_insecure) .danger_accept_invalid_hostnames(self.imap_config.imap_insecure) .build() - .context("cannot create TLS connector")?; + .map_err(Error::CreateTlsConnectorError)?; debug!("create client"); debug!("host: {}", self.imap_config.imap_host); @@ -60,7 +56,7 @@ impl<'a> ImapBackend<'a> { } let client = client_builder .connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?)) - .context("cannot connect to IMAP server")?; + .map_err(Error::ConnectImapServerError)?; debug!("create session"); debug!("login: {}", self.imap_config.imap_login); @@ -70,23 +66,24 @@ impl<'a> ImapBackend<'a> { &self.imap_config.imap_login, &self.imap_config.imap_passwd()?, ) - .map_err(|res| res.0) - .context("cannot login to IMAP server")?; + .map_err(|res| Error::LoginImapServerError(res.0))?; sess.debug = log_enabled!(Level::Trace); self.sess = Some(sess); } - match self.sess { + let sess = match self.sess { Some(ref mut sess) => Ok(sess), - None => Err(anyhow!("cannot get IMAP session")), - } + None => Err(Error::GetSessionError), + }?; + + Ok(sess) } fn search_new_msgs(&mut self, query: &str) -> Result> { let uids: Vec = self .sess()? .uid_search(query) - .context("cannot search new messages")? + .map_err(Error::SearchNewMsgsError)? .into_iter() .collect(); debug!("found {} new messages", uids.len()); @@ -101,7 +98,7 @@ impl<'a> ImapBackend<'a> { debug!("examine mailbox {:?}", mbox); self.sess()? .examine(mbox) - .context(format!("cannot examine mailbox {}", mbox))?; + .map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?; debug!("init messages hashset"); let mut msgs_set: HashSet = self @@ -123,7 +120,7 @@ impl<'a> ImapBackend<'a> { false }) }) - .context("cannot start the idle mode")?; + .map_err(Error::StartIdleModeError)?; let uids: Vec = self .search_new_msgs(&self.account_config.notify_query)? @@ -142,13 +139,11 @@ impl<'a> ImapBackend<'a> { let fetches = self .sess()? .uid_fetch(uids, "(UID ENVELOPE)") - .context("cannot fetch new messages enveloppe")?; + .map_err(Error::FetchNewMsgsEnvelopeError)?; for fetch in fetches.iter() { let msg = from_imap_fetch(fetch)?; - let uid = fetch.uid.ok_or_else(|| { - anyhow!("cannot retrieve message {}'s UID", fetch.message) - })?; + let uid = fetch.uid.ok_or_else(|| Error::GetUidError(fetch.message))?; let from = msg.sender.to_owned().into(); self.account_config.run_notify_cmd(&msg.subject, &from)?; @@ -171,7 +166,7 @@ impl<'a> ImapBackend<'a> { self.sess()? .examine(mbox) - .context(format!("cannot examine mailbox `{}`", mbox))?; + .map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?; loop { debug!("begin loop"); @@ -185,7 +180,7 @@ impl<'a> ImapBackend<'a> { false }) }) - .context("cannot start the idle mode")?; + .map_err(Error::StartIdleModeError)?; let cmds = self.account_config.watch_cmds.clone(); thread::spawn(move || { @@ -204,9 +199,14 @@ impl<'a> ImapBackend<'a> { impl<'a> Backend<'a> for ImapBackend<'a> { fn add_mbox(&mut self, mbox: &str) -> Result<()> { + trace!(">> add mailbox"); + self.sess()? .create(mbox) - .context(format!("cannot create imap mailbox {:?}", mbox)) + .map_err(|err| Error::CreateMboxError(err, mbox.to_owned()))?; + + trace!("<< add mailbox"); + Ok(()) } fn get_mboxes(&mut self) -> Result { @@ -215,7 +215,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let imap_mboxes = self .sess()? .list(Some(""), Some("*")) - .context("cannot list mailboxes")?; + .map_err(Error::ListMboxesError)?; let mboxes = Mboxes { mboxes: imap_mboxes .iter() @@ -244,16 +244,21 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn del_mbox(&mut self, mbox: &str) -> Result<()> { + trace!(">> delete imap mailbox"); + self.sess()? .delete(mbox) - .context(format!("cannot delete imap mailbox {:?}", mbox)) + .map_err(|err| Error::DeleteMboxError(err, mbox.to_owned()))?; + + trace!("<< delete imap mailbox"); + Ok(()) } fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result { let last_seq = self .sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))? + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? .exists as usize; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { @@ -273,9 +278,10 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let fetches = self .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") - .context(format!("cannot fetch messages within range {:?}", range))?; + .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; - from_imap_fetches(fetches) + let envelopes = from_imap_fetches(fetches)?; + Ok(envelopes) } fn search_envelopes( @@ -289,7 +295,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let last_seq = self .sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))? + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? .exists; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { @@ -301,10 +307,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let seqs: Vec = if sort.is_empty() { self.sess()? .search(query) - .context(format!( - "cannot find envelopes in {:?} with query {:?}", - mbox, query - ))? + .map_err(|err| Error::SearchMsgsError(err, mbox.to_owned(), query.to_owned()))? .iter() .map(|seq| seq.to_string()) .collect() @@ -313,10 +316,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let charset = imap::extensions::sort::SortCharset::Utf8; self.sess()? .sort(&sort, charset, query) - .context(format!( - "cannot find envelopes in {:?} with query {:?}", - mbox, query - ))? + .map_err(|err| Error::SortMsgsError(err, mbox.to_owned(), query.to_owned()))? .iter() .map(|seq| seq.to_string()) .collect() @@ -329,9 +329,10 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let fetches = self .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") - .context(format!("cannot fetch messages within range {:?}", range))?; + .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; - from_imap_fetches(fetches) + let envelopes = from_imap_fetches(fetches)?; + Ok(envelopes) } fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result { @@ -340,11 +341,11 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .append(mbox, msg) .flags(into_imap_flags(&flags)) .finish() - .context(format!("cannot append message to {:?}", mbox))?; + .map_err(|err| Error::AppendMsgError(err, mbox.to_owned()))?; let last_seq = self .sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))? + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? .exists; Ok(last_seq.to_string()) } @@ -352,17 +353,18 @@ impl<'a> Backend<'a> for ImapBackend<'a> { fn get_msg(&mut self, mbox: &str, seq: &str) -> Result { self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; let fetches = self .sess()? .fetch(seq, "(FLAGS INTERNALDATE BODY[])") - .context(format!("cannot fetch messages {:?}", seq))?; + .map_err(|err| Error::FetchMsgsBySeqError(err, seq.to_owned()))?; let fetch = fetches .first() - .ok_or_else(|| anyhow!("cannot find message {:?}", seq))?; + .ok_or_else(|| Error::FindMsgError(seq.to_owned()))?; let msg_raw = fetch.body().unwrap_or_default().to_owned(); let mut msg = Msg::from_parsed_mail( - mailparse::parse_mail(&msg_raw).context("cannot parse message")?, + mailparse::parse_mail(&msg_raw) + .map_err(|err| Error::ParseMsgError(err, seq.to_owned()))?, self.account_config, )?; msg.raw = msg_raw; @@ -391,13 +393,13 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let flags: Flags = flags.into(); self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; self.sess()? .store(seq_range, format!("+FLAGS ({})", flags)) - .context(format!("cannot add flags {:?}", &flags))?; + .map_err(|err| Error::AddFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; self.sess()? .expunge() - .context(format!("cannot expunge mailbox {:?}", mbox))?; + .map_err(|err| Error::ExpungeError(err, mbox.to_owned()))?; Ok(()) } @@ -405,10 +407,10 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let flags: Flags = flags.into(); self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; self.sess()? .store(seq_range, format!("FLAGS ({})", flags)) - .context(format!("cannot set flags {:?}", &flags))?; + .map_err(|err| Error::SetFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; Ok(()) } @@ -416,18 +418,24 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let flags: Flags = flags.into(); self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; self.sess()? .store(seq_range, format!("-FLAGS ({})", flags)) - .context(format!("cannot remove flags {:?}", &flags))?; + .map_err(|err| Error::DelFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; Ok(()) } fn disconnect(&mut self) -> Result<()> { + trace!(">> imap logout"); + if let Some(ref mut sess) = self.sess { - debug!("logout from IMAP server"); - sess.logout().context("cannot logout from IMAP server")?; + debug!("logout from imap server"); + sess.logout().map_err(Error::LogoutError)?; + } else { + debug!("no session found"); } + + trace!("<< imap logout"); Ok(()) } } diff --git a/cli/src/backends/imap/imap_envelope.rs b/lib/src/backend/imap/imap_envelope.rs similarity index 53% rename from cli/src/backends/imap/imap_envelope.rs rename to lib/src/backend/imap/imap_envelope.rs index 8a90e3e..639d009 100644 --- a/cli/src/backends/imap/imap_envelope.rs +++ b/lib/src/backend/imap/imap_envelope.rs @@ -3,10 +3,15 @@ //! This module provides IMAP types and conversion utilities related //! to the envelope. -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::msg::Envelope; +use rfc2047_decoder; -use super::from_imap_flags; +use crate::{ + backend::{ + from_imap_flags, + imap::{Error, Result}, + }, + msg::Envelope, +}; /// Represents the raw envelope returned by the `imap` crate. pub type ImapFetch = imap::types::Fetch; @@ -14,7 +19,7 @@ pub type ImapFetch = imap::types::Fetch; pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { let envelope = fetch .envelope() - .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?; + .ok_or_else(|| Error::GetEnvelopeError(fetch.message))?; let id = fetch.message.to_string(); @@ -24,10 +29,8 @@ pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { .subject .as_ref() .map(|subj| { - rfc2047_decoder::decode(subj).context(format!( - "cannot decode subject of message {}", - fetch.message - )) + rfc2047_decoder::decode(subj) + .map_err(|err| Error::DecodeSubjectError(err, fetch.message)) }) .unwrap_or_else(|| Ok(String::default()))?; @@ -36,32 +39,26 @@ pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { .as_ref() .and_then(|addrs| addrs.get(0)) .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) - .ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?; + .ok_or_else(|| Error::GetSenderError(fetch.message))?; let sender = if let Some(ref name) = sender.name { - rfc2047_decoder::decode(&name.to_vec()).context(format!( - "cannot decode sender's name of message {}", - fetch.message, - ))? + rfc2047_decoder::decode(&name.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message))? } else { let mbox = sender .mailbox .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) + .ok_or_else(|| Error::GetSenderError(fetch.message)) .and_then(|mbox| { - rfc2047_decoder::decode(&mbox.to_vec()).context(format!( - "cannot decode sender's mailbox of message {}", - fetch.message, - )) + rfc2047_decoder::decode(&mbox.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) })?; let host = sender .host .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) + .ok_or_else(|| Error::GetSenderError(fetch.message)) .and_then(|host| { - rfc2047_decoder::decode(&host.to_vec()).context(format!( - "cannot decode sender's host of message {}", - fetch.message, - )) + rfc2047_decoder::decode(&host.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) })?; format!("{}@{}", mbox, host) }; diff --git a/lib/src/backend/imap/imap_envelopes.rs b/lib/src/backend/imap/imap_envelopes.rs new file mode 100644 index 0000000..3cbb010 --- /dev/null +++ b/lib/src/backend/imap/imap_envelopes.rs @@ -0,0 +1,18 @@ +use crate::{ + backend::{ + imap::{from_imap_fetch, Result}, + ImapFetch, + }, + msg::Envelopes, +}; + +/// Represents the list of raw envelopes returned by the `imap` crate. +pub type ImapFetches = imap::types::ZeroCopy>; + +pub fn from_imap_fetches(fetches: ImapFetches) -> Result { + let mut envelopes = Envelopes::default(); + for fetch in fetches.iter().rev() { + envelopes.push(from_imap_fetch(fetch)?); + } + Ok(envelopes) +} diff --git a/cli/src/backends/imap/imap_flag.rs b/lib/src/backend/imap/imap_flag.rs similarity index 95% rename from cli/src/backends/imap/imap_flag.rs rename to lib/src/backend/imap/imap_flag.rs index 1a24ab0..58ec612 100644 --- a/cli/src/backends/imap/imap_flag.rs +++ b/lib/src/backend/imap/imap_flag.rs @@ -1,4 +1,4 @@ -use himalaya_lib::msg::Flag; +use crate::msg::Flag; pub fn from_imap_flag(imap_flag: &imap::types::Flag<'_>) -> Flag { match imap_flag { diff --git a/cli/src/backends/imap/imap_flags.rs b/lib/src/backend/imap/imap_flags.rs similarity index 91% rename from cli/src/backends/imap/imap_flags.rs rename to lib/src/backend/imap/imap_flags.rs index 186328a..3aa42d5 100644 --- a/cli/src/backends/imap/imap_flags.rs +++ b/lib/src/backend/imap/imap_flags.rs @@ -1,6 +1,7 @@ -use himalaya_lib::msg::{Flag, Flags}; - -use super::from_imap_flag; +use crate::{ + backend::from_imap_flag, + msg::{Flag, Flags}, +}; pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec> { flags diff --git a/cli/src/backends/imap/msg_sort_criterion.rs b/lib/src/backend/imap/msg_sort_criterion.rs similarity index 95% rename from cli/src/backends/imap/msg_sort_criterion.rs rename to lib/src/backend/imap/msg_sort_criterion.rs index d20e9bd..222677b 100644 --- a/cli/src/backends/imap/msg_sort_criterion.rs +++ b/lib/src/backend/imap/msg_sort_criterion.rs @@ -3,9 +3,10 @@ //! This module regroups everything related to deserialization of //! message sort criteria. -use anyhow::{anyhow, Error, Result}; use std::{convert::TryFrom, ops::Deref}; +use crate::backend::imap::Error; + /// Represents the message sort criteria. It is just a wrapper around /// the `imap::extensions::sort::SortCriterion`. pub struct SortCriteria<'a>(Vec>); @@ -53,7 +54,7 @@ impl<'a> TryFrom<&'a str> for SortCriteria<'a> { "to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( &imap::extensions::sort::SortCriterion::To, )), - _ => Err(anyhow!("cannot parse sort criterion {:?}", criterion_str)), + _ => Err(Error::ParseSortCriterionError(criterion_str.to_owned())), }?); } Ok(Self(criteria)) diff --git a/lib/src/backend/maildir/error.rs b/lib/src/backend/maildir/error.rs new file mode 100644 index 0000000..898d249 --- /dev/null +++ b/lib/src/backend/maildir/error.rs @@ -0,0 +1,49 @@ +use std::{io, path}; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MaildirError { + #[error("cannot find maildir sender")] + FindSenderError, + #[error("cannot read maildir directory {0}")] + ReadDirError(path::PathBuf), + #[error("cannot parse maildir subdirectory {0}")] + ParseSubdirError(path::PathBuf), + #[error("cannot get maildir envelopes at page {0}")] + GetEnvelopesOutOfBoundsError(usize), + #[error("cannot search maildir envelopes: feature not implemented")] + SearchEnvelopesUnimplementedError, + #[error("cannot get maildir message {0}")] + GetMsgError(String), + #[error("cannot decode maildir entry")] + DecodeEntryError(#[source] io::Error), + #[error("cannot parse maildir message")] + ParseMsgError(#[source] maildir::MailEntryError), + #[error("cannot decode header {0}")] + DecodeHeaderError(#[source] rfc2047_decoder::Error, String), + #[error("cannot parse maildir message header {0}")] + ParseHeaderError(#[source] mailparse::MailParseError, String), + #[error("cannot create maildir subdirectory {1}")] + CreateSubdirError(#[source] io::Error, String), + #[error("cannot decode maildir subdirectory")] + DecodeSubdirError(#[source] io::Error), + #[error("cannot delete subdirectories at {1}")] + DeleteAllDirError(#[source] io::Error, path::PathBuf), + #[error("cannot get current directory")] + GetCurrentDirError(#[source] io::Error), + #[error("cannot store maildir message with flags")] + StoreWithFlagsError(#[source] maildir::MaildirError), + #[error("cannot copy maildir message")] + CopyMsgError(#[source] io::Error), + #[error("cannot move maildir message")] + MoveMsgError(#[source] io::Error), + #[error("cannot delete maildir message")] + DelMsgError(#[source] io::Error), + #[error("cannot add maildir flags")] + AddFlagsError(#[source] io::Error), + #[error("cannot set maildir flags")] + SetFlagsError(#[source] io::Error), + #[error("cannot remove maildir flags")] + DelFlagsError(#[source] io::Error), +} diff --git a/cli/src/backends/maildir/maildir_backend.rs b/lib/src/backend/maildir/maildir_backend.rs similarity index 50% rename from cli/src/backends/maildir/maildir_backend.rs rename to lib/src/backend/maildir/maildir_backend.rs index 2c4e2d0..c031246 100644 --- a/cli/src/backends/maildir/maildir_backend.rs +++ b/lib/src/backend/maildir/maildir_backend.rs @@ -3,20 +3,18 @@ //! This module contains the definition of the maildir backend and its //! traits implementation. -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::{ - account::{AccountConfig, MaildirBackendConfig}, - mbox::{Mbox, Mboxes}, - msg::Envelopes, -}; use log::{debug, info, trace}; use std::{env, ffi::OsStr, fs, path::PathBuf}; use crate::{ - backends::{maildir_envelopes, Backend, IdMapper}, - msg::Msg, + account::{AccountConfig, MaildirBackendConfig}, + backend::{backend::Result, maildir_envelopes, maildir_flags, Backend, IdMapper}, + mbox::{Mbox, Mboxes}, + msg::{Envelopes, Flags, Msg}, }; +use super::MaildirError; + /// Represents the maildir backend. pub struct MaildirBackend<'a> { account_config: &'a AccountConfig, @@ -35,11 +33,12 @@ impl<'a> MaildirBackend<'a> { } fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result { - if mdir_path.is_dir() { + let path = if mdir_path.is_dir() { Ok(mdir_path) } else { - Err(anyhow!("cannot read maildir directory {:?}", mdir_path)) - } + Err(MaildirError::ReadDirError(mdir_path.to_owned())) + }?; + Ok(path) } /// Creates a maildir instance from a string slice. @@ -60,7 +59,13 @@ impl<'a> MaildirBackend<'a> { // then for relative path to `maildir-dir`, .or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir))) // and finally for relative path to the current directory. - .or_else(|_| self.validate_mdir_path(env::current_dir()?.join(&dir))) + .or_else(|_| { + self.validate_mdir_path( + env::current_dir() + .map_err(MaildirError::GetCurrentDirError)? + .join(&dir), + ) + }) .or_else(|_| { // Otherwise creates a maildir instance from a maildir // subdirectory by adding a "." in front of the name @@ -82,7 +87,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { trace!("subdir path: {:?}", path); fs::create_dir(&path) - .with_context(|| format!("cannot create maildir subdir {:?} at {:?}", subdir, path))?; + .map_err(|err| MaildirError::CreateSubdirError(err, subdir.to_owned()))?; info!("<< add maildir subdir"); Ok(()) @@ -100,19 +105,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { }) } for entry in self.mdir.list_subdirs() { - let dir = entry?; + let dir = entry.map_err(MaildirError::DecodeSubdirError)?; let dirname = dir.path().file_name(); mboxes.push(Mbox { delim: String::from("/"), name: dirname .and_then(OsStr::to_str) .and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) }) - .ok_or_else(|| { - anyhow!( - "cannot parse maildir subdirectory name from path {:?}", - dirname - ) - })? + .ok_or_else(|| MaildirError::ParseSubdirError(dir.path().to_owned()))? .into(), ..Mbox::default() }); @@ -131,7 +131,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { trace!("dir path: {:?}", path); fs::remove_dir_all(&path) - .with_context(|| format!("cannot delete maildir {:?} from {:?}", dir, path))?; + .map_err(|err| MaildirError::DeleteAllDirError(err, path.to_owned()))?; info!("<< delete maildir dir"); Ok(()) @@ -143,16 +143,11 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("page size: {:?}", page_size); debug!("page: {:?}", page); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; + let mdir = self.get_mdir_from_dir(dir)?; // Reads envelopes from the "cur" folder of the selected // maildir. - let mut envelopes = - maildir_envelopes::from_maildir_entries(mdir.list_cur()).with_context(|| { - format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) - })?; + let mut envelopes = maildir_envelopes::from_maildir_entries(mdir.list_cur())?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -160,10 +155,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let page_begin = page * page_size; debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { - return Err(anyhow!( - "cannot get maildir envelopes at page {:?} (out of bounds)", - page_begin + 1, - )); + return Err(MaildirError::GetEnvelopesOutOfBoundsError(page_begin + 1))?; } let page_end = envelopes.len().min(page_begin + page_size); debug!("page end: {:?}", page_end); @@ -207,9 +199,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { ) -> Result { info!(">> search maildir envelopes"); info!("<< search maildir envelopes"); - Err(anyhow!( - "cannot find maildir envelopes: feature not implemented" - )) + Err(MaildirError::SearchEnvelopesUnimplementedError)? } fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result { @@ -217,27 +207,20 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("dir: {:?}", dir); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; + let flags = Flags::from(flags); + debug!("flags: {:?}", flags); + + let mdir = self.get_mdir_from_dir(dir)?; let id = mdir - .store_cur_with_flags(msg, &flags.to_string()) - .with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?; + .store_cur_with_flags(msg, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::StoreWithFlagsError)?; debug!("id: {:?}", id); let hash = format!("{:x}", md5::compute(&id)); debug!("hash: {:?}", hash); // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?; - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + let mut mapper = IdMapper::new(mdir.path())?; + mapper.append(vec![(hash.clone(), id.clone())])?; info!("<< add maildir message"); Ok(hash) @@ -248,32 +231,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let id = IdMapper::new(mdir.path())? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - let mut mail_entry = mdir.find(&id).ok_or_else(|| { - anyhow!( - "cannot find maildir message by id {:?} at {:?}", - id, - mdir.path() - ) - })?; - let parsed_mail = mail_entry.parsed().with_context(|| { - format!("cannot parse maildir message {:?} at {:?}", id, mdir.path()) - })?; - let msg = Msg::from_parsed_mail(parsed_mail, self.account_config).with_context(|| { - format!("cannot parse maildir message {:?} at {:?}", id, mdir.path()) - })?; + let mut mail_entry = mdir + .find(&id) + .ok_or_else(|| MaildirError::GetMsgError(id.to_owned()))?; + let parsed_mail = mail_entry.parsed().map_err(MaildirError::ParseMsgError)?; + let msg = Msg::from_parsed_mail(parsed_mail, self.account_config)?; trace!("message: {:?}", msg); info!("<< get maildir message"); @@ -285,46 +250,19 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("source dir: {:?}", dir_src); debug!("destination dir: {:?}", dir_dst); - let mdir_src = self - .get_mdir_from_dir(dir_src) - .with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?; - let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| { - format!("cannot get destination maildir instance from {:?}", dir_dst) - })?; - let id = IdMapper::new(mdir_src.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir_src.path() - ) - })?; + let mdir_src = self.get_mdir_from_dir(dir_src)?; + let mdir_dst = self.get_mdir_from_dir(dir_dst)?; + let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir_src.copy_to(&id, &mdir_dst).with_context(|| { - format!( - "cannot copy message {:?} from maildir {:?} to maildir {:?}", - id, - mdir_src.path(), - mdir_dst.path() - ) - })?; + mdir_src + .copy_to(&id, &mdir_dst) + .map_err(MaildirError::CopyMsgError)?; // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| { - format!("cannot create id mapper instance for {:?}", mdir_dst.path()) - })?; + let mut mapper = IdMapper::new(mdir_dst.path())?; let hash = format!("{:x}", md5::compute(&id)); - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + mapper.append(vec![(hash.clone(), id.clone())])?; info!("<< copy maildir message"); Ok(()) @@ -335,46 +273,19 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("source dir: {:?}", dir_src); debug!("destination dir: {:?}", dir_dst); - let mdir_src = self - .get_mdir_from_dir(dir_src) - .with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?; - let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| { - format!("cannot get destination maildir instance from {:?}", dir_dst) - })?; - let id = IdMapper::new(mdir_src.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir_src.path() - ) - })?; + let mdir_src = self.get_mdir_from_dir(dir_src)?; + let mdir_dst = self.get_mdir_from_dir(dir_dst)?; + let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir_src.move_to(&id, &mdir_dst).with_context(|| { - format!( - "cannot move message {:?} from maildir {:?} to maildir {:?}", - id, - mdir_src.path(), - mdir_dst.path() - ) - })?; + mdir_src + .move_to(&id, &mdir_dst) + .map_err(MaildirError::MoveMsgError)?; // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| { - format!("cannot create id mapper instance for {:?}", mdir_dst.path()) - })?; + let mut mapper = IdMapper::new(mdir_dst.path())?; let hash = format!("{:x}", md5::compute(&id)); - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + mapper.append(vec![(hash.clone(), id.clone())])?; info!("<< move maildir message"); Ok(()) @@ -385,27 +296,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.delete(&id).with_context(|| { - format!( - "cannot delete message {:?} from maildir {:?}", - id, - mdir.path() - ) - })?; + mdir.delete(&id).map_err(MaildirError::DelMsgError)?; info!("<< delete maildir message"); Ok(()) @@ -415,25 +309,15 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { info!(">> add maildir message flags"); debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); + let flags = Flags::from(flags); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - debug!("flags: {:?}", flags); - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.add_flags(&id, &flags.to_string()) - .with_context(|| format!("cannot add flags {:?} to maildir message {:?}", flags, id))?; + + mdir.add_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::AddFlagsError)?; info!("<< add maildir message flags"); Ok(()) @@ -443,25 +327,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { info!(">> set maildir message flags"); debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); + let flags = Flags::from(flags); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - debug!("flags: {:?}", flags); - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.set_flags(&id, &flags.to_string()) - .with_context(|| format!("cannot set flags {:?} to maildir message {:?}", flags, id))?; + mdir.set_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::SetFlagsError)?; info!("<< set maildir message flags"); Ok(()) @@ -471,30 +344,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { info!(">> delete maildir message flags"); debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); + let flags = Flags::from(flags); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - debug!("flags: {:?}", flags); - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.remove_flags(&id, &flags.to_string()) - .with_context(|| { - format!( - "cannot delete flags {:?} to maildir message {:?}", - flags, id - ) - })?; + mdir.remove_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::DelFlagsError)?; info!("<< delete maildir message flags"); Ok(()) diff --git a/cli/src/backends/maildir/maildir_envelope.rs b/lib/src/backend/maildir/maildir_envelope.rs similarity index 83% rename from cli/src/backends/maildir/maildir_envelope.rs rename to lib/src/backend/maildir/maildir_envelope.rs index 8cc9884..58966fd 100644 --- a/cli/src/backends/maildir/maildir_envelope.rs +++ b/lib/src/backend/maildir/maildir_envelope.rs @@ -1,13 +1,13 @@ -use anyhow::{anyhow, Context, Result}; use chrono::DateTime; -use himalaya_lib::msg::Envelope; use log::trace; use crate::{ - backends::maildir_flags, - msg::{from_slice_to_addrs, Addr}, + backend::{backend::Result, maildir_flags}, + msg::{from_slice_to_addrs, Addr, Envelope}, }; +use super::MaildirError; + /// Represents the raw envelope returned by the `maildir` crate. pub type MaildirEnvelope = maildir::MailEntry; @@ -20,7 +20,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { envelope.id = format!("{:x}", md5::compute(&envelope.internal_id)); envelope.flags = maildir_flags::from_maildir_entry(&entry); - let parsed_mail = entry.parsed().context("cannot parse maildir mail entry")?; + let parsed_mail = entry.parsed().map_err(MaildirError::ParseMsgError)?; trace!(">> parse headers"); for h in parsed_mail.get_headers() { @@ -28,7 +28,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { trace!("header key: {:?}", k); let v = rfc2047_decoder::decode(h.get_value_raw()) - .context(format!("cannot decode value from header {:?}", k))?; + .map_err(|err| MaildirError::DecodeHeaderError(err, k.to_owned()))?; trace!("header value: {:?}", v); match k.to_lowercase().as_str() { @@ -43,7 +43,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { } "from" => { envelope.sender = from_slice_to_addrs(v) - .context(format!("cannot parse header {:?}", k))? + .map_err(|err| MaildirError::ParseHeaderError(err, k.to_owned()))? .and_then(|senders| { if senders.is_empty() { None @@ -59,7 +59,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { group_name.to_owned() } }) - .ok_or_else(|| anyhow!("cannot find sender"))?; + .ok_or_else(|| MaildirError::FindSenderError)?; } _ => (), } diff --git a/cli/src/backends/maildir/maildir_envelopes.rs b/lib/src/backend/maildir/maildir_envelopes.rs similarity index 52% rename from cli/src/backends/maildir/maildir_envelopes.rs rename to lib/src/backend/maildir/maildir_envelopes.rs index 4921227..ff83a58 100644 --- a/cli/src/backends/maildir/maildir_envelopes.rs +++ b/lib/src/backend/maildir/maildir_envelopes.rs @@ -1,25 +1,21 @@ //! Maildir mailbox module. //! //! This module provides Maildir types and conversion utilities -//! related to the envelope +//! related to the envelope. -use himalaya_lib::msg::Envelopes; -use anyhow::{Result, Context}; +use crate::{backend::backend::Result, msg::Envelopes}; -use super::maildir_envelope; +use super::{maildir_envelope, MaildirError}; -/// Represents a list of raw envelopees returned by the `maildir` crate. +/// Represents a list of raw envelopees returned by the `maildir` +/// crate. pub type MaildirEnvelopes = maildir::MailEntries; pub fn from_maildir_entries(mail_entries: MaildirEnvelopes) -> Result { let mut envelopes = Envelopes::default(); for entry in mail_entries { - envelopes.push( - maildir_envelope::from_maildir_entry( - entry.context("cannot decode maildir mail entry")?, - ) - .context("cannot parse maildir mail entry")?, - ); + let entry = entry.map_err(MaildirError::DecodeEntryError)?; + envelopes.push(maildir_envelope::from_maildir_entry(entry)?); } Ok(envelopes) } diff --git a/lib/src/backend/maildir/maildir_flag.rs b/lib/src/backend/maildir/maildir_flag.rs new file mode 100644 index 0000000..f506e4a --- /dev/null +++ b/lib/src/backend/maildir/maildir_flag.rs @@ -0,0 +1,24 @@ +use crate::msg::Flag; + +pub fn from_char(c: char) -> Flag { + match c { + 'r' | 'R' => Flag::Answered, + 's' | 'S' => Flag::Seen, + 't' | 'T' => Flag::Deleted, + 'd' | 'D' => Flag::Draft, + 'f' | 'F' => Flag::Flagged, + 'p' | 'P' => Flag::Custom(String::from("Passed")), + flag => Flag::Custom(flag.to_string()), + } +} + +pub fn to_normalized_char(flag: &Flag) -> Option { + match flag { + Flag::Answered => Some('R'), + Flag::Seen => Some('S'), + Flag::Deleted => Some('T'), + Flag::Draft => Some('D'), + Flag::Flagged => Some('F'), + _ => None, + } +} diff --git a/lib/src/backend/maildir/maildir_flags.rs b/lib/src/backend/maildir/maildir_flags.rs new file mode 100644 index 0000000..db537d7 --- /dev/null +++ b/lib/src/backend/maildir/maildir_flags.rs @@ -0,0 +1,11 @@ +use crate::msg::Flags; + +use super::maildir_flag; + +pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags { + entry.flags().chars().map(maildir_flag::from_char).collect() +} + +pub fn to_normalized_string(flags: &Flags) -> String { + String::from_iter(flags.iter().filter_map(maildir_flag::to_normalized_char)) +} diff --git a/lib/src/backend/mod.rs b/lib/src/backend/mod.rs new file mode 100644 index 0000000..665c543 --- /dev/null +++ b/lib/src/backend/mod.rs @@ -0,0 +1,73 @@ +pub mod backend; +pub use backend::*; + +pub mod id_mapper; +pub use id_mapper::*; + +#[cfg(feature = "imap-backend")] +pub mod imap { + pub mod imap_backend; + pub use imap_backend::*; + + pub mod imap_envelopes; + pub use imap_envelopes::*; + + pub mod imap_envelope; + pub use imap_envelope::*; + + pub mod imap_flags; + pub use imap_flags::*; + + pub mod imap_flag; + pub use imap_flag::*; + + pub mod msg_sort_criterion; + + pub mod error; + pub use error::*; +} + +#[cfg(feature = "imap-backend")] +pub use self::imap::*; + +#[cfg(feature = "maildir-backend")] +pub mod maildir { + pub mod maildir_backend; + pub use maildir_backend::*; + + pub mod maildir_envelopes; + pub use maildir_envelopes::*; + + pub mod maildir_envelope; + pub use maildir_envelope::*; + + pub mod maildir_flags; + pub use maildir_flags::*; + + pub mod maildir_flag; + pub use maildir_flag::*; + + pub mod error; + pub use error::*; +} + +#[cfg(feature = "maildir-backend")] +pub use self::maildir::*; + +#[cfg(feature = "notmuch-backend")] +pub mod notmuch { + pub mod notmuch_backend; + pub use notmuch_backend::*; + + pub mod notmuch_envelopes; + pub use notmuch_envelopes::*; + + pub mod notmuch_envelope; + pub use notmuch_envelope::*; + + pub mod error; + pub use error::*; +} + +#[cfg(feature = "notmuch-backend")] +pub use self::notmuch::*; diff --git a/lib/src/backend/notmuch/error.rs b/lib/src/backend/notmuch/error.rs new file mode 100644 index 0000000..5ff1485 --- /dev/null +++ b/lib/src/backend/notmuch/error.rs @@ -0,0 +1,49 @@ +use std::io; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NotmuchError { + #[error("cannot parse notmuch message header {1}")] + ParseMsgHeaderError(#[source] notmuch::Error, String), + #[error("cannot parse notmuch message date {1}")] + ParseMsgDateError(#[source] chrono::ParseError, String), + #[error("cannot find notmuch message header {0}")] + FindMsgHeaderError(String), + #[error("cannot find notmuch message sender")] + FindSenderError, + #[error("cannot parse notmuch message senders {1}")] + ParseSendersError(#[source] mailparse::MailParseError, String), + #[error("cannot open notmuch database")] + OpenDbError(#[source] notmuch::Error), + #[error("cannot build notmuch query")] + BuildQueryError(#[source] notmuch::Error), + #[error("cannot search notmuch envelopes")] + SearchEnvelopesError(#[source] notmuch::Error), + #[error("cannot get notmuch envelopes at page {0}")] + GetEnvelopesOutOfBoundsError(usize), + #[error("cannot add notmuch mailbox: feature not implemented")] + AddMboxUnimplementedError, + #[error("cannot delete notmuch mailbox: feature not implemented")] + DelMboxUnimplementedError, + #[error("cannot copy notmuch message: feature not implemented")] + CopyMsgUnimplementedError, + #[error("cannot move notmuch message: feature not implemented")] + MoveMsgUnimplementedError, + #[error("cannot index notmuch message")] + IndexFileError(#[source] notmuch::Error), + #[error("cannot find notmuch message")] + FindMsgError(#[source] notmuch::Error), + #[error("cannot find notmuch message")] + FindMsgEmptyError, + #[error("cannot read notmuch raw message from file")] + ReadMsgError(#[source] io::Error), + #[error("cannot parse notmuch raw message")] + ParseMsgError(#[source] mailparse::MailParseError), + #[error("cannot delete notmuch message")] + DelMsgError(#[source] notmuch::Error), + #[error("cannot add notmuch tag")] + AddTagError(#[source] notmuch::Error), + #[error("cannot delete notmuch tag")] + DelTagError(#[source] notmuch::Error), +} diff --git a/cli/src/backends/notmuch/notmuch_backend.rs b/lib/src/backend/notmuch/notmuch_backend.rs similarity index 61% rename from cli/src/backends/notmuch/notmuch_backend.rs rename to lib/src/backend/notmuch/notmuch_backend.rs index b9bcc12..8918c9b 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/lib/src/backend/notmuch/notmuch_backend.rs @@ -1,16 +1,13 @@ +use log::{debug, info, trace}; use std::fs; -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::{ - account::{AccountConfig, NotmuchBackendConfig}, - mbox::{Mbox, Mboxes}, - msg::Envelopes, -}; -use log::{debug, info, trace}; - use crate::{ - backends::{notmuch_envelopes, Backend, IdMapper, MaildirBackend}, - msg::Msg, + account::{AccountConfig, NotmuchBackendConfig}, + backend::{ + backend::Result, notmuch_envelopes, Backend, IdMapper, MaildirBackend, NotmuchError, + }, + mbox::{Mbox, Mboxes}, + msg::{Envelopes, Msg}, }; /// Represents the Notmuch backend. @@ -37,12 +34,7 @@ impl<'a> NotmuchBackend<'a> { notmuch_config.notmuch_database_dir.clone(), notmuch::DatabaseMode::ReadWrite, ) - .with_context(|| { - format!( - "cannot open notmuch database at {:?}", - notmuch_config.notmuch_database_dir - ) - })?, + .map_err(NotmuchError::OpenDbError)?, }; info!("<< create new notmuch backend"); @@ -59,12 +51,12 @@ impl<'a> NotmuchBackend<'a> { let query_builder = self .db .create_query(query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let mut envelopes = - notmuch_envelopes::from_notmuch_msgs(query_builder.search_messages().with_context( - || format!("cannot find notmuch envelopes from query {:?}", query), - )?) - .with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; + let mut envelopes = notmuch_envelopes::from_notmuch_msgs( + query_builder + .search_messages() + .map_err(NotmuchError::SearchEnvelopesError)?, + )?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -72,10 +64,7 @@ impl<'a> NotmuchBackend<'a> { let page_begin = page * page_size; debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { - return Err(anyhow!( - "cannot get notmuch envelopes at page {:?} (out of bounds)", - page_begin + 1, - )); + return Err(NotmuchError::GetEnvelopesOutOfBoundsError(page_begin + 1))?; } let page_end = envelopes.len().min(page_begin + page_size); debug!("page end: {:?}", page_end); @@ -113,9 +102,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn add_mbox(&mut self, _mbox: &str) -> Result<()> { info!(">> add notmuch mailbox"); info!("<< add notmuch mailbox"); - Err(anyhow!( - "cannot add notmuch mailbox: feature not implemented" - )) + Err(NotmuchError::AddMboxUnimplementedError)? } fn get_mboxes(&mut self) -> Result { @@ -139,9 +126,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn del_mbox(&mut self, _mbox: &str) -> Result<()> { info!(">> delete notmuch mailbox"); info!("<< delete notmuch mailbox"); - Err(anyhow!( - "cannot delete notmuch mailbox: feature not implemented" - )) + Err(NotmuchError::DelMboxUnimplementedError)? } fn get_envelopes( @@ -205,51 +190,32 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let dir = &self.notmuch_config.notmuch_database_dir; // Adds the message to the maildir folder and gets its hash. - let hash = self - .mdir - .add_msg("", msg, "seen") - .with_context(|| { - format!( - "cannot add notmuch message to maildir {:?}", - self.notmuch_config.notmuch_database_dir - ) - })? - .to_string(); + let hash = self.mdir.add_msg("", msg, "seen")?; debug!("hash: {:?}", hash); // Retrieves the file path of the added message by its maildir // identifier. - let mut mapper = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))?; - let id = mapper - .find(&hash) - .with_context(|| format!("cannot find notmuch message from short hash {:?}", hash))?; + let mut mapper = IdMapper::new(dir)?; + let id = mapper.find(&hash)?; debug!("id: {:?}", id); let file_path = dir.join("cur").join(format!("{}:2,S", id)); debug!("file path: {:?}", file_path); + println!("file_path: {:?}", file_path); // Adds the message to the notmuch database by indexing it. let id = self .db .index_file(&file_path, None) - .with_context(|| format!("cannot index notmuch message from file {:?}", file_path))? + .map_err(NotmuchError::IndexFileError)? .id() .to_string(); let hash = format!("{:x}", md5::compute(&id)); // Appends hash entry to the id mapper cache file. - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + mapper.append(vec![(hash.clone(), id.clone())])?; // Attaches tags to the notmuch message. - self.add_flags("", &hash, tags) - .with_context(|| format!("cannot add flags to notmuch message {:?}", id))?; + self.add_flags("", &hash, tags)?; info!("<< add notmuch envelopes"); Ok(hash) @@ -260,31 +226,19 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("short hash: {:?}", short_hash); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let msg_file_path = self .db .find_message(&id) - .with_context(|| format!("cannot find notmuch message {:?}", id))? - .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? + .map_err(NotmuchError::FindMsgError)? + .ok_or_else(|| NotmuchError::FindMsgEmptyError)? .filename() .to_owned(); debug!("message file path: {:?}", msg_file_path); - let raw_msg = fs::read(&msg_file_path).with_context(|| { - format!("cannot read notmuch message from file {:?}", msg_file_path) - })?; - let msg = mailparse::parse_mail(&raw_msg) - .with_context(|| format!("cannot parse raw notmuch message {:?}", id))?; - let msg = Msg::from_parsed_mail(msg, &self.account_config) - .with_context(|| format!("cannot parse notmuch message {:?}", id))?; + let raw_msg = fs::read(&msg_file_path).map_err(NotmuchError::ReadMsgError)?; + let msg = mailparse::parse_mail(&raw_msg).map_err(NotmuchError::ParseMsgError)?; + let msg = Msg::from_parsed_mail(msg, &self.account_config)?; trace!("message: {:?}", msg); info!("<< get notmuch message"); @@ -294,17 +248,13 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn copy_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { info!(">> copy notmuch message"); info!("<< copy notmuch message"); - Err(anyhow!( - "cannot copy notmuch message: feature not implemented" - )) + Err(NotmuchError::CopyMsgUnimplementedError)? } fn move_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { info!(">> move notmuch message"); info!("<< move notmuch message"); - Err(anyhow!( - "cannot move notmuch message: feature not implemented" - )) + Err(NotmuchError::MoveMsgUnimplementedError)? } fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> { @@ -312,27 +262,19 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("short hash: {:?}", short_hash); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let msg_file_path = self .db .find_message(&id) - .with_context(|| format!("cannot find notmuch message {:?}", id))? - .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? + .map_err(NotmuchError::FindMsgError)? + .ok_or_else(|| NotmuchError::FindMsgEmptyError)? .filename() .to_owned(); debug!("message file path: {:?}", msg_file_path); self.db .remove_message(msg_file_path) - .with_context(|| format!("cannot delete notmuch message {:?}", id))?; + .map_err(NotmuchError::DelMsgError)?; info!("<< delete notmuch message"); Ok(()) @@ -343,15 +285,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("tags: {:?}", tags); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let query = format!("id:{}", id); debug!("query: {:?}", query); @@ -359,15 +293,14 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let query_builder = self .db .create_query(&query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; let msgs = query_builder .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::SearchEnvelopesError)?; + for msg in msgs { for tag in tags.iter() { - msg.add_tag(*tag).with_context(|| { - format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id()) - })? + msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?; } } @@ -380,15 +313,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("tags: {:?}", tags); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let query = format!("id:{}", id); debug!("query: {:?}", query); @@ -396,18 +321,15 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let query_builder = self .db .create_query(&query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; let msgs = query_builder .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::SearchEnvelopesError)?; for msg in msgs { - msg.remove_all_tags().with_context(|| { - format!("cannot remove all tags from notmuch message {:?}", msg.id()) - })?; + msg.remove_all_tags().map_err(NotmuchError::DelTagError)?; + for tag in tags.iter() { - msg.add_tag(*tag).with_context(|| { - format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id()) - })? + msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?; } } @@ -420,15 +342,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("tags: {:?}", tags); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let query = format!("id:{}", id); debug!("query: {:?}", query); @@ -436,19 +350,13 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let query_builder = self .db .create_query(&query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; let msgs = query_builder .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::SearchEnvelopesError)?; for msg in msgs { for tag in tags.iter() { - msg.remove_tag(*tag).with_context(|| { - format!( - "cannot delete tag {:?} from notmuch message {:?}", - tag, - msg.id() - ) - })? + msg.remove_tag(*tag).map_err(NotmuchError::DelTagError)?; } } diff --git a/cli/src/backends/notmuch/notmuch_envelope.rs b/lib/src/backend/notmuch/notmuch_envelope.rs similarity index 69% rename from cli/src/backends/notmuch/notmuch_envelope.rs rename to lib/src/backend/notmuch/notmuch_envelope.rs index 4d53ba2..6361a9a 100644 --- a/cli/src/backends/notmuch/notmuch_envelope.rs +++ b/lib/src/backend/notmuch/notmuch_envelope.rs @@ -3,12 +3,13 @@ //! This module provides Notmuch types and conversion utilities //! related to the envelope -use anyhow::{anyhow, Context, Result}; use chrono::DateTime; -use himalaya_lib::msg::{Envelope, Flag}; use log::{info, trace}; -use crate::msg::{from_slice_to_addrs, Addr}; +use crate::{ + backend::{backend::Result, NotmuchError}, + msg::{from_slice_to_addrs, Addr, Envelope, Flag}, +}; /// Represents the raw envelope returned by the `notmuch` crate. pub type RawNotmuchEnvelope = notmuch::Message; @@ -20,15 +21,16 @@ pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { let id = format!("{:x}", md5::compute(&internal_id)); let subject = raw_envelope .header("subject") - .context("cannot get header \"Subject\" from notmuch message")? + .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("subject")))? .unwrap_or_default() .to_string(); let sender = raw_envelope .header("from") - .context("cannot get header \"From\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", internal_id))? + .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("from")))? + .ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("from")))? .to_string(); - let sender = from_slice_to_addrs(sender)? + let sender = from_slice_to_addrs(&sender) + .map_err(|err| NotmuchError::ParseSendersError(err, sender.to_owned()))? .and_then(|senders| { if senders.is_empty() { None @@ -42,17 +44,14 @@ pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { } Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), }) - .ok_or_else(|| anyhow!("cannot find sender"))?; + .ok_or_else(|| NotmuchError::FindSenderError)?; let date = raw_envelope .header("date") - .context("cannot get header \"Date\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", internal_id))? + .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("date")))? + .ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("date")))? .to_string(); let date = DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) - .context(format!( - "cannot parse message date {:?} of notmuch message {:?}", - date, internal_id - )) + .map_err(|err| NotmuchError::ParseMsgDateError(err, date.to_owned())) .map(|date| date.naive_local().to_string()) .ok(); diff --git a/cli/src/backends/notmuch/notmuch_envelopes.rs b/lib/src/backend/notmuch/notmuch_envelopes.rs similarity index 66% rename from cli/src/backends/notmuch/notmuch_envelopes.rs rename to lib/src/backend/notmuch/notmuch_envelopes.rs index 7235a46..7bf1240 100644 --- a/cli/src/backends/notmuch/notmuch_envelopes.rs +++ b/lib/src/backend/notmuch/notmuch_envelopes.rs @@ -1,5 +1,4 @@ -use anyhow::{Context, Result}; -use himalaya_lib::msg::Envelopes; +use crate::{backend::backend::Result, msg::Envelopes}; use super::notmuch_envelope; @@ -10,8 +9,7 @@ pub type RawNotmuchEnvelopes = notmuch::Messages; pub fn from_notmuch_msgs(msgs: RawNotmuchEnvelopes) -> Result { let mut envelopes = Envelopes::default(); for msg in msgs { - let envelope = - notmuch_envelope::from_notmuch_msg(msg).context("cannot parse notmuch message")?; + let envelope = notmuch_envelope::from_notmuch_msg(msg)?; envelopes.push(envelope); } Ok(envelopes) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index d8398a8..ab692bc 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,5 +1,6 @@ mod process; pub mod account; +pub mod backend; pub mod mbox; pub mod msg; diff --git a/cli/src/msg/addr_entity.rs b/lib/src/msg/addr.rs similarity index 91% rename from cli/src/msg/addr_entity.rs rename to lib/src/msg/addr.rs index f55278d..0a8b6d5 100644 --- a/cli/src/msg/addr_entity.rs +++ b/lib/src/msg/addr.rs @@ -2,9 +2,10 @@ //! //! This module regroups email address entities and converters. -use anyhow::Result; use mailparse; -use std::fmt::Debug; +use std::{fmt, result}; + +use crate::msg::Result; /// Defines a single email address. pub type Addr = mailparse::MailAddr; @@ -13,7 +14,9 @@ pub type Addr = mailparse::MailAddr; pub type Addrs = mailparse::MailAddrList; /// Converts a slice into an optional list of addresses. -pub fn from_slice_to_addrs + Debug>(addrs: S) -> Result> { +pub fn from_slice_to_addrs + fmt::Debug>( + addrs: S, +) -> result::Result, mailparse::MailParseError> { let addrs = mailparse::addrparse(addrs.as_ref())?; Ok(if addrs.is_empty() { None } else { Some(addrs) }) } diff --git a/lib/src/msg/error.rs b/lib/src/msg/error.rs new file mode 100644 index 0000000..0cf689f --- /dev/null +++ b/lib/src/msg/error.rs @@ -0,0 +1,56 @@ +use std::{ + env, io, + path::{self, PathBuf}, + result, +}; +use thiserror::Error; + +use crate::account; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot expand attachment path {1}")] + ExpandAttachmentPathError(#[source] shellexpand::LookupError, String), + #[error("cannot read attachment at {1}")] + ReadAttachmentError(#[source] io::Error, PathBuf), + #[error("cannot parse template")] + ParseTplError(#[source] mailparse::MailParseError), + #[error("cannot parse content type of attachment {1}")] + ParseAttachmentContentTypeError(#[source] lettre::message::header::ContentTypeErr, String), + #[error("cannot write temporary multipart on the disk")] + WriteTmpMultipartError(#[source] io::Error), + #[error("cannot write temporary multipart on the disk")] + BuildSendableMsgError(#[source] lettre::error::Error), + #[error("cannot parse {1} value: {2}")] + ParseHeaderError(#[source] mailparse::MailParseError, String, String), + #[error("cannot build envelope")] + BuildEnvelopeError(#[source] lettre::error::Error), + #[error("cannot get file name of attachment {0}")] + GetAttachmentFilenameError(PathBuf), + #[error("cannot parse recipient")] + ParseRecipientError, + + #[error("cannot parse message or address")] + ParseAddressError(#[from] lettre::address::AddressError), + + #[error(transparent)] + AccountError(#[from] account::Error), + + #[error("cannot get content type of multipart")] + GetMultipartContentTypeError, + #[error("cannot find encrypted part of multipart")] + GetEncryptedPartMultipartError, + #[error("cannot parse encrypted part of multipart")] + ParseEncryptedPartError(#[source] mailparse::MailParseError), + #[error("cannot get body from encrypted part")] + GetEncryptedPartBodyError(#[source] mailparse::MailParseError), + #[error("cannot write encrypted part to temporary file")] + WriteEncryptedPartBodyError(#[source] io::Error), + #[error("cannot write encrypted part to temporary file")] + DecryptPartError(#[source] account::Error), + + #[error("cannot delete local draft: {1}")] + DeleteLocalDraftError(#[source] io::Error, path::PathBuf), +} + +pub type Result = result::Result; diff --git a/lib/src/msg/flags.rs b/lib/src/msg/flags.rs index 50db6ce..28faad1 100644 --- a/lib/src/msg/flags.rs +++ b/lib/src/msg/flags.rs @@ -1,6 +1,5 @@ -use std::{fmt, ops}; - use serde::Serialize; +use std::{fmt, ops}; use super::Flag; diff --git a/lib/src/msg/mod.rs b/lib/src/msg/mod.rs index d3bec40..9470357 100644 --- a/lib/src/msg/mod.rs +++ b/lib/src/msg/mod.rs @@ -1,3 +1,6 @@ +mod error; +pub use error::*; + mod flag; pub use flag::*; @@ -9,3 +12,18 @@ pub use envelope::*; mod envelopes; pub use envelopes::*; + +mod parts; +pub use parts::*; + +mod addr; +pub use addr::*; + +mod tpl; +pub use tpl::*; + +mod msg; +pub use msg::*; + +mod msg_utils; +pub use msg_utils::*; diff --git a/cli/src/msg/msg_entity.rs b/lib/src/msg/msg.rs similarity index 84% rename from cli/src/msg/msg_entity.rs rename to lib/src/msg/msg.rs index 0ee49ce..64f04f8 100644 --- a/cli/src/msg/msg_entity.rs +++ b/lib/src/msg/msg.rs @@ -1,10 +1,6 @@ use ammonia; -use anyhow::{anyhow, Context, Error, Result}; use chrono::{DateTime, Local, TimeZone, Utc}; use convert_case::{Case, Casing}; -use himalaya_lib::account::{ - AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM, -}; use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use log::{info, trace, warn}; @@ -17,19 +13,14 @@ use std::{ fs, path::PathBuf, }; +use tree_magic; use uuid::Uuid; use crate::{ - backends::Backend, + account::{AccountConfig, DEFAULT_SIG_DELIM}, msg::{ - from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils, - Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride, - }, - output::PrinterService, - smtp::SmtpService, - ui::{ - choice::{self, PostEditChoice, PreEditChoice}, - editor, + from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, Addr, + Addrs, BinaryPart, Error, Part, Parts, Result, TextPlainPart, TplOverride, }, }; @@ -329,100 +320,6 @@ impl Msg { Ok(self) } - fn _edit_with_editor(&self, tpl: TplOverride, account: &AccountConfig) -> Result { - let tpl = self.to_tpl(tpl, account)?; - let tpl = editor::open_with_tpl(tpl)?; - Self::from_tpl(&tpl) - } - - pub fn edit_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( - mut self, - tpl: TplOverride, - account: &AccountConfig, - printer: &mut P, - backend: Box<&'a mut B>, - smtp: &mut S, - ) -> Result> { - info!("start editing with editor"); - - let draft = msg_utils::local_draft_path(); - if draft.exists() { - loop { - match choice::pre_edit() { - Ok(choice) => match choice { - PreEditChoice::Edit => { - let tpl = editor::open_with_draft()?; - self.merge_with(Msg::from_tpl(&tpl)?); - break; - } - PreEditChoice::Discard => { - self.merge_with(self._edit_with_editor(tpl.clone(), account)?); - break; - } - PreEditChoice::Quit => return Ok(backend), - }, - Err(err) => { - println!("{}", err); - continue; - } - } - } - } else { - self.merge_with(self._edit_with_editor(tpl.clone(), account)?); - } - - loop { - match choice::post_edit() { - Ok(PostEditChoice::Send) => { - printer.print_str("Sending message…")?; - let sent_msg = smtp.send(account, &self)?; - let sent_folder = account - .mailboxes - .get("sent") - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_SENT_FOLDER); - printer - .print_str(format!("Adding message to the {:?} folder…", sent_folder))?; - backend.add_msg(&sent_folder, &sent_msg, "seen")?; - msg_utils::remove_local_draft()?; - printer.print_struct("Done!")?; - break; - } - Ok(PostEditChoice::Edit) => { - self.merge_with(self._edit_with_editor(tpl.clone(), account)?); - continue; - } - Ok(PostEditChoice::LocalDraft) => { - printer.print_struct("Message successfully saved locally")?; - break; - } - Ok(PostEditChoice::RemoteDraft) => { - let tpl = self.to_tpl(TplOverride::default(), account)?; - let draft_folder = account - .mailboxes - .get("draft") - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_DRAFT_FOLDER); - backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?; - msg_utils::remove_local_draft()?; - printer - .print_struct(format!("Message successfully saved to {}", draft_folder))?; - break; - } - Ok(PostEditChoice::Discard) => { - msg_utils::remove_local_draft()?; - break; - } - Err(err) => { - println!("{}", err); - continue; - } - } - } - - Ok(backend) - } - pub fn encrypt(mut self, encrypt: bool) -> Self { self.encrypt = encrypt; self @@ -431,14 +328,15 @@ impl Msg { pub fn add_attachments(mut self, attachments_paths: Vec<&str>) -> Result { for path in attachments_paths { let path = shellexpand::full(path) - .context(format!(r#"cannot expand attachment path "{}""#, path))?; + .map_err(|err| Error::ExpandAttachmentPathError(err, path.to_owned()))?; let path = PathBuf::from(path.to_string()); let filename: String = path .file_name() - .ok_or_else(|| anyhow!("cannot get file name of attachment {:?}", path))? + .ok_or_else(|| Error::GetAttachmentFilenameError(path.to_owned()))? .to_string_lossy() .into(); - let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?; + let content = + fs::read(&path).map_err(|err| Error::ReadAttachmentError(err, path.to_owned()))?; let mime = tree_magic::from_u8(&content); self.parts.push(Part::Binary(BinaryPart { @@ -562,7 +460,7 @@ impl Msg { info!("begin: building message from template"); trace!("template: {:?}", tpl); - let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).context("cannot parse template")?; + 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()) @@ -613,10 +511,9 @@ impl Msg { for part in self.attachments() { multipart = multipart.singlepart(Attachment::new(part.filename.clone()).body( part.content, - part.mime.parse().context(format!( - "cannot parse content type of attachment {}", - part.filename - ))?, + part.mime.parse().map_err(|err| { + Error::ParseAttachmentContentTypeError(err, part.filename) + })?, )) } multipart @@ -624,16 +521,15 @@ impl Msg { if self.encrypt { let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string()); - fs::write(multipart_buffer.clone(), multipart.formatted())?; + fs::write(multipart_buffer.clone(), multipart.formatted()) + .map_err(Error::WriteTmpMultipartError)?; let addr = self .to .as_ref() .and_then(|addrs| addrs.clone().extract_single_info()) .map(|addr| addr.addr) - .ok_or_else(|| anyhow!("cannot find recipient"))?; - let encrypted_multipart = account - .pgp_encrypt_file(&addr, multipart_buffer.clone())? - .ok_or_else(|| anyhow!("cannot find pgp encrypt command in config"))?; + .ok_or_else(|| Error::ParseRecipientError)?; + let encrypted_multipart = account.pgp_encrypt_file(&addr, multipart_buffer.clone())?; trace!("encrypted multipart: {:#?}", encrypted_multipart); multipart = MultiPart::encrypted(String::from("application/pgp-encrypted")) .singlepart( @@ -650,7 +546,7 @@ impl Msg { msg_builder .multipart(multipart) - .context("cannot build sendable message") + .map_err(Error::BuildSendableMsgError) } pub fn from_parsed_mail( @@ -686,24 +582,24 @@ impl Msg { } }, "from" => { - msg.from = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.from = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "to" => { - msg.to = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.to = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "reply-to" => { - msg.reply_to = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.reply_to = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "cc" => { - msg.cc = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.cc = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "bcc" => { - msg.bcc = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.bcc = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } key => { msg.headers.insert(key.to_lowercase(), val); @@ -712,8 +608,7 @@ impl Msg { trace!("<< parse header"); } - msg.parts = Parts::from_parsed_mail(config, &parsed_mail) - .context("cannot parsed message mime parts")?; + msg.parts = Parts::from_parsed_mail(config, &parsed_mail)?; trace!("message: {:?}", msg); info!("<< build message from parsed mail"); @@ -840,7 +735,7 @@ impl TryInto for &Msg { .as_ref() .map(from_addrs_to_sendable_addrs) .unwrap_or(Ok(vec![]))?; - Ok(lettre::address::Envelope::new(from, to).context("cannot create envelope")?) + Ok(lettre::address::Envelope::new(from, to).map_err(Error::BuildEnvelopeError)?) } } diff --git a/lib/src/msg/msg_utils.rs b/lib/src/msg/msg_utils.rs new file mode 100644 index 0000000..3c61d7d --- /dev/null +++ b/lib/src/msg/msg_utils.rs @@ -0,0 +1,24 @@ +use log::{debug, trace}; +use std::{env, fs, path}; + +use crate::msg::{Error, Result}; + +pub fn local_draft_path() -> path::PathBuf { + trace!(">> get local draft path"); + + let path = env::temp_dir().join("himalaya-draft.eml"); + debug!("local draft path: {:?}", path); + + trace!("<< get local draft path"); + path +} + +pub fn remove_local_draft() -> Result<()> { + trace!(">> remove local draft"); + + let path = local_draft_path(); + fs::remove_file(&path).map_err(|err| Error::DeleteLocalDraftError(err, path))?; + + trace!("<< remove local draft"); + Ok(()) +} diff --git a/cli/src/msg/parts_entity.rs b/lib/src/msg/parts.rs similarity index 82% rename from cli/src/msg/parts_entity.rs rename to lib/src/msg/parts.rs index 36eac50..9de2bdb 100644 --- a/cli/src/msg/parts_entity.rs +++ b/lib/src/msg/parts.rs @@ -1,5 +1,3 @@ -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::account::AccountConfig; use mailparse::MailHeaderMap; use serde::Serialize; use std::{ @@ -8,6 +6,8 @@ use std::{ }; use uuid::Uuid; +use crate::{account::AccountConfig, msg}; + #[derive(Debug, Clone, Default, Serialize)] pub struct TextPlainPart { pub content: String, @@ -52,7 +52,7 @@ impl Parts { pub fn from_parsed_mail<'a>( account: &'a AccountConfig, part: &'a mailparse::ParsedMail<'a>, - ) -> Result { + ) -> msg::Result { let mut parts = vec![]; if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() { @@ -83,7 +83,7 @@ fn build_parts_map_rec( account: &AccountConfig, parsed_mail: &mailparse::ParsedMail, parts: &mut Vec, -) -> Result<()> { +) -> msg::Result<()> { if parsed_mail.subparts.is_empty() { let cdisp = parsed_mail.get_content_disposition(); match cdisp.disposition { @@ -117,16 +117,15 @@ fn build_parts_map_rec( let ctype = parsed_mail .get_headers() .get_first_value("content-type") - .ok_or_else(|| anyhow!("cannot get content type of multipart"))?; + .ok_or_else(|| msg::Error::GetMultipartContentTypeError)?; if ctype.starts_with("multipart/encrypted") { let decrypted_part = parsed_mail .subparts .get(1) - .ok_or_else(|| anyhow!("cannot find encrypted part of multipart")) - .and_then(|part| decrypt_part(account, part)) - .context("cannot decrypt part of multipart")?; + .ok_or_else(|| msg::Error::GetEncryptedPartMultipartError) + .and_then(|part| decrypt_part(account, part))?; let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes()) - .context("cannot parse decrypted part of multipart")?; + .map_err(msg::Error::ParseEncryptedPartError)?; build_parts_map_rec(account, &parsed_mail, parts)?; } else { for part in parsed_mail.subparts.iter() { @@ -138,14 +137,14 @@ fn build_parts_map_rec( Ok(()) } -fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result { +fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> msg::Result { let msg_path = env::temp_dir().join(Uuid::new_v4().to_string()); let msg_body = msg .get_body() - .context("cannot get body from encrypted part")?; - fs::write(msg_path.clone(), &msg_body) - .context(format!("cannot write encrypted part to temporary file"))?; - account - .pgp_decrypt_file(msg_path.clone())? - .ok_or_else(|| anyhow!("cannot find pgp decrypt command in config")) + .map_err(msg::Error::GetEncryptedPartBodyError)?; + fs::write(msg_path.clone(), &msg_body).map_err(msg::Error::WriteEncryptedPartBodyError)?; + let content = account + .pgp_decrypt_file(msg_path.clone()) + .map_err(msg::Error::DecryptPartError)?; + Ok(content) } diff --git a/lib/src/msg/tpl.rs b/lib/src/msg/tpl.rs new file mode 100644 index 0000000..b7ba08a --- /dev/null +++ b/lib/src/msg/tpl.rs @@ -0,0 +1,15 @@ +//! Module related to message template CLI. +//! +//! This module provides subcommands, arguments and a command matcher related to message template. + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct TplOverride<'a> { + pub subject: Option<&'a str>, + pub from: Option>, + pub to: Option>, + pub cc: Option>, + pub bcc: Option>, + pub headers: Option>, + pub body: Option<&'a str>, + pub sig: Option<&'a str>, +} diff --git a/lib/src/process.rs b/lib/src/process.rs index 3f19104..36bc526 100644 --- a/lib/src/process.rs +++ b/lib/src/process.rs @@ -1,25 +1,30 @@ -use log::debug; -use std::{io, process::Command, result, string}; +use log::{debug, trace}; +use std::{io, process, result, string}; use thiserror::Error; #[derive(Error, Debug)] -pub enum ProcessError { - #[error("cannot run command")] - RunCmdError(#[from] io::Error), +pub enum Error { + #[error("cannot run command: {1}")] + RunCmdError(#[source] io::Error, String), #[error("cannot parse command output")] - ParseCmdOutputError(#[from] string::FromUtf8Error), + ParseCmdOutputError(#[source] string::FromUtf8Error), } -type Result = result::Result; +pub type Result = result::Result; pub fn run_cmd(cmd: &str) -> Result { - debug!("running command: {}", cmd); + trace!(">> run command"); + debug!("command: {}", cmd); let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(&["/C", cmd]).output() + process::Command::new("cmd").args(&["/C", cmd]).output() } else { - Command::new("sh").arg("-c").arg(cmd).output() - }?; + process::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)?; - Ok(String::from_utf8(output.stdout)?) + debug!("command output: {}", output); + trace!("<< run command"); + Ok(output) } diff --git a/tests/emails/alice-to-patrick-encrypted.eml b/lib/tests/emails/alice-to-patrick-encrypted.eml similarity index 100% rename from tests/emails/alice-to-patrick-encrypted.eml rename to lib/tests/emails/alice-to-patrick-encrypted.eml diff --git a/tests/emails/alice-to-patrick.eml b/lib/tests/emails/alice-to-patrick.eml similarity index 100% rename from tests/emails/alice-to-patrick.eml rename to lib/tests/emails/alice-to-patrick.eml diff --git a/tests/keys/alice.asc b/lib/tests/keys/alice.asc similarity index 100% rename from tests/keys/alice.asc rename to lib/tests/keys/alice.asc diff --git a/tests/keys/alice.pub.asc b/lib/tests/keys/alice.pub.asc similarity index 100% rename from tests/keys/alice.pub.asc rename to lib/tests/keys/alice.pub.asc diff --git a/tests/keys/patrick.asc b/lib/tests/keys/patrick.asc similarity index 100% rename from tests/keys/patrick.asc rename to lib/tests/keys/patrick.asc diff --git a/tests/keys/patrick.pub.asc b/lib/tests/keys/patrick.pub.asc similarity index 100% rename from tests/keys/patrick.pub.asc rename to lib/tests/keys/patrick.pub.asc diff --git a/tests/test_imap_backend.rs b/lib/tests/test_imap_backend.rs similarity index 84% rename from tests/test_imap_backend.rs rename to lib/tests/test_imap_backend.rs index d0d5035..3235822 100644 --- a/tests/test_imap_backend.rs +++ b/lib/tests/test_imap_backend.rs @@ -1,7 +1,7 @@ #[cfg(feature = "imap-backend")] -use himalaya::{ - backends::{Backend, ImapBackend, ImapEnvelopes}, - config::{AccountConfig, ImapBackendConfig}, +use himalaya_lib::{ + account::{AccountConfig, ImapBackendConfig}, + backend::{Backend, ImapBackend}, }; #[cfg(feature = "imap-backend")] @@ -46,7 +46,6 @@ fn test_imap_backend() { // check that the envelope of the added message exists let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); let envelope = envelopes.first().unwrap(); assert_eq!("alice@localhost", envelope.sender); @@ -56,20 +55,16 @@ fn test_imap_backend() { imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) .unwrap(); let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); // check that the message can be moved imap.move_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) .unwrap(); let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(0, envelopes.len()); let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(2, envelopes.len()); let id = envelopes.first().unwrap().id.to_string(); diff --git a/tests/test_maildir_backend.rs b/lib/tests/test_maildir_backend.rs similarity index 66% rename from tests/test_maildir_backend.rs rename to lib/tests/test_maildir_backend.rs index 11eaa52..9a9e22a 100644 --- a/tests/test_maildir_backend.rs +++ b/lib/tests/test_maildir_backend.rs @@ -1,9 +1,10 @@ use maildir::Maildir; use std::{collections::HashMap, env, fs, iter::FromIterator}; -use himalaya::{ - backends::{Backend, MaildirBackend, MaildirEnvelopes, MaildirFlag}, - config::{AccountConfig, MaildirBackendConfig}, +use himalaya_lib::{ + account::{AccountConfig, MaildirBackendConfig}, + backend::{Backend, MaildirBackend}, + msg::Flag, }; #[test] @@ -33,7 +34,7 @@ fn test_maildir_backend() { // check that a message can be added let msg = include_bytes!("./emails/alice-to-patrick.eml"); - let hash = mdir.add_msg("inbox", msg, "seen").unwrap().to_string(); + let hash = mdir.add_msg("inbox", msg, "seen").unwrap(); // check that the added message exists let msg = mdir.get_msg("inbox", &hash).unwrap(); @@ -43,48 +44,42 @@ fn test_maildir_backend() { // check that the envelope of the added message exists let envelopes = mdir.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert_eq!(1, envelopes.len()); assert_eq!("alice@localhost", envelope.sender); assert_eq!("Plain message", envelope.subject); // check that a flag can be added to the message - mdir.add_flags("inbox", &envelope.hash, "flagged passed") - .unwrap(); + mdir.add_flags("inbox", &envelope.id, "flagged").unwrap(); let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&MaildirFlag::Seen)); - assert!(envelope.flags.contains(&MaildirFlag::Flagged)); - assert!(envelope.flags.contains(&MaildirFlag::Passed)); + assert!(envelope.flags.contains(&Flag::Seen)); + assert!(envelope.flags.contains(&Flag::Flagged)); // check that the message flags can be changed - mdir.set_flags("inbox", &envelope.hash, "passed").unwrap(); + mdir.set_flags("inbox", &envelope.id, "answered").unwrap(); let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(!envelope.flags.contains(&MaildirFlag::Seen)); - assert!(!envelope.flags.contains(&MaildirFlag::Flagged)); - assert!(envelope.flags.contains(&MaildirFlag::Passed)); + assert!(!envelope.flags.contains(&Flag::Seen)); + assert!(!envelope.flags.contains(&Flag::Flagged)); + assert!(envelope.flags.contains(&Flag::Answered)); // check that a flag can be removed from the message - mdir.del_flags("inbox", &envelope.hash, "passed").unwrap(); + mdir.del_flags("inbox", &envelope.id, "answered").unwrap(); let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(!envelope.flags.contains(&MaildirFlag::Seen)); - assert!(!envelope.flags.contains(&MaildirFlag::Flagged)); - assert!(!envelope.flags.contains(&MaildirFlag::Passed)); + assert!(!envelope.flags.contains(&Flag::Seen)); + assert!(!envelope.flags.contains(&Flag::Flagged)); + assert!(!envelope.flags.contains(&Flag::Answered)); // check that the message can be copied - mdir.copy_msg("inbox", "subdir", &envelope.hash).unwrap(); + mdir.copy_msg("inbox", "subdir", &envelope.id).unwrap(); assert!(mdir.get_msg("inbox", &hash).is_ok()); assert!(mdir.get_msg("subdir", &hash).is_ok()); assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); // check that the message can be moved - mdir.move_msg("inbox", "subdir", &envelope.hash).unwrap(); + mdir.move_msg("inbox", "subdir", &envelope.id).unwrap(); assert!(mdir.get_msg("inbox", &hash).is_err()); assert!(mdir.get_msg("subdir", &hash).is_ok()); assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); diff --git a/tests/test_notmuch_backend.rs b/lib/tests/test_notmuch_backend.rs similarity index 64% rename from tests/test_notmuch_backend.rs rename to lib/tests/test_notmuch_backend.rs index dbd44c5..1a886e7 100644 --- a/tests/test_notmuch_backend.rs +++ b/lib/tests/test_notmuch_backend.rs @@ -1,17 +1,17 @@ #[cfg(feature = "notmuch-backend")] use std::{collections::HashMap, env, fs, iter::FromIterator}; - #[cfg(feature = "notmuch-backend")] -use himalaya::{ - backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes}, +use himalaya_lib::{ + account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}, + backend::{Backend, MaildirBackend, NotmuchBackend}, }; -#[cfg(feature = "notmuch-backend")] -use himalaya_lib::account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig} #[cfg(feature = "notmuch-backend")] #[test] fn test_notmuch_backend() { + use himalaya_lib::msg::Flag; + // set up maildir folders and notmuch database let mdir: maildir::Maildir = env::temp_dir().join("himalaya-test-notmuch").into(); if let Err(_) = fs::remove_dir_all(mdir.path()) {} @@ -44,7 +44,6 @@ fn test_notmuch_backend() { // check that the envelope of the added message exists let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert_eq!(1, envelopes.len()); assert_eq!("alice@localhost", envelope.sender); @@ -52,37 +51,34 @@ fn test_notmuch_backend() { // check that a flag can be added to the message notmuch - .add_flags("", &envelope.hash, "flagged passed") + .add_flags("", &envelope.id, "flagged answered") .unwrap(); let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&"inbox".into())); - assert!(envelope.flags.contains(&"seen".into())); - assert!(envelope.flags.contains(&"flagged".into())); - assert!(envelope.flags.contains(&"passed".into())); + assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); + assert!(envelope.flags.contains(&Flag::Custom("seen".into()))); + assert!(envelope.flags.contains(&Flag::Custom("flagged".into()))); + assert!(envelope.flags.contains(&Flag::Custom("answered".into()))); // check that the message flags can be changed notmuch - .set_flags("", &envelope.hash, "inbox passed") + .set_flags("", &envelope.id, "inbox answered") .unwrap(); let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&"inbox".into())); - assert!(!envelope.flags.contains(&"seen".into())); - assert!(!envelope.flags.contains(&"flagged".into())); - assert!(envelope.flags.contains(&"passed".into())); + assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("seen".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("flagged".into()))); + assert!(envelope.flags.contains(&Flag::Custom("answered".into()))); // check that a flag can be removed from the message - notmuch.del_flags("", &envelope.hash, "passed").unwrap(); + notmuch.del_flags("", &envelope.id, "answered").unwrap(); let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&"inbox".into())); - assert!(!envelope.flags.contains(&"seen".into())); - assert!(!envelope.flags.contains(&"flagged".into())); - assert!(!envelope.flags.contains(&"passed".into())); + assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("seen".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("flagged".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("answered".into()))); // check that the message can be deleted notmuch.del_msg("", &hash).unwrap();