diff --git a/.gitignore b/.gitignore index 2d69402..e4a6d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,46 @@ # Cargo build directory -/target +target/ +debug/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb # Nix build directory -/result -/result-lib +result +result-* # Direnv /.envrc /.direnv + + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea/ + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +## Others +.metadata/ diff --git a/Cargo.lock b/Cargo.lock index ad142eb..adda362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "email-encoding" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b91dddc343e7eaa27f9764e5bffe57370d957017fdd75244f5045e829a8441" +dependencies = [ + "base64", + "memchr 2.4.1", +] + +[[package]] +name = "email_address" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8684b7c9cb4857dfa1e5b9629ef584ba618c9b93bae60f58cb23f4f271d0468e" + [[package]] name = "encoding_rs" version = "0.8.30" @@ -441,6 +457,7 @@ dependencies = [ "convert_case", "env_logger", "erased-serde", + "himalaya-lib", "html-escape", "imap", "imap-proto", @@ -468,6 +485,29 @@ dependencies = [ [[package]] name = "himalaya-lib" version = "0.1.0" +dependencies = [ + "ammonia", + "chrono", + "convert_case", + "html-escape", + "imap", + "imap-proto", + "lettre", + "log", + "maildir", + "mailparse", + "md5", + "native-tls", + "notmuch", + "regex", + "rfc2047-decoder", + "serde", + "shellexpand", + "thiserror", + "toml", + "tree_magic", + "uuid", +] [[package]] name = "hostname" @@ -590,11 +630,13 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lettre" -version = "0.10.0-rc.4" +version = "0.10.0-rc.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d8da8f34d086b081c9cc3b57d3bb3b51d16fc06b5c848a188e2f14d58ac2a5" +checksum = "0f7e87d9d44162eea7abd87b1a7540fcb10d5e58e8bb4f173178f3dc6e453944" dependencies = [ "base64", + "email-encoding", + "email_address", "fastrand", "futures-util", "hostname", @@ -605,8 +647,8 @@ dependencies = [ "nom 7.1.1", "once_cell", "quoted_printable", - "regex", "serde", + "socket2", ] [[package]] @@ -1257,6 +1299,16 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "string_cache" version = "0.8.3" @@ -1365,6 +1417,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.1.44" 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/README.md b/README.md index b7b71dc..6cf38bd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +*📢 Announcement: Himalaya receives support help, see the +[discussion](https://github.com/soywod/himalaya/discussions/399) for +more details.* + # 📫 Himalaya Command-line interface for email management diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000..03314f7 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6cda92b..25b638c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -4,8 +4,8 @@ description = "Command-line interface for email management" version = "0.5.10" authors = ["soywod "] edition = "2018" -license-file = "LICENSE" -readme = "README.md" +license-file = "../LICENSE" +readme = "../README.md" categories = ["command-line-interface", "command-line-utilities", "email"] keywords = ["cli", "mail", "email", "client", "imap"] homepage = "https://github.com/soywod/himalaya/wiki" @@ -31,8 +31,9 @@ clap = { version = "2.33.3", default-features = false, features = ["suggestions" convert_case = "0.5.0" env_logger = "0.8.3" erased-serde = "0.3.18" +himalaya-lib = { path = "../lib" } html-escape = "0.2.9" -lettre = { version = "0.10.0-rc.1", features = ["serde"] } +lettre = { version = "0.10.0-rc.7", features = ["serde"] } log = "0.4.14" mailparse = "0.13.6" native-tls = "0.2.8" diff --git a/cli/src/backends/imap/imap_envelope.rs b/cli/src/backends/imap/imap_envelope.rs deleted file mode 100644 index d087eb1..0000000 --- a/cli/src/backends/imap/imap_envelope.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! IMAP envelope module. -//! -//! This module provides IMAP types and conversion utilities related -//! to the envelope. - -use anyhow::{anyhow, Context, Error, Result}; -use std::{convert::TryFrom, ops::Deref}; - -use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -use super::{ImapFlag, ImapFlags}; - -/// Represents a list of IMAP envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct ImapEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for ImapEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl PrintTable for ImapEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -// impl Envelopes for ImapEnvelopes { -// // -// } - -/// Represents the IMAP envelope. The envelope is just a message -/// subset, and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct ImapEnvelope { - /// Represents the sequence number of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2 - pub id: u32, - - /// Represents the flags attached to the message. - pub flags: ImapFlags, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the internal date of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3 - pub date: Option, -} - -impl Table for ImapEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("ID").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let id = self.id.to_string(); - let flags = self.flags.to_symbols_string(); - let unseen = !self.flags.contains(&ImapFlag::Seen); - let subject = &self.subject; - let sender = &self.sender; - let date = self.date.as_deref().unwrap_or_default(); - Row::new() - .cell(Cell::new(id).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopes returned by the `imap` crate. -pub type RawImapEnvelopes = imap::types::ZeroCopy>; - -impl TryFrom for ImapEnvelopes { - type Error = Error; - - fn try_from(raw_envelopes: RawImapEnvelopes) -> Result { - let mut envelopes = vec![]; - for raw_envelope in raw_envelopes.iter().rev() { - envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?); - } - Ok(Self { envelopes }) - } -} - -/// Represents the raw envelope returned by the `imap` crate. -pub type RawImapEnvelope = imap::types::Fetch; - -impl TryFrom<&RawImapEnvelope> for ImapEnvelope { - type Error = Error; - - fn try_from(fetch: &RawImapEnvelope) -> Result { - let envelope = fetch - .envelope() - .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?; - - // Get the sequence number - let id = fetch.message; - - // Get the flags - let flags = ImapFlags::try_from(fetch.flags())?; - - // Get the subject - let subject = envelope - .subject - .as_ref() - .map(|subj| { - rfc2047_decoder::decode(subj).context(format!( - "cannot decode subject of message {}", - fetch.message - )) - }) - .unwrap_or_else(|| Ok(String::default()))?; - - // Get the sender - let sender = envelope - .sender - .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))?; - 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, - ))? - } else { - let mbox = sender - .mailbox - .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) - .and_then(|mbox| { - rfc2047_decoder::decode(&mbox.to_vec()).context(format!( - "cannot decode sender's mailbox of message {}", - fetch.message, - )) - })?; - let host = sender - .host - .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) - .and_then(|host| { - rfc2047_decoder::decode(&host.to_vec()).context(format!( - "cannot decode sender's host of message {}", - fetch.message, - )) - })?; - format!("{}@{}", mbox, host) - }; - - // Get the internal date - let date = fetch - .internal_date() - .map(|date| date.naive_local().to_string()); - - Ok(Self { - id, - flags, - subject, - sender, - date, - }) - } -} diff --git a/cli/src/backends/imap/imap_flag.rs b/cli/src/backends/imap/imap_flag.rs deleted file mode 100644 index 87bb8b9..0000000 --- a/cli/src/backends/imap/imap_flag.rs +++ /dev/null @@ -1,151 +0,0 @@ -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - fmt, - ops::Deref, -}; - -/// Represents the imap flag variants. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub enum ImapFlag { - Seen, - Answered, - Flagged, - Deleted, - Draft, - Recent, - MayCreate, - Custom(String), -} - -impl From<&str> for ImapFlag { - fn from(flag_str: &str) -> Self { - match flag_str { - "seen" => ImapFlag::Seen, - "answered" => ImapFlag::Answered, - "flagged" => ImapFlag::Flagged, - "deleted" => ImapFlag::Deleted, - "draft" => ImapFlag::Draft, - "recent" => ImapFlag::Recent, - "maycreate" | "may-create" => ImapFlag::MayCreate, - flag_str => ImapFlag::Custom(flag_str.into()), - } - } -} - -impl TryFrom<&imap::types::Flag<'_>> for ImapFlag { - type Error = Error; - - fn try_from(flag: &imap::types::Flag<'_>) -> Result { - Ok(match flag { - imap::types::Flag::Seen => ImapFlag::Seen, - imap::types::Flag::Answered => ImapFlag::Answered, - imap::types::Flag::Flagged => ImapFlag::Flagged, - imap::types::Flag::Deleted => ImapFlag::Deleted, - imap::types::Flag::Draft => ImapFlag::Draft, - imap::types::Flag::Recent => ImapFlag::Recent, - imap::types::Flag::MayCreate => ImapFlag::MayCreate, - imap::types::Flag::Custom(custom) => ImapFlag::Custom(custom.to_string()), - _ => return Err(anyhow!("cannot parse imap flag")), - }) - } -} - -/// Represents the imap flags. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)] -pub struct ImapFlags(pub Vec); - -impl ImapFlags { - /// Builds a symbols string - pub fn to_symbols_string(&self) -> String { - let mut flags = String::new(); - flags.push_str(if self.contains(&ImapFlag::Seen) { - " " - } else { - "✷" - }); - flags.push_str(if self.contains(&ImapFlag::Answered) { - "↵" - } else { - " " - }); - flags.push_str(if self.contains(&ImapFlag::Flagged) { - "⚑" - } else { - " " - }); - flags - } -} - -impl Deref for ImapFlags { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for ImapFlags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut glue = ""; - - for flag in &self.0 { - write!(f, "{}", glue)?; - match flag { - ImapFlag::Seen => write!(f, "\\Seen")?, - ImapFlag::Answered => write!(f, "\\Answered")?, - ImapFlag::Flagged => write!(f, "\\Flagged")?, - ImapFlag::Deleted => write!(f, "\\Deleted")?, - ImapFlag::Draft => write!(f, "\\Draft")?, - ImapFlag::Recent => write!(f, "\\Recent")?, - ImapFlag::MayCreate => write!(f, "\\MayCreate")?, - ImapFlag::Custom(custom) => write!(f, "{}", custom)?, - } - glue = " "; - } - - Ok(()) - } -} - -impl<'a> Into>> for ImapFlags { - fn into(self) -> Vec> { - self.0 - .into_iter() - .map(|flag| match flag { - ImapFlag::Seen => imap::types::Flag::Seen, - ImapFlag::Answered => imap::types::Flag::Answered, - ImapFlag::Flagged => imap::types::Flag::Flagged, - ImapFlag::Deleted => imap::types::Flag::Deleted, - ImapFlag::Draft => imap::types::Flag::Draft, - ImapFlag::Recent => imap::types::Flag::Recent, - ImapFlag::MayCreate => imap::types::Flag::MayCreate, - ImapFlag::Custom(custom) => imap::types::Flag::Custom(custom.into()), - }) - .collect() - } -} - -impl From<&str> for ImapFlags { - fn from(flags_str: &str) -> Self { - ImapFlags( - flags_str - .split_whitespace() - .map(|flag_str| flag_str.trim().into()) - .collect(), - ) - } -} - -impl TryFrom<&[imap::types::Flag<'_>]> for ImapFlags { - type Error = Error; - - fn try_from(flags: &[imap::types::Flag<'_>]) -> Result { - let mut f = vec![]; - for flag in flags { - f.push(flag.try_into()?); - } - Ok(Self(f)) - } -} diff --git a/cli/src/backends/imap/imap_mbox.rs b/cli/src/backends/imap/imap_mbox.rs deleted file mode 100644 index 223ab6c..0000000 --- a/cli/src/backends/imap/imap_mbox.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! IMAP mailbox module. -//! -//! This module provides IMAP types and conversion utilities related -//! to the mailbox. - -use anyhow::Result; -use serde::Serialize; -use std::fmt::{self, Display}; -use std::ops::Deref; - -use crate::mbox::Mboxes; -use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -use super::ImapMboxAttrs; - -/// Represents a list of IMAP mailboxes. -#[derive(Debug, Default, Serialize)] -pub struct ImapMboxes { - #[serde(rename = "response")] - pub mboxes: Vec, -} - -impl Deref for ImapMboxes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.mboxes - } -} - -impl PrintTable for ImapMboxes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -impl Mboxes for ImapMboxes { - // -} - -/// Represents the IMAP mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct ImapMbox { - /// Represents the mailbox hierarchie delimiter. - pub delim: String, - - /// Represents the mailbox name. - pub name: String, - - /// Represents the mailbox attributes. - pub attrs: ImapMboxAttrs, -} - -impl ImapMbox { - pub fn new(name: &str) -> Self { - Self { - name: name.into(), - ..Self::default() - } - } -} - -impl Display for ImapMbox { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for ImapMbox { - fn head() -> Row { - Row::new() - .cell(Cell::new("DELIM").bold().underline().white()) - .cell(Cell::new("NAME").bold().underline().white()) - .cell( - Cell::new("ATTRIBUTES") - .shrinkable() - .bold() - .underline() - .white(), - ) - } - - fn row(&self) -> Row { - Row::new() - .cell(Cell::new(&self.delim).white()) - .cell(Cell::new(&self.name).green()) - .cell(Cell::new(&self.attrs.to_string()).shrinkable().blue()) - } -} - -#[cfg(test)] -mod tests { - use crate::backends::ImapMboxAttr; - - use super::*; - - #[test] - fn it_should_create_new_mbox() { - assert_eq!(ImapMbox::default(), ImapMbox::new("")); - assert_eq!( - ImapMbox { - name: "INBOX".into(), - ..ImapMbox::default() - }, - ImapMbox::new("INBOX") - ); - } - - #[test] - fn it_should_display_mbox() { - let default_mbox = ImapMbox::default(); - assert_eq!("", default_mbox.to_string()); - - let new_mbox = ImapMbox::new("INBOX"); - assert_eq!("INBOX", new_mbox.to_string()); - - let full_mbox = ImapMbox { - delim: ".".into(), - name: "Sent".into(), - attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]), - }; - assert_eq!("Sent", full_mbox.to_string()); - } -} - -/// Represents a list of raw mailboxes returned by the `imap` crate. -pub type RawImapMboxes = imap::types::ZeroCopy>; - -impl<'a> From for ImapMboxes { - fn from(raw_mboxes: RawImapMboxes) -> Self { - Self { - mboxes: raw_mboxes.iter().map(ImapMbox::from).collect(), - } - } -} - -/// Represents the raw mailbox returned by the `imap` crate. -pub type RawImapMbox = imap::types::Name; - -impl<'a> From<&'a RawImapMbox> for ImapMbox { - fn from(raw_mbox: &'a RawImapMbox) -> Self { - Self { - delim: raw_mbox.delimiter().unwrap_or_default().into(), - name: raw_mbox.name().into(), - attrs: raw_mbox.attributes().into(), - } - } -} diff --git a/cli/src/backends/imap/imap_mbox_attr.rs b/cli/src/backends/imap/imap_mbox_attr.rs deleted file mode 100644 index 208d067..0000000 --- a/cli/src/backends/imap/imap_mbox_attr.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! IMAP mailbox attribute module. -//! -//! This module provides IMAP types and conversion utilities related -//! to the mailbox attribute. - -/// Represents the raw mailbox attribute returned by the `imap` crate. -pub use imap::types::NameAttribute as RawImapMboxAttr; -use std::{ - fmt::{self, Display}, - ops::Deref, -}; - -/// Represents the attributes of the mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct ImapMboxAttrs(pub Vec); - -impl Deref for ImapMboxAttrs { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for ImapMboxAttrs { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut glue = ""; - for attr in self.iter() { - write!(f, "{}{}", glue, attr)?; - glue = ", "; - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] -pub enum ImapMboxAttr { - NoInferiors, - NoSelect, - Marked, - Unmarked, - Custom(String), -} - -/// Makes the attribute displayable. -impl Display for ImapMboxAttr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ImapMboxAttr::NoInferiors => write!(f, "NoInferiors"), - ImapMboxAttr::NoSelect => write!(f, "NoSelect"), - ImapMboxAttr::Marked => write!(f, "Marked"), - ImapMboxAttr::Unmarked => write!(f, "Unmarked"), - ImapMboxAttr::Custom(custom) => write!(f, "{}", custom), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_should_display_attrs() { - macro_rules! attrs_from { - ($($attr:expr),*) => { - ImapMboxAttrs(vec![$($attr,)*]).to_string() - }; - } - - let empty_attr = attrs_from![]; - let single_attr = attrs_from![ImapMboxAttr::NoInferiors]; - let multiple_attrs = attrs_from![ - ImapMboxAttr::Custom("AttrCustom".into()), - ImapMboxAttr::NoInferiors - ]; - - assert_eq!("", empty_attr); - assert_eq!("NoInferiors", single_attr); - assert!(multiple_attrs.contains("NoInferiors")); - assert!(multiple_attrs.contains("AttrCustom")); - assert!(multiple_attrs.contains(",")); - } - - #[test] - fn it_should_display_attr() { - macro_rules! attr_from { - ($attr:ident) => { - ImapMboxAttr::$attr.to_string() - }; - ($custom:literal) => { - ImapMboxAttr::Custom($custom.into()).to_string() - }; - } - - assert_eq!("NoInferiors", attr_from![NoInferiors]); - assert_eq!("NoSelect", attr_from![NoSelect]); - assert_eq!("Marked", attr_from![Marked]); - assert_eq!("Unmarked", attr_from![Unmarked]); - assert_eq!("CustomAttr", attr_from!["CustomAttr"]); - } -} - -impl<'a> From<&'a [RawImapMboxAttr<'a>]> for ImapMboxAttrs { - fn from(raw_attrs: &'a [RawImapMboxAttr<'a>]) -> Self { - Self(raw_attrs.iter().map(ImapMboxAttr::from).collect()) - } -} - -impl<'a> From<&'a RawImapMboxAttr<'a>> for ImapMboxAttr { - fn from(attr: &'a RawImapMboxAttr<'a>) -> Self { - match attr { - RawImapMboxAttr::NoInferiors => Self::NoInferiors, - RawImapMboxAttr::NoSelect => Self::NoSelect, - RawImapMboxAttr::Marked => Self::Marked, - RawImapMboxAttr::Unmarked => Self::Unmarked, - RawImapMboxAttr::Custom(cow) => Self::Custom(cow.to_string()), - } - } -} diff --git a/cli/src/backends/maildir/maildir_backend.rs b/cli/src/backends/maildir/maildir_backend.rs deleted file mode 100644 index a2e85a5..0000000 --- a/cli/src/backends/maildir/maildir_backend.rs +++ /dev/null @@ -1,493 +0,0 @@ -//! Maildir backend module. -//! -//! This module contains the definition of the maildir backend and its -//! traits implementation. - -use anyhow::{anyhow, Context, Result}; -use log::{debug, info, trace}; -use std::{convert::TryInto, env, fs, path::PathBuf}; - -use crate::{ - backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, - config::{AccountConfig, MaildirBackendConfig}, - mbox::Mboxes, - msg::{Envelopes, Msg}, -}; - -/// Represents the maildir backend. -pub struct MaildirBackend<'a> { - account_config: &'a AccountConfig, - mdir: maildir::Maildir, -} - -impl<'a> MaildirBackend<'a> { - pub fn new( - account_config: &'a AccountConfig, - maildir_config: &'a MaildirBackendConfig, - ) -> Self { - Self { - account_config, - mdir: maildir_config.maildir_dir.clone().into(), - } - } - - fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result { - if mdir_path.is_dir() { - Ok(mdir_path) - } else { - Err(anyhow!("cannot read maildir directory {:?}", mdir_path)) - } - } - - /// Creates a maildir instance from a string slice. - pub fn get_mdir_from_dir(&self, dir: &str) -> Result { - let dir = self.account_config.get_mbox_alias(dir)?; - - // If the dir points to the inbox folder, creates a maildir - // instance from the root folder. - if &dir == "inbox" { - return self - .validate_mdir_path(self.mdir.path().to_owned()) - .map(maildir::Maildir::from); - } - - // If the dir is a valid maildir path, creates a maildir - // instance from it. First checks for absolute path, - self.validate_mdir_path((&dir).into()) - // 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(|_| { - // Otherwise creates a maildir instance from a maildir - // subdirectory by adding a "." in front of the name - // as described in the [spec]. - // - // [spec]: http://www.courier-mta.org/imap/README.maildirquota.html - self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir))) - }) - .map(maildir::Maildir::from) - } -} - -impl<'a> Backend<'a> for MaildirBackend<'a> { - fn add_mbox(&mut self, subdir: &str) -> Result<()> { - info!(">> add maildir subdir"); - debug!("subdir: {:?}", subdir); - - let path = self.mdir.path().join(format!(".{}", subdir)); - trace!("subdir path: {:?}", path); - - fs::create_dir(&path) - .with_context(|| format!("cannot create maildir subdir {:?} at {:?}", subdir, path))?; - - info!("<< add maildir subdir"); - Ok(()) - } - - fn get_mboxes(&mut self) -> Result> { - info!(">> get maildir dirs"); - - let dirs: MaildirMboxes = - self.mdir.list_subdirs().try_into().with_context(|| { - format!("cannot parse maildir dirs from {:?}", self.mdir.path()) - })?; - trace!("dirs: {:?}", dirs); - - info!("<< get maildir dirs"); - Ok(Box::new(dirs)) - } - - fn del_mbox(&mut self, dir: &str) -> Result<()> { - info!(">> delete maildir dir"); - debug!("dir: {:?}", dir); - - let path = self.mdir.path().join(format!(".{}", dir)); - trace!("dir path: {:?}", path); - - fs::remove_dir_all(&path) - .with_context(|| format!("cannot delete maildir {:?} from {:?}", dir, path))?; - - info!("<< delete maildir dir"); - Ok(()) - } - - fn get_envelopes( - &mut self, - dir: &str, - page_size: usize, - page: usize, - ) -> Result> { - info!(">> get maildir envelopes"); - debug!("dir: {:?}", dir); - 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))?; - - // Reads envelopes from the "cur" folder of the selected - // maildir. - let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().with_context(|| { - format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) - })?; - debug!("envelopes len: {:?}", envelopes.len()); - trace!("envelopes: {:?}", envelopes); - - // Calculates pagination boundaries. - 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, - )); - } - let page_end = envelopes.len().min(page_begin + page_size); - debug!("page end: {:?}", page_end); - - // Sorts envelopes by most recent date. - envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); - - // Applies pagination boundaries. - envelopes.envelopes = envelopes[page_begin..page_end].to_owned(); - - // Appends envelopes hash to the id mapper cache file and - // calculates the new short hash length. The short hash length - // represents the minimum hash length possible to avoid - // conflicts. - let short_hash_len = { - let mut mapper = IdMapper::new(mdir.path())?; - let entries = envelopes - .iter() - .map(|env| (env.hash.to_owned(), env.id.to_owned())) - .collect(); - mapper.append(entries)? - }; - debug!("short hash length: {:?}", short_hash_len); - - // Shorten envelopes hash. - envelopes - .iter_mut() - .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); - - info!("<< get maildir envelopes"); - Ok(Box::new(envelopes)) - } - - fn search_envelopes( - &mut self, - _dir: &str, - _query: &str, - _sort: &str, - _page_size: usize, - _page: usize, - ) -> Result> { - info!(">> search maildir envelopes"); - info!("<< search maildir envelopes"); - Err(anyhow!( - "cannot find maildir envelopes: feature not implemented" - )) - } - - fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result> { - info!(">> add maildir message"); - 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: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; - let id = mdir - .store_cur_with_flags(msg, &flags.to_string()) - .with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?; - 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 - ) - })?; - - info!("<< add maildir message"); - Ok(Box::new(hash)) - } - - fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result { - info!(">> get maildir message"); - 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() - ) - })?; - 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()) - })?; - trace!("message: {:?}", msg); - - info!("<< get maildir message"); - Ok(msg) - } - - fn copy_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { - info!(">> copy maildir message"); - 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() - ) - })?; - 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() - ) - })?; - - // 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 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 - ) - })?; - - info!("<< copy maildir message"); - Ok(()) - } - - fn move_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { - info!(">> move maildir message"); - 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() - ) - })?; - 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() - ) - })?; - - // 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 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 - ) - })?; - - info!("<< move maildir message"); - Ok(()) - } - - fn del_msg(&mut self, dir: &str, short_hash: &str) -> Result<()> { - info!(">> delete maildir message"); - 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() - ) - })?; - debug!("id: {:?}", id); - mdir.delete(&id).with_context(|| { - format!( - "cannot delete message {:?} from maildir {:?}", - id, - mdir.path() - ) - })?; - - info!("<< delete maildir message"); - Ok(()) - } - - fn add_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { - info!(">> add maildir message flags"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - debug!("flags: {:?}", flags); - - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; - 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() - ) - })?; - debug!("id: {:?}", id); - mdir.add_flags(&id, &flags.to_string()) - .with_context(|| format!("cannot add flags {:?} to maildir message {:?}", flags, id))?; - - info!("<< add maildir message flags"); - Ok(()) - } - - fn set_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { - info!(">> set maildir message flags"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - debug!("flags: {:?}", flags); - - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; - 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() - ) - })?; - debug!("id: {:?}", id); - mdir.set_flags(&id, &flags.to_string()) - .with_context(|| format!("cannot set flags {:?} to maildir message {:?}", flags, id))?; - - info!("<< set maildir message flags"); - Ok(()) - } - - fn del_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { - info!(">> delete maildir message flags"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - debug!("flags: {:?}", flags); - - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; - 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() - ) - })?; - debug!("id: {:?}", id); - mdir.remove_flags(&id, &flags.to_string()) - .with_context(|| { - format!( - "cannot delete flags {:?} to maildir message {:?}", - flags, id - ) - })?; - - info!("<< delete maildir message flags"); - Ok(()) - } -} diff --git a/cli/src/backends/maildir/maildir_envelope.rs b/cli/src/backends/maildir/maildir_envelope.rs deleted file mode 100644 index 10cb3be..0000000 --- a/cli/src/backends/maildir/maildir_envelope.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Maildir mailbox module. -//! -//! This module provides Maildir types and conversion utilities -//! related to the envelope - -use anyhow::{anyhow, Context, Error, Result}; -use chrono::DateTime; -use log::trace; -use std::{ - convert::{TryFrom, TryInto}, - ops::{Deref, DerefMut}, -}; - -use crate::{ - backends::{MaildirFlag, MaildirFlags}, - msg::{from_slice_to_addrs, Addr}, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct MaildirEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for MaildirEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl DerefMut for MaildirEnvelopes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.envelopes - } -} - -impl PrintTable for MaildirEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -// impl Envelopes for MaildirEnvelopes { -// // -// } - -/// Represents the envelope. The envelope is just a message subset, -/// and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct MaildirEnvelope { - /// Represents the id of the message. - pub id: String, - - /// Represents the MD5 hash of the message id. - pub hash: String, - - /// Represents the flags of the message. - pub flags: MaildirFlags, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the date of the message. - pub date: String, -} - -impl Table for MaildirEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("HASH").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let hash = self.hash.clone(); - let unseen = !self.flags.contains(&MaildirFlag::Seen); - let flags = self.flags.to_symbols_string(); - let subject = &self.subject; - let sender = &self.sender; - let date = &self.date; - Row::new() - .cell(Cell::new(hash).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopees returned by the `maildir` crate. -pub type RawMaildirEnvelopes = maildir::MailEntries; - -impl<'a> TryFrom for MaildirEnvelopes { - type Error = Error; - - fn try_from(mail_entries: RawMaildirEnvelopes) -> Result { - let mut envelopes = vec![]; - for entry in mail_entries { - let envelope: MaildirEnvelope = entry - .context("cannot decode maildir mail entry")? - .try_into() - .context("cannot parse maildir mail entry")?; - envelopes.push(envelope); - } - - Ok(MaildirEnvelopes { envelopes }) - } -} - -/// Represents the raw envelope returned by the `maildir` crate. -pub type RawMaildirEnvelope = maildir::MailEntry; - -impl<'a> TryFrom for MaildirEnvelope { - type Error = Error; - - fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result { - trace!(">> build envelope from maildir parsed mail"); - - let mut envelope = Self::default(); - - envelope.id = mail_entry.id().into(); - envelope.hash = format!("{:x}", md5::compute(&envelope.id)); - envelope.flags = (&mail_entry) - .try_into() - .context("cannot parse maildir flags")?; - - let parsed_mail = mail_entry - .parsed() - .context("cannot parse maildir mail entry")?; - - trace!(">> parse headers"); - for h in parsed_mail.get_headers() { - let k = h.get_key(); - trace!("header key: {:?}", k); - - let v = rfc2047_decoder::decode(h.get_value_raw()) - .context(format!("cannot decode value from header {:?}", k))?; - trace!("header value: {:?}", v); - - match k.to_lowercase().as_str() { - "date" => { - envelope.date = - DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0) - .context(format!("cannot parse maildir message date {:?}", v))? - .naive_local() - .to_string(); - } - "subject" => { - envelope.subject = v.into(); - } - "from" => { - envelope.sender = from_slice_to_addrs(v) - .context(format!("cannot parse header {:?}", k))? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => { - group_name.to_owned() - } - }) - .ok_or_else(|| anyhow!("cannot find sender"))?; - } - _ => (), - } - } - trace!("<< parse headers"); - - trace!("envelope: {:?}", envelope); - trace!("<< build envelope from maildir parsed mail"); - Ok(envelope) - } -} diff --git a/cli/src/backends/maildir/maildir_flag.rs b/cli/src/backends/maildir/maildir_flag.rs deleted file mode 100644 index 1c97cce..0000000 --- a/cli/src/backends/maildir/maildir_flag.rs +++ /dev/null @@ -1,129 +0,0 @@ -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - ops::Deref, -}; - -/// Represents the maildir flag variants. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub enum MaildirFlag { - Passed, - Replied, - Seen, - Trashed, - Draft, - Flagged, - Custom(char), -} - -/// Represents the maildir flags. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)] -pub struct MaildirFlags(pub Vec); - -impl MaildirFlags { - /// Builds a symbols string - pub fn to_symbols_string(&self) -> String { - let mut flags = String::new(); - flags.push_str(if self.contains(&MaildirFlag::Seen) { - " " - } else { - "✷" - }); - flags.push_str(if self.contains(&MaildirFlag::Replied) { - "↵" - } else { - " " - }); - flags.push_str(if self.contains(&MaildirFlag::Passed) { - "↗" - } else { - " " - }); - flags.push_str(if self.contains(&MaildirFlag::Flagged) { - "⚑" - } else { - " " - }); - flags - } -} - -impl Deref for MaildirFlags { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ToString for MaildirFlags { - fn to_string(&self) -> String { - self.0 - .iter() - .map(|flag| { - let flag_char: char = flag.into(); - flag_char - }) - .collect() - } -} - -impl TryFrom<&str> for MaildirFlags { - type Error = Error; - - fn try_from(flags_str: &str) -> Result { - let mut flags = vec![]; - for flag_str in flags_str.split_whitespace() { - flags.push(flag_str.trim().try_into()?); - } - Ok(MaildirFlags(flags)) - } -} - -impl From<&maildir::MailEntry> for MaildirFlags { - fn from(mail_entry: &maildir::MailEntry) -> Self { - let mut flags = vec![]; - for c in mail_entry.flags().chars() { - flags.push(match c { - 'P' => MaildirFlag::Passed, - 'R' => MaildirFlag::Replied, - 'S' => MaildirFlag::Seen, - 'T' => MaildirFlag::Trashed, - 'D' => MaildirFlag::Draft, - 'F' => MaildirFlag::Flagged, - custom => MaildirFlag::Custom(custom), - }) - } - Self(flags) - } -} - -impl Into for &MaildirFlag { - fn into(self) -> char { - match self { - MaildirFlag::Passed => 'P', - MaildirFlag::Replied => 'R', - MaildirFlag::Seen => 'S', - MaildirFlag::Trashed => 'T', - MaildirFlag::Draft => 'D', - MaildirFlag::Flagged => 'F', - MaildirFlag::Custom(custom) => *custom, - } - } -} - -impl TryFrom<&str> for MaildirFlag { - type Error = Error; - - fn try_from(flag_str: &str) -> Result { - match flag_str { - "passed" => Ok(MaildirFlag::Passed), - "replied" => Ok(MaildirFlag::Replied), - "seen" => Ok(MaildirFlag::Seen), - "trashed" => Ok(MaildirFlag::Trashed), - "draft" => Ok(MaildirFlag::Draft), - "flagged" => Ok(MaildirFlag::Flagged), - flag_str => Err(anyhow!("cannot parse maildir flag {:?}", flag_str)), - } - } -} diff --git a/cli/src/backends/maildir/maildir_mbox.rs b/cli/src/backends/maildir/maildir_mbox.rs deleted file mode 100644 index 3f2ec2f..0000000 --- a/cli/src/backends/maildir/maildir_mbox.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Maildir mailbox module. -//! -//! This module provides Maildir types and conversion utilities -//! related to the mailbox - -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - ffi::OsStr, - fmt::{self, Display}, - ops::Deref, -}; - -use crate::{ - mbox::Mboxes, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of Maildir mailboxes. -#[derive(Debug, Default, serde::Serialize)] -pub struct MaildirMboxes { - #[serde(rename = "response")] - pub mboxes: Vec, -} - -impl Deref for MaildirMboxes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.mboxes - } -} - -impl PrintTable for MaildirMboxes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -impl Mboxes for MaildirMboxes { - // -} - -/// Represents the mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct MaildirMbox { - /// Represents the mailbox name. - pub name: String, -} - -impl MaildirMbox { - pub fn new(name: &str) -> Self { - Self { name: name.into() } - } -} - -impl Display for MaildirMbox { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for MaildirMbox { - fn head() -> Row { - Row::new().cell(Cell::new("SUBDIR").bold().underline().white()) - } - - fn row(&self) -> Row { - Row::new().cell(Cell::new(&self.name).green()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_should_create_new_mbox() { - assert_eq!(MaildirMbox::default(), MaildirMbox::new("")); - assert_eq!( - MaildirMbox { - name: "INBOX".into(), - ..MaildirMbox::default() - }, - MaildirMbox::new("INBOX") - ); - } - - #[test] - fn it_should_display_mbox() { - let default_mbox = MaildirMbox::default(); - assert_eq!("", default_mbox.to_string()); - - let new_mbox = MaildirMbox::new("INBOX"); - assert_eq!("INBOX", new_mbox.to_string()); - - let full_mbox = MaildirMbox { - name: "Sent".into(), - }; - assert_eq!("Sent", full_mbox.to_string()); - } -} - -/// Represents a list of raw mailboxes returned by the `maildir` crate. -pub type RawMaildirMboxes = maildir::MaildirEntries; - -impl TryFrom for MaildirMboxes { - type Error = Error; - - fn try_from(mail_entries: RawMaildirMboxes) -> Result { - let mut mboxes = vec![]; - for entry in mail_entries { - mboxes.push(entry?.try_into()?); - } - Ok(MaildirMboxes { mboxes }) - } -} - -/// Represents the raw mailbox returned by the `maildir` crate. -pub type RawMaildirMbox = maildir::Maildir; - -impl TryFrom for MaildirMbox { - type Error = Error; - - fn try_from(mail_entry: RawMaildirMbox) -> Result { - let subdir_name = mail_entry.path().file_name(); - Ok(Self { - name: subdir_name - .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 {:?}", - subdir_name, - ) - })? - .into(), - }) - } -} diff --git a/cli/src/backends/notmuch/notmuch_envelope.rs b/cli/src/backends/notmuch/notmuch_envelope.rs deleted file mode 100644 index 626d949..0000000 --- a/cli/src/backends/notmuch/notmuch_envelope.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! Notmuch mailbox module. -//! -//! This module provides Notmuch types and conversion utilities -//! related to the envelope - -use anyhow::{anyhow, Context, Error, Result}; -use chrono::DateTime; -use log::{info, trace}; -use std::{ - convert::{TryFrom, TryInto}, - ops::{Deref, DerefMut}, -}; - -use crate::{ - msg::{from_slice_to_addrs, Addr}, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct NotmuchEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for NotmuchEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl DerefMut for NotmuchEnvelopes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.envelopes - } -} - -impl PrintTable for NotmuchEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -/// Represents the envelope. The envelope is just a message subset, -/// and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct NotmuchEnvelope { - /// Represents the id of the message. - pub id: String, - - /// Represents the MD5 hash of the message id. - pub hash: String, - - /// Represents the tags of the message. - pub flags: Vec, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the date of the message. - pub date: String, -} - -impl Table for NotmuchEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("HASH").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let hash = self.hash.to_string(); - let unseen = !self.flags.contains(&String::from("unread")); - let flags = String::new(); - let subject = &self.subject; - let sender = &self.sender; - let date = &self.date; - Row::new() - .cell(Cell::new(hash).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopees returned by the `notmuch` crate. -pub type RawNotmuchEnvelopes = notmuch::Messages; - -impl<'a> TryFrom for NotmuchEnvelopes { - type Error = Error; - - fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result { - let mut envelopes = vec![]; - for raw_envelope in raw_envelopes { - let envelope: NotmuchEnvelope = raw_envelope - .try_into() - .context("cannot parse notmuch mail entry")?; - envelopes.push(envelope); - } - Ok(NotmuchEnvelopes { envelopes }) - } -} - -/// Represents the raw envelope returned by the `notmuch` crate. -pub type RawNotmuchEnvelope = notmuch::Message; - -impl<'a> TryFrom for NotmuchEnvelope { - type Error = Error; - - fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result { - info!("begin: try building envelope from notmuch parsed mail"); - - let id = raw_envelope.id().to_string(); - let hash = format!("{:x}", md5::compute(&id)); - let subject = raw_envelope - .header("subject") - .context("cannot get header \"Subject\" from notmuch message")? - .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 {:?}", id))? - .to_string(); - let sender = from_slice_to_addrs(sender)? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), - }) - .ok_or_else(|| anyhow!("cannot find sender"))?; - let date = raw_envelope - .header("date") - .context("cannot get header \"Date\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", id))? - .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, id - ))? - .naive_local() - .to_string(); - - let envelope = Self { - id, - hash, - flags: raw_envelope.tags().collect(), - subject, - sender, - date, - }; - trace!("envelope: {:?}", envelope); - - info!("end: try building envelope from notmuch parsed mail"); - Ok(envelope) - } -} diff --git a/cli/src/backends/notmuch/notmuch_mbox.rs b/cli/src/backends/notmuch/notmuch_mbox.rs deleted file mode 100644 index 2fe1262..0000000 --- a/cli/src/backends/notmuch/notmuch_mbox.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Notmuch mailbox module. -//! -//! This module provides Notmuch types and conversion utilities -//! related to the mailbox - -use anyhow::Result; -use std::{ - fmt::{self, Display}, - ops::Deref, -}; - -use crate::{ - mbox::Mboxes, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of Notmuch mailboxes. -#[derive(Debug, Default, serde::Serialize)] -pub struct NotmuchMboxes { - #[serde(rename = "response")] - pub mboxes: Vec, -} - -impl Deref for NotmuchMboxes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.mboxes - } -} - -impl PrintTable for NotmuchMboxes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -impl Mboxes for NotmuchMboxes { - // -} - -/// Represents the notmuch virtual mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct NotmuchMbox { - /// Represents the virtual mailbox name. - pub name: String, - - /// Represents the query associated to the virtual mailbox name. - pub query: String, -} - -impl NotmuchMbox { - pub fn new(name: &str, query: &str) -> Self { - Self { - name: name.into(), - query: query.into(), - } - } -} - -impl Display for NotmuchMbox { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for NotmuchMbox { - fn head() -> Row { - Row::new() - .cell(Cell::new("NAME").bold().underline().white()) - .cell(Cell::new("QUERY").bold().underline().white()) - } - - fn row(&self) -> Row { - Row::new() - .cell(Cell::new(&self.name).white()) - .cell(Cell::new(&self.query).green()) - } -} diff --git a/cli/src/config/account.rs b/cli/src/config/account.rs index d593b8f..3a11deb 100644 --- a/cli/src/config/account.rs +++ b/cli/src/config/account.rs @@ -12,8 +12,9 @@ use std::{ ops::Deref, }; +use himalaya_lib::account::DeserializedAccountConfig; + use crate::{ - config::DeserializedAccountConfig, output::{PrintTable, PrintTableOpts, WriteColor}, ui::{Cell, Row, Table}, }; diff --git a/cli/src/config/account_handlers.rs b/cli/src/config/account_handlers.rs index 4ee2c57..4e0e082 100644 --- a/cli/src/config/account_handlers.rs +++ b/cli/src/config/account_handlers.rs @@ -3,10 +3,11 @@ //! This module gathers all account actions triggered by the CLI. use anyhow::Result; +use himalaya_lib::account::{Account, DeserializedConfig}; use log::{info, trace}; use crate::{ - config::{AccountConfig, Accounts, DeserializedConfig}, + config::Accounts, output::{PrintTableOpts, PrinterService}, }; @@ -14,7 +15,7 @@ use crate::{ pub fn list<'a, P: PrinterService>( max_width: Option, config: &DeserializedConfig, - account_config: &AccountConfig, + account_config: &Account, printer: &mut P, ) -> Result<()> { info!(">> account list handler"); @@ -36,13 +37,13 @@ pub fn list<'a, P: PrinterService>( #[cfg(test)] mod tests { + use himalaya_lib::account::{ + Account, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig, + }; use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator}; use termcolor::ColorSpec; - use crate::{ - config::{DeserializedAccountConfig, DeserializedImapAccountConfig}, - output::{Print, PrintTable, WriteColor}, - }; + use crate::output::{Print, PrintTable, WriteColor}; use super::*; @@ -121,7 +122,7 @@ mod tests { ..DeserializedConfig::default() }; - let account_config = AccountConfig::default(); + let account_config = Account::default(); let mut printer = PrinterServiceTest::default(); assert!(list(None, &config, &account_config, &mut printer).is_ok()); diff --git a/cli/src/config/format.rs b/cli/src/config/format.rs deleted file mode 100644 index 304b7f6..0000000 --- a/cli/src/config/format.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::Deserialize; - -/// Represents the text/plain format as defined in the [RFC2646]. The -/// format is then used by the table system to adjust the way it is -/// rendered. -/// -/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] -#[serde(tag = "type", content = "width", rename_all = "lowercase")] -pub enum Format { - // Forces the content width with a fixed amount of pixels. - Fixed(usize), - // Makes the content fit the terminal. - Auto, - // Does not restrict the content. - Flowed, -} - -impl Default for Format { - fn default() -> Self { - Self::Auto - } -} diff --git a/cli/src/config/hooks.rs b/cli/src/config/hooks.rs deleted file mode 100644 index 4bd44f0..0000000 --- a/cli/src/config/hooks.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Hooks { - pub pre_send: Option, -} 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/imap/imap_envelopes.rs b/cli/src/imap/imap_envelopes.rs new file mode 100644 index 0000000..8095b9b --- /dev/null +++ b/cli/src/imap/imap_envelopes.rs @@ -0,0 +1,16 @@ +use anyhow::{Context, Result}; +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>; + +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).context("cannot parse imap fetch")?); + } + Ok(envelopes) +} 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 32384d5..9fefb19 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -2,103 +2,39 @@ pub mod mbox { pub mod mbox; pub use mbox::*; + pub mod mboxes; + pub use mboxes::*; + pub mod mbox_args; 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::*; + pub mod envelopes; + pub use envelopes::*; + 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_mbox; - pub use imap_mbox::*; - - pub mod imap_mbox_attr; - pub use imap_mbox_attr::*; - - pub mod imap_envelope; - pub use imap_envelope::*; - - 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_mbox; - pub use maildir_mbox::*; - - pub mod maildir_envelope; - pub use maildir_envelope::*; - - 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_mbox; - pub use notmuch_mbox::*; - - pub mod notmuch_envelope; - pub use notmuch_envelope::*; - } - - #[cfg(feature = "notmuch-backend")] - pub use self::notmuch::*; } pub mod smtp { @@ -107,12 +43,6 @@ pub mod smtp { } pub mod config { - pub mod deserialized_config; - pub use deserialized_config::*; - - pub mod deserialized_account_config; - pub use deserialized_account_config::*; - pub mod config_args; pub mod account_args; @@ -120,15 +50,6 @@ pub mod config { pub mod account; pub use account::*; - - pub mod account_config; - pub use account_config::*; - - pub mod format; - pub use format::*; - - pub mod hooks; - pub use hooks::*; } pub mod compl; diff --git a/cli/src/main.rs b/cli/src/main.rs index 66f3900..e81924f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,14 +1,14 @@ -use anyhow::Result; +use anyhow::{Context, Result}; +use himalaya_lib::{ + account::{Account, 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, AccountConfig, BackendConfig, - DeserializedConfig, DEFAULT_INBOX_FOLDER, - }, + config::{account_args, account_handlers, config_args}, mbox::{mbox_args, mbox_handlers}, msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers}, output::{output_args, OutputFmt, StdoutPrinter}, @@ -16,13 +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, config::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")) @@ -55,7 +58,7 @@ fn main() -> Result<()> { if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { let config = DeserializedConfig::from_opt_path(None)?; let (account_config, backend_config) = - AccountConfig::from_config_and_opt_account_name(&config, None)?; + Account::from_config_and_opt_account_name(&config, None)?; let mut printer = StdoutPrinter::from(OutputFmt::Plain); let url = Url::parse(&raw_args[1])?; let mut smtp = LettreService::from(&account_config); @@ -111,7 +114,7 @@ fn main() -> Result<()> { // Init entities and services. let config = DeserializedConfig::from_opt_path(m.value_of("config"))?; let (account_config, backend_config) = - AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?; + Account::from_config_and_opt_account_name(&config, m.value_of("account"))?; let mbox = m .value_of("mbox-source") .or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str())) @@ -277,8 +280,9 @@ fn main() -> Result<()> { Some(msg_args::Cmd::Send(raw_msg)) => { return msg_handlers::send(raw_msg, &account_config, &mut printer, backend, &mut smtp); } - Some(msg_args::Cmd::Write(atts, encrypt)) => { + Some(msg_args::Cmd::Write(tpl, atts, encrypt)) => { return msg_handlers::write( + tpl, atts, encrypt, &account_config, @@ -343,5 +347,5 @@ fn main() -> Result<()> { _ => (), } - backend.disconnect() + backend.disconnect().context("cannot disconnect") } diff --git a/cli/src/mbox/mbox.rs b/cli/src/mbox/mbox.rs index 45f7713..e98e743 100644 --- a/cli/src/mbox/mbox.rs +++ b/cli/src/mbox/mbox.rs @@ -1,7 +1,19 @@ -use std::fmt; +use himalaya_lib::mbox::Mbox; -use crate::output::PrintTable; +use crate::ui::{Cell, Row, Table}; -pub trait Mboxes: fmt::Debug + erased_serde::Serialize + PrintTable { - // +impl Table for Mbox { + fn head() -> Row { + Row::new() + .cell(Cell::new("DELIM").bold().underline().white()) + .cell(Cell::new("NAME").bold().underline().white()) + .cell(Cell::new("DESC").bold().underline().white()) + } + + fn row(&self) -> Row { + Row::new() + .cell(Cell::new(&self.delim).white()) + .cell(Cell::new(&self.name).blue()) + .cell(Cell::new(&self.desc).green()) + } } diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index 4a110e9..b5a9aa9 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -3,18 +3,15 @@ //! This module gathers all mailbox actions triggered by the CLI. use anyhow::Result; +use himalaya_lib::{account::Account, backend::Backend}; use log::{info, trace}; -use crate::{ - backends::Backend, - config::AccountConfig, - output::{PrintTableOpts, PrinterService}, -}; +use crate::output::{PrintTableOpts, PrinterService}; /// Lists all mailboxes. pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( max_width: Option, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -22,7 +19,8 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let mboxes = backend.get_mboxes()?; trace!("mailboxes: {:?}", mboxes); printer.print_table( - mboxes, + // TODO: remove Box + Box::new(mboxes), PrintTableOpts { format: &config.format, max_width, @@ -32,15 +30,15 @@ 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}, + }; use std::{fmt::Debug, io}; use termcolor::ColorSpec; - use crate::{ - backends::{ImapMbox, ImapMboxAttr, ImapMboxAttrs, ImapMboxes}, - mbox::Mboxes, - msg::{Envelopes, Msg}, - output::{Print, PrintTable, WriteColor}, - }; + use crate::output::{Print, PrintTable, WriteColor}; use super::*; @@ -90,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 { @@ -111,32 +109,29 @@ 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> { - Ok(Box::new(ImapMboxes { + fn get_mboxes(&mut self) -> backend::Result { + Ok(Mboxes { mboxes: vec![ - ImapMbox { + Mbox { delim: "/".into(), name: "INBOX".into(), - attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]), + desc: "desc".into(), }, - ImapMbox { + Mbox { delim: "/".into(), name: "Sent".into(), - attrs: ImapMboxAttrs(vec![ - ImapMboxAttr::NoInferiors, - ImapMboxAttr::Custom("HasNoChildren".into()), - ]), + desc: "desc".into(), }, ], - })) + }) } - 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,36 +141,36 @@ 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!() } } - let config = AccountConfig::default(); + let config = Account::default(); let mut printer = PrinterServiceTest::default(); let mut backend = TestBackend {}; let backend = Box::new(&mut backend); @@ -184,9 +179,9 @@ mod tests { assert_eq!( concat![ "\n", - "DELIM │NAME │ATTRIBUTES \n", - "/ │INBOX │NoSelect \n", - "/ │Sent │NoInferiors, HasNoChildren \n", + "DELIM │NAME │DESC \n", + "/ │INBOX │desc \n", + "/ │Sent │desc \n", "\n" ], printer.writer.content diff --git a/cli/src/mbox/mboxes.rs b/cli/src/mbox/mboxes.rs new file mode 100644 index 0000000..9a032ff --- /dev/null +++ b/cli/src/mbox/mboxes.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use himalaya_lib::mbox::Mboxes; + +use crate::{ + output::{PrintTable, PrintTableOpts, WriteColor}, + ui::Table, +}; + +impl PrintTable for Mboxes { + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; + Ok(()) + } +} diff --git a/cli/src/msg/envelope.rs b/cli/src/msg/envelope.rs index 91c5c04..2f96dd9 100644 --- a/cli/src/msg/envelope.rs +++ b/cli/src/msg/envelope.rs @@ -1,13 +1,30 @@ -use std::{any, fmt}; +use himalaya_lib::msg::{Envelope, Flag}; -use crate::output::PrintTable; +use crate::ui::{Cell, Row, Table}; -pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any { - fn as_any(&self) -> &dyn any::Any; -} +impl Table for Envelope { + fn head() -> Row { + Row::new() + .cell(Cell::new("ID").bold().underline().white()) + .cell(Cell::new("FLAGS").bold().underline().white()) + .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) + .cell(Cell::new("SENDER").bold().underline().white()) + .cell(Cell::new("DATE").bold().underline().white()) + } -impl Envelopes for T { - fn as_any(&self) -> &dyn any::Any { - self + fn row(&self) -> Row { + let id = self.id.to_string(); + let flags = self.flags.to_symbols_string(); + let unseen = !self.flags.contains(&Flag::Seen); + let subject = &self.subject; + let sender = &self.sender; + let date = self.date.as_deref().unwrap_or_default(); + + Row::new() + .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(flags).bold_if(unseen).white()) + .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) + .cell(Cell::new(sender).bold_if(unseen).blue()) + .cell(Cell::new(date).bold_if(unseen).yellow()) } } diff --git a/cli/src/msg/envelopes.rs b/cli/src/msg/envelopes.rs new file mode 100644 index 0000000..0a524a9 --- /dev/null +++ b/cli/src/msg/envelopes.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use himalaya_lib::msg::Envelopes; + +use crate::{ + output::{PrintTable, PrintTableOpts, WriteColor}, + ui::Table, +}; + +impl PrintTable for Envelopes { + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; + Ok(()) + } +} 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 32e02b6..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(AttachmentPaths<'a>, Encrypt), + Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt), Flag(Option>), Tpl(Option>), @@ -261,7 +265,8 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("attachments paths: {:?}", attachment_paths); let encrypt = m.is_present("encrypt"); debug!("encrypt: {}", encrypt); - return Ok(Some(Cmd::Write(attachment_paths, encrypt))); + let tpl = from_args(m); + return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt))); } if let Some(m) = m.subcommand_matches("template") { @@ -412,6 +417,7 @@ pub fn subcmds<'a>() -> Vec> { ), SubCommand::with_name("write") .about("Writes a new message") + .args(&tpl_args::tpl_args()) .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("send") diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index 80520c5..4041fdf 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -4,6 +4,11 @@ use anyhow::{Context, Result}; use atty::Stream; +use himalaya_lib::{ + account::{Account, DEFAULT_SENT_FOLDER}, + backend::Backend, + msg::{Msg, Part, Parts, TextPlainPart, TplOverride}, +}; use log::{debug, info, trace}; use mailparse::addrparse; use std::{ @@ -14,18 +19,16 @@ use std::{ use url::Url; use crate::{ - backends::Backend, - config::{AccountConfig, DEFAULT_SENT_FOLDER}, - msg::{Msg, Part, Parts, TextPlainPart}, output::{PrintTableOpts, PrinterService}, smtp::SmtpService, + ui::editor, }; /// Downloads all message attachments to the user account downloads directory. pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -89,17 +92,17 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( attachments_paths: Vec<&str>, encrypt: bool, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - backend + let msg = backend .get_msg(mbox, seq)? .into_forward(config)? .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor(config, printer, backend, smtp)?; + .encrypt(encrypt); + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; Ok(()) } @@ -109,7 +112,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( page_size: Option, page: usize, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, imap: Box<&'a mut B>, ) -> Result<()> { @@ -118,7 +121,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = imap.get_envelopes(mbox, page_size, page)?; trace!("envelopes: {:?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, @@ -131,7 +134,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// [mailto]: https://en.wikipedia.org/wiki/Mailto pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( url: &Url, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, @@ -183,7 +186,7 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( }; trace!("message: {:?}", msg); - msg.edit_with_editor(config, printer, backend, smtp)?; + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; Ok(()) } @@ -209,7 +212,7 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( raw: bool, headers: Vec<&str>, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -230,18 +233,19 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( attachments_paths: Vec<&str>, encrypt: bool, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - backend + let msg = backend .get_msg(mbox, seq)? .into_reply(all, config)? .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor(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. @@ -281,7 +285,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( page_size: Option, page: usize, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -290,7 +294,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?; trace!("messages: {:#?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, @@ -306,7 +310,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( page_size: Option, page: usize, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -315,7 +319,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?; trace!("envelopes: {:#?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, @@ -326,7 +330,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Send a raw message. pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( raw_msg: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&mut B>, smtp: &mut S, @@ -364,16 +368,17 @@ 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: TplOverride, attachments_paths: Vec<&str>, encrypt: bool, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - Msg::default() + let msg = Msg::default() .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor(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 e3254ff..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)] -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 b2db225..558e76a 100644 --- a/cli/src/msg/tpl_handlers.rs +++ b/cli/src/msg/tpl_handlers.rs @@ -4,20 +4,19 @@ use anyhow::Result; use atty::Stream; +use himalaya_lib::{ + account::Account, + backend::Backend, + msg::{Msg, TplOverride}, +}; use std::io::{self, BufRead}; -use crate::{ - backends::Backend, - config::AccountConfig, - msg::{Msg, TplOverride}, - output::PrinterService, - smtp::SmtpService, -}; +use crate::{output::PrinterService, smtp::SmtpService}; /// Generate a new message template. pub fn new<'a, P: PrinterService>( opts: TplOverride<'a>, - account: &'a AccountConfig, + account: &'a Account, printer: &'a mut P, ) -> Result<()> { let tpl = Msg::default().to_tpl(opts, account)?; @@ -30,7 +29,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>( all: bool, opts: TplOverride<'a>, mbox: &str, - config: &'a AccountConfig, + config: &'a Account, printer: &'a mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -46,7 +45,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, opts: TplOverride<'a>, mbox: &str, - config: &'a AccountConfig, + config: &'a Account, printer: &'a mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -60,7 +59,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Saves a message based on a template. pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( mbox: &str, - config: &AccountConfig, + config: &Account, attachments_paths: Vec<&str>, tpl: &str, printer: &mut P, @@ -85,7 +84,7 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Sends a message based on a template. pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( mbox: &str, - account: &AccountConfig, + account: &Account, attachments_paths: Vec<&str>, tpl: &str, printer: &mut P, diff --git a/cli/src/output/print.rs b/cli/src/output/print.rs index a843501..8faf0a4 100644 --- a/cli/src/output/print.rs +++ b/cli/src/output/print.rs @@ -8,12 +8,14 @@ pub trait Print { impl Print for &str { fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - writeln!(writer, "{}", self).context("cannot write string to writer") + writeln!(writer, "{}", self).context("cannot write string to writer")?; + Ok(writer.reset()?) } } impl Print for String { fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - self.as_str().print(writer) + self.as_str().print(writer)?; + Ok(writer.reset()?) } } diff --git a/cli/src/output/print_table.rs b/cli/src/output/print_table.rs index 45557b9..a99e316 100644 --- a/cli/src/output/print_table.rs +++ b/cli/src/output/print_table.rs @@ -1,9 +1,8 @@ use anyhow::Result; +use himalaya_lib::account::TextPlainFormat; use std::io; use termcolor::{self, StandardStream}; -use crate::config::Format; - pub trait WriteColor: io::Write + termcolor::WriteColor {} impl WriteColor for StandardStream {} @@ -13,6 +12,6 @@ pub trait PrintTable { } pub struct PrintTableOpts<'a> { - pub format: &'a Format, + pub format: &'a TextPlainFormat, pub max_width: Option, } diff --git a/cli/src/smtp/smtp_service.rs b/cli/src/smtp/smtp_service.rs index 13ea6ce..7a9db3b 100644 --- a/cli/src/smtp/smtp_service.rs +++ b/cli/src/smtp/smtp_service.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use himalaya_lib::{account::Account, msg::Msg}; use lettre::{ self, transport::smtp::{ @@ -9,14 +10,14 @@ use lettre::{ }; use std::convert::TryInto; -use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd}; +use crate::output::pipe_cmd; pub trait SmtpService { - fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result>; + fn send(&mut self, account: &Account, msg: &Msg) -> Result>; } pub struct LettreService<'a> { - account: &'a AccountConfig, + account: &'a Account, transport: Option, } @@ -55,14 +56,14 @@ impl LettreService<'_> { } impl SmtpService for LettreService<'_> { - fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result> { + fn send(&mut self, account: &Account, msg: &Msg) -> Result> { let mut raw_msg = msg.into_sendable_msg(account)?.formatted(); let envelope: lettre::address::Envelope = if let Some(cmd) = account.hooks.pre_send.as_deref() { for cmd in cmd.split('|') { raw_msg = pipe_cmd(cmd.trim(), &raw_msg) - .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))? + .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?; } let parsed_mail = mailparse::parse_mail(&raw_msg)?; Msg::from_parsed_mail(parsed_mail, account)?.try_into() @@ -75,8 +76,8 @@ impl SmtpService for LettreService<'_> { } } -impl<'a> From<&'a AccountConfig> for LettreService<'a> { - fn from(account: &'a AccountConfig) -> Self { +impl<'a> From<&'a Account> for LettreService<'a> { + fn from(account: &'a Account) -> Self { Self { account, transport: None, diff --git a/cli/src/ui/editor.rs b/cli/src/ui/editor.rs index 9e7a5c2..613f6c5 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::{Account, 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: &Account) -> 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: &Account, + 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/cli/src/ui/table.rs b/cli/src/ui/table.rs index 0c97a13..82fef6f 100644 --- a/cli/src/ui/table.rs +++ b/cli/src/ui/table.rs @@ -5,15 +5,13 @@ //! [builder design pattern]: https://refactoring.guru/design-patterns/builder use anyhow::{Context, Result}; +use himalaya_lib::account::TextPlainFormat; use log::trace; use termcolor::{Color, ColorSpec}; use terminal_size; use unicode_width::UnicodeWidthStr; -use crate::{ - config::Format, - output::{Print, PrintTableOpts, WriteColor}, -}; +use crate::output::{Print, PrintTableOpts, WriteColor}; /// Defines the default terminal size. /// This is used when the size cannot be determined by the `terminal_size` crate. @@ -134,7 +132,9 @@ impl Print for Cell { .context(format!(r#"cannot apply colors to cell "{}""#, self.value))?; // Writes the colorized cell to stdout - write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value)) + write!(writer, "{}", self.value) + .context(format!(r#"cannot print cell "{}""#, self.value))?; + Ok(writer.reset()?) } } @@ -169,11 +169,11 @@ where /// Writes the table to the writer. fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> { - let is_format_flowed = matches!(opts.format, Format::Flowed); + let is_format_flowed = matches!(opts.format, TextPlainFormat::Flowed); let max_width = match opts.format { - Format::Fixed(width) => opts.max_width.unwrap_or(*width), - Format::Flowed => 0, - Format::Auto => opts + TextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width), + TextPlainFormat::Flowed => 0, + TextPlainFormat::Auto => opts .max_width .or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize)) .unwrap_or(DEFAULT_TERM_WIDTH), diff --git a/flake.nix b/flake.nix index d7c0014..c3c7af2 100644 --- a/flake.nix +++ b/flake.nix @@ -16,19 +16,8 @@ (system: let name = "himalaya"; - pkgs = import nixpkgs { - inherit system; - overlays = [ - rust-overlay.overlay - (self: super: { - # Because rust-overlay bundles multiple rust packages - # into one derivation, specify that mega-bundle here, - # so that crate2nix will use them automatically. - rustc = self.rust-bin.stable.latest.default; - cargo = self.rust-bin.stable.latest.default; - }) - ]; - }; + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; in rec { # nix build @@ -68,17 +57,19 @@ # nix develop devShell = pkgs.mkShell { - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + RUSTUP_TOOLCHAIN = "stable"; inputsFrom = builtins.attrValues self.packages.${system}; - buildInputs = with pkgs; [ - cargo - cargo-watch - trunk - ripgrep - rust-analyzer - rustfmt + nativeBuildInputs = with pkgs; [ + # Nix LSP + formatter rnix-lsp nixpkgs-fmt + + # Rust env + (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) + cargo-watch + rust-analyzer + + # Notmuch notmuch ]; }; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8316d53..2549a13 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -3,4 +3,33 @@ name = "himalaya-lib" version = "0.1.0" edition = "2021" +[features] +imap-backend = ["imap", "imap-proto"] +maildir-backend = ["maildir", "md5"] +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 } +imap-proto = { version = "0.14.3", optional = true } +maildir = { version = "0.6.1", optional = true } +md5 = { version = "0.7.0", optional = true } +notmuch = { version = "0.7.1", optional = true } diff --git a/cli/src/config/account_config.rs b/lib/src/account/account_config.rs similarity index 69% rename from cli/src/config/account_config.rs rename to lib/src/account/account_config.rs index 0d32a19..15e3dc9 100644 --- a/cli/src/config/account_config.rs +++ b/lib/src/account/account_config.rs @@ -1,14 +1,78 @@ -use anyhow::{anyhow, Context, Result}; +//! Account config module. +//! +//! This module contains the representation of the user account. + use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; use log::{debug, info, trace}; use mailparse::MailAddr; +use serde::Deserialize; +use shellexpand; use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf}; +use thiserror::Error; -use crate::{config::*, output::run_cmd}; +use crate::process::{self, ProcessError}; + +use super::*; + +pub const DEFAULT_PAGE_SIZE: usize = 10; +pub const DEFAULT_SIG_DELIM: &str = "-- \n"; + +pub const DEFAULT_INBOX_FOLDER: &str = "INBOX"; +pub const DEFAULT_SENT_FOLDER: &str = "Sent"; +pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts"; + +#[derive(Debug, Error)] +pub enum AccountError { + #[error("cannot encrypt file using pgp")] + EncryptFileError(#[source] ProcessError), + #[error("cannot find encrypt file command from config file")] + EncryptFileMissingCmdError, + + #[error("cannot decrypt file using pgp")] + DecryptFileError(#[source] ProcessError), + #[error("cannot find decrypt file command from config file")] + DecryptFileMissingCmdError, + + #[error("cannot get smtp password")] + GetSmtpPasswdError(#[source] ProcessError), + #[error("cannot get smtp password: password is empty")] + GetSmtpPasswdEmptyError, + + #[cfg(feature = "imap-backend")] + #[error("cannot get imap password")] + GetImapPasswdError(#[source] ProcessError), + #[cfg(feature = "imap-backend")] + #[error("cannot get imap password: password is empty")] + GetImapPasswdEmptyError, + + #[error("cannot find default account")] + FindDefaultAccountError, + #[error("cannot find account {0}")] + FindAccountError(String), + #[error("cannot parse account address {0}")] + ParseAccountAddrError(#[source] mailparse::MailParseError, String), + #[error("cannot find account address in {0}")] + ParseAccountAddrNotFoundError(String), + + #[cfg(feature = "maildir-backend")] + #[error("cannot expand maildir path")] + ExpandMaildirPathError(#[source] shellexpand::LookupError), + #[cfg(feature = "notmuch-backend")] + #[error("cannot expand notmuch path")] + ExpandNotmuchDatabasePathError(#[source] shellexpand::LookupError), + #[error("cannot expand mailbox alias {1}")] + ExpandMboxAliasError(#[source] shellexpand::LookupError, String), + + #[error("cannot parse download file name from {0}")] + ParseDownloadFileNameError(PathBuf), + + #[error("cannot start the notify mode")] + StartNotifyModeError(#[source] ProcessError), +} /// Represents the user account. #[derive(Debug, Default, Clone)] -pub struct AccountConfig { +pub struct Account { /// Represents the name of the user account. pub name: String, /// Makes this account the default one. @@ -31,7 +95,7 @@ pub struct AccountConfig { pub watch_cmds: Vec, /// Represents the text/plain format as defined in the /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) - pub format: Format, + pub format: TextPlainFormat, /// Overrides the default headers displayed at the top of /// the read message. pub read_headers: Vec, @@ -61,12 +125,13 @@ pub struct AccountConfig { pub pgp_decrypt_cmd: Option, } -impl<'a> AccountConfig { - /// tries to create an account from a config and an optional account name. +impl<'a> Account { + /// Tries to create an account from a config and an optional + /// account name. pub fn from_config_and_opt_account_name( config: &'a DeserializedConfig, account_name: Option<&str>, - ) -> Result<(AccountConfig, BackendConfig)> { + ) -> Result<(Account, BackendConfig), AccountError> { info!("begin: parsing account and backend configs from config and account name"); debug!("account name: {:?}", account_name.unwrap_or("default")); @@ -87,12 +152,12 @@ impl<'a> AccountConfig { } }) .map(|(name, account)| (name.to_owned(), account)) - .ok_or_else(|| anyhow!("cannot find default account")), + .ok_or_else(|| AccountError::FindDefaultAccountError), Some(name) => config .accounts .get(name) .map(|account| (name.to_owned(), account)) - .ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)), + .ok_or_else(|| AccountError::FindAccountError(name.to_owned())), }?; let base_account = account.to_base(); @@ -136,7 +201,7 @@ impl<'a> AccountConfig { .or_else(|| sig.map(|sig| sig.to_owned())) .map(|sig| format!("{}{}", sig_delim, sig.trim_end())); - let account_config = AccountConfig { + let account_config = Account { name, display_name: base_account .name @@ -146,7 +211,11 @@ impl<'a> AccountConfig { downloads_dir, sig, default_page_size, - notify_cmd: base_account.notify_cmd.clone(), + notify_cmd: base_account + .notify_cmd + .as_ref() + .or_else(|| config.notify_cmd.as_ref()) + .cloned(), notify_query: base_account .notify_query .as_ref() @@ -191,13 +260,17 @@ impl<'a> AccountConfig { #[cfg(feature = "maildir-backend")] DeserializedAccountConfig::Maildir(config) => { BackendConfig::Maildir(MaildirBackendConfig { - maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(), + maildir_dir: shellexpand::full(&config.maildir_dir) + .map_err(AccountError::ExpandMaildirPathError)? + .to_string() + .into(), }) } #[cfg(feature = "notmuch-backend")] DeserializedAccountConfig::Notmuch(config) => { BackendConfig::Notmuch(NotmuchBackendConfig { - notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)? + notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir) + .map_err(AccountError::ExpandNotmuchDatabasePathError)? .to_string() .into(), }) @@ -210,7 +283,7 @@ impl<'a> AccountConfig { } /// Builds the full RFC822 compliant address of the user account. - pub fn address(&self) -> Result { + pub fn address(&self) -> Result { let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c)); let addr = if self.display_name.is_empty() { self.email.clone() @@ -222,67 +295,63 @@ impl<'a> AccountConfig { }; Ok(mailparse::addrparse(&addr) - .context(format!( - "cannot parse account address {:?}", - self.display_name - ))? + .map_err(|err| AccountError::ParseAccountAddrError(err, addr.to_owned()))? .first() - .ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))? + .ok_or_else(|| AccountError::ParseAccountAddrNotFoundError(addr.to_owned()))? .clone()) } /// Builds the user account SMTP credentials. - pub fn smtp_creds(&self) -> Result { - let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?; + pub fn smtp_creds(&self) -> Result { + let passwd = + process::run(&self.smtp_passwd_cmd).map_err(AccountError::GetSmtpPasswdError)?; let passwd = passwd - .trim_end_matches(|c| c == '\r' || c == '\n') - .to_owned(); + .lines() + .next() + .ok_or_else(|| AccountError::GetSmtpPasswdEmptyError)?; - Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd)) + Ok(SmtpCredentials::new( + self.smtp_login.to_owned(), + passwd.to_owned(), + )) } /// 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); - run_cmd(&encrypt_file_cmd).map(Some).context(format!( - "cannot run pgp encrypt command {:?}", - encrypt_file_cmd - )) + Ok(process::run(&encrypt_file_cmd).map_err(AccountError::EncryptFileError)?) } else { - Ok(None) + Err(AccountError::EncryptFileMissingCmdError) } } /// 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); - run_cmd(&decrypt_file_cmd).map(Some).context(format!( - "cannot run pgp decrypt command {:?}", - decrypt_file_cmd - )) + Ok(process::run(&decrypt_file_cmd).map_err(AccountError::DecryptFileError)?) } else { - Ok(None) + Err(AccountError::DecryptFileMissingCmdError) } } /// Gets the download path from a file name. - pub fn get_download_file_path>(&self, file_name: S) -> Result { + pub fn get_download_file_path>( + &self, + file_name: S, + ) -> Result { let file_path = self.downloads_dir.join(file_name.as_ref()); self.get_unique_download_file_path(&file_path, |path, _count| path.is_file()) - .context(format!( - "cannot get download file path of {:?}", - file_name.as_ref() - )) } - /// Gets the unique download path from a file name by adding suffixes in case of name conflicts. + /// Gets the unique download path from a file name by adding + /// suffixes in case of name conflicts. pub fn get_unique_download_file_path( &self, original_file_path: &PathBuf, is_file: impl Fn(&PathBuf, u8) -> bool, - ) -> Result { + ) -> Result { let mut count = 0; let file_ext = original_file_path .extension() @@ -298,7 +367,9 @@ impl<'a> AccountConfig { .file_stem() .and_then(OsStr::to_str) .map(|fstem| format!("{}_{}{}", fstem, count, file_ext)) - .ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?, + .ok_or_else(|| { + AccountError::ParseDownloadFileNameError(file_path.to_owned()) + })?, )); } @@ -306,7 +377,7 @@ impl<'a> AccountConfig { } /// Runs the notify command. - pub fn run_notify_cmd>(&self, subject: S, sender: S) -> Result<()> { + pub fn run_notify_cmd>(&self, subject: S, sender: S) -> Result<(), AccountError> { let subject = subject.as_ref(); let sender = sender.as_ref(); @@ -317,22 +388,22 @@ impl<'a> AccountConfig { .map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender)) .unwrap_or(default_cmd); - debug!("run command: {}", cmd); - run_cmd(&cmd).context("cannot run notify cmd")?; + process::run(&cmd).map_err(AccountError::StartNotifyModeError)?; Ok(()) } /// Gets the mailbox alias if exists, otherwise returns the /// mailbox. Also tries to expand shell variables. - pub fn get_mbox_alias(&self, mbox: &str) -> Result { + pub fn get_mbox_alias(&self, mbox: &str) -> Result { let mbox = self .mailboxes .get(&mbox.trim().to_lowercase()) .map(|s| s.as_str()) .unwrap_or(mbox); - shellexpand::full(mbox) + let mbox = shellexpand::full(mbox) .map(String::from) - .with_context(|| format!("cannot expand mailbox path {:?}", mbox)) + .map_err(|err| AccountError::ExpandMboxAliasError(err, mbox.to_owned()))?; + Ok(mbox) } } @@ -368,12 +439,14 @@ pub struct ImapBackendConfig { #[cfg(feature = "imap-backend")] impl ImapBackendConfig { /// Gets the IMAP password of the user account. - pub fn imap_passwd(&self) -> Result { - let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?; + pub fn imap_passwd(&self) -> Result { + let passwd = + process::run(&self.imap_passwd_cmd).map_err(AccountError::GetImapPasswdError)?; let passwd = passwd - .trim_end_matches(|c| c == '\r' || c == '\n') - .to_owned(); - Ok(passwd) + .lines() + .next() + .ok_or_else(|| AccountError::GetImapPasswdEmptyError)?; + Ok(passwd.to_string()) } } @@ -393,13 +466,39 @@ pub struct NotmuchBackendConfig { pub notmuch_database_dir: PathBuf, } +/// Represents the text/plain format as defined in the [RFC2646]. +/// +/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt +#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[serde(tag = "type", content = "width", rename_all = "lowercase")] +pub enum TextPlainFormat { + // Forces the content width with a fixed amount of pixels. + Fixed(usize), + // Makes the content fit the terminal. + Auto, + // Does not restrict the content. + Flowed, +} + +impl Default for TextPlainFormat { + fn default() -> Self { + Self::Auto + } +} + +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Hooks { + pub pre_send: Option, +} + #[cfg(test)] mod tests { use super::*; #[test] fn it_should_get_unique_download_file_path() { - let account = AccountConfig::default(); + let account = Account::default(); let path = PathBuf::from("downloads/file.ext"); // When file path is unique diff --git a/cli/src/config/deserialized_account_config.rs b/lib/src/account/deserialized_account_config.rs similarity index 94% rename from cli/src/config/deserialized_account_config.rs rename to lib/src/account/deserialized_account_config.rs index 8ada7f9..8ba6278 100644 --- a/cli/src/config/deserialized_account_config.rs +++ b/lib/src/account/deserialized_account_config.rs @@ -1,7 +1,12 @@ +//! Deserialized account config module. +//! +//! This module contains the raw deserialized representation of an +//! account in the accounts section of the user configuration file. + use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; -use crate::config::{Format, Hooks}; +use super::*; pub trait ToDeserializedBaseAccountConfig { fn to_base(&self) -> DeserializedBaseAccountConfig; @@ -53,9 +58,8 @@ macro_rules! make_account_config { pub notify_query: Option, /// Overrides the watch commands for this account. pub watch_cmds: Option>, - /// Represents the text/plain format as defined in the - /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) - pub format: Option, + /// Represents the text/plain format. + pub format: Option, /// Represents the default headers displayed at the top of /// the read message. #[serde(default)] diff --git a/cli/src/config/deserialized_config.rs b/lib/src/account/deserialized_config.rs similarity index 56% rename from cli/src/config/deserialized_config.rs rename to lib/src/account/deserialized_config.rs index e26b5a0..9da1798 100644 --- a/cli/src/config/deserialized_config.rs +++ b/lib/src/account/deserialized_config.rs @@ -1,17 +1,25 @@ -use anyhow::{Context, Result}; -use log::{debug, info, trace}; +//! Deserialized config module. +//! +//! This module contains the raw deserialized representation of the +//! user configuration file. + +use log::{debug, trace}; use serde::Deserialize; -use std::{collections::HashMap, env, fs, path::PathBuf}; +use std::{collections::HashMap, env, fs, io, path::PathBuf}; +use thiserror::Error; use toml; -use crate::config::DeserializedAccountConfig; +use super::*; -pub const DEFAULT_PAGE_SIZE: usize = 10; -pub const DEFAULT_SIG_DELIM: &str = "-- \n"; - -pub const DEFAULT_INBOX_FOLDER: &str = "INBOX"; -pub const DEFAULT_SENT_FOLDER: &str = "Sent"; -pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts"; +#[derive(Error, Debug)] +pub enum DeserializeConfigError { + #[error("cannot read config file")] + ReadConfigFile(#[source] io::Error), + #[error("cannot parse config file")] + ParseConfigFile(#[source] toml::de::Error), + #[error("cannot read environment variable {1}")] + ReadEnvVar(#[source] env::VarError, &'static str), +} /// Represents the user config file. #[derive(Debug, Default, Clone, Deserialize)] @@ -41,33 +49,38 @@ pub struct DeserializedConfig { impl DeserializedConfig { /// Tries to create a config from an optional path. - pub fn from_opt_path(path: Option<&str>) -> Result { - info!("begin: try to parse config from path"); + pub fn from_opt_path(path: Option<&str>) -> Result { + trace!(">> parse config from path"); debug!("path: {:?}", path); + let path = path.map(|s| s.into()).unwrap_or(Self::path()?); - let content = fs::read_to_string(path).context("cannot read config file")?; - let config = toml::from_str(&content).context("cannot parse config file")?; - info!("end: try to parse config from path"); + let content = fs::read_to_string(path).map_err(DeserializeConfigError::ReadConfigFile)?; + let config = toml::from_str(&content).map_err(DeserializeConfigError::ParseConfigFile)?; + trace!("config: {:?}", config); + trace!("<< parse config from path"); Ok(config) } - /// Tries to get the XDG config file path from XDG_CONFIG_HOME environment variable. - fn path_from_xdg() -> Result { - let path = - env::var("XDG_CONFIG_HOME").context("cannot find \"XDG_CONFIG_HOME\" env var")?; + /// Tries to get the XDG config file path from XDG_CONFIG_HOME + /// environment variable. + fn path_from_xdg() -> Result { + let path = env::var("XDG_CONFIG_HOME") + .map_err(|err| DeserializeConfigError::ReadEnvVar(err, "XDG_CONFIG_HOME"))?; let path = PathBuf::from(path).join("himalaya").join("config.toml"); Ok(path) } - /// Tries to get the XDG config file path from HOME environment variable. - fn path_from_xdg_alt() -> Result { + /// Tries to get the XDG config file path from HOME environment + /// variable. + fn path_from_xdg_alt() -> Result { let home_var = if cfg!(target_family = "windows") { "USERPROFILE" } else { "HOME" }; - let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?; + let path = + env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, home_var))?; let path = PathBuf::from(path) .join(".config") .join("himalaya") @@ -75,23 +88,24 @@ impl DeserializedConfig { Ok(path) } - /// Tries to get the .himalayarc config file path from HOME environment variable. - fn path_from_home() -> Result { + /// Tries to get the .himalayarc config file path from HOME + /// environment variable. + fn path_from_home() -> Result { let home_var = if cfg!(target_family = "windows") { "USERPROFILE" } else { "HOME" }; - let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?; + let path = + env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, home_var))?; let path = PathBuf::from(path).join(".himalayarc"); Ok(path) } /// Tries to get the config file path. - pub fn path() -> Result { + pub fn path() -> Result { Self::path_from_xdg() .or_else(|_| Self::path_from_xdg_alt()) .or_else(|_| Self::path_from_home()) - .context("cannot find config path") } } diff --git a/lib/src/account/mod.rs b/lib/src/account/mod.rs new file mode 100644 index 0000000..01a8c4d --- /dev/null +++ b/lib/src/account/mod.rs @@ -0,0 +1,12 @@ +//! Account module. +//! +//! This module contains everything related to the user configuration. + +mod account_config; +pub use account_config::*; + +mod deserialized_config; +pub use deserialized_config::*; + +mod deserialized_account_config; +pub use deserialized_account_config::*; diff --git a/cli/src/backends/backend.rs b/lib/src/backend/backend.rs similarity index 54% rename from cli/src/backends/backend.rs rename to lib/src/backend/backend.rs index d00ad1f..b1d1d99 100644 --- a/cli/src/backends/backend.rs +++ b/lib/src/backend/backend.rs @@ -3,27 +3,58 @@ //! This module exposes the backend trait, which can be used to create //! custom backend implementations. -use anyhow::Result; +use std::result; + +use thiserror::Error; use crate::{ + account, mbox::Mboxes, - msg::{Envelopes, Msg}, + 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::AccountError), + + #[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<()> { Ok(()) } fn add_mbox(&mut self, mbox: &str) -> Result<()>; - fn get_mboxes(&mut self) -> Result>; + fn get_mboxes(&mut self) -> Result; fn del_mbox(&mut self, mbox: &str) -> Result<()>; - fn get_envelopes( - &mut self, - mbox: &str, - page_size: usize, - page: usize, - ) -> Result>; + fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result; fn search_envelopes( &mut self, mbox: &str, @@ -31,8 +62,8 @@ pub trait Backend<'a> { sort: &str, page_size: usize, page: usize, - ) -> Result>; - fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result>; + ) -> Result; + fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result; fn get_msg(&mut self, mbox: &str, id: &str) -> Result; fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>; fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> 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..ff3b233 --- /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::AccountError), + #[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 64% rename from cli/src/backends/imap/imap_backend.rs rename to lib/src/backend/imap/imap_backend.rs index f6319e5..eb8f2a0 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/lib/src/backend/imap/imap_backend.rs @@ -2,38 +2,32 @@ //! //! This module contains the definition of the IMAP backend. -use anyhow::{anyhow, Context, Result}; +use imap::types::NameAttribute; use log::{debug, log_enabled, trace, Level}; use native_tls::{TlsConnector, TlsStream}; -use std::{ - collections::HashSet, - convert::{TryFrom, TryInto}, - net::TcpStream, - thread, -}; +use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread}; use crate::{ - backends::{ - imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes, + account::{Account, ImapBackendConfig}, + backend::{ + backend::Result, from_imap_fetch, from_imap_fetches, + imap::msg_sort_criterion::SortCriteria, imap::Error, into_imap_flags, Backend, }, - config::{AccountConfig, ImapBackendConfig}, - mbox::Mboxes, - msg::{Envelopes, Msg}, - output::run_cmd, + mbox::{Mbox, Mboxes}, + msg::{Envelopes, Flags, Msg}, + process, }; -use super::ImapFlags; - type ImapSess = imap::Session>; pub struct ImapBackend<'a> { - account_config: &'a AccountConfig, + account_config: &'a Account, imap_config: &'a ImapBackendConfig, sess: Option, } impl<'a> ImapBackend<'a> { - pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self { + pub fn new(account_config: &'a Account, imap_config: &'a ImapBackendConfig) -> Self { Self { account_config, imap_config, @@ -49,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); @@ -62,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); @@ -72,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()); @@ -103,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 @@ -125,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)? @@ -144,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 = ImapEnvelope::try_from(fetch)?; - let uid = fetch.uid.ok_or_else(|| { - anyhow!("cannot retrieve message {}'s UID", fetch.message) - })?; + let msg = from_imap_fetch(fetch)?; + 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)?; @@ -173,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"); @@ -187,14 +180,14 @@ 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 || { debug!("batch execution of {} cmd(s)", cmds.len()); cmds.iter().for_each(|cmd| { debug!("running command {:?}…", cmd); - let res = run_cmd(cmd); + let res = process::run(cmd); debug!("{:?}", res); }) }); @@ -206,40 +199,70 @@ 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> { - let mboxes: ImapMboxes = self + fn get_mboxes(&mut self) -> Result { + trace!(">> get imap mailboxes"); + + let imap_mboxes = self .sess()? .list(Some(""), Some("*")) - .context("cannot list mailboxes")? - .into(); - Ok(Box::new(mboxes)) + .map_err(Error::ListMboxesError)?; + let mboxes = Mboxes { + mboxes: imap_mboxes + .iter() + .map(|imap_mbox| Mbox { + delim: imap_mbox.delimiter().unwrap_or_default().into(), + name: imap_mbox.name().into(), + desc: imap_mbox + .attributes() + .iter() + .map(|attr| match attr { + NameAttribute::Marked => "Marked", + NameAttribute::Unmarked => "Unmarked", + NameAttribute::NoSelect => "NoSelect", + NameAttribute::NoInferiors => "NoInferiors", + NameAttribute::Custom(custom) => custom.trim_start_matches('\\'), + }) + .collect::>() + .join(", "), + }) + .collect(), + }; + + trace!("imap mailboxes: {:?}", mboxes); + trace!("<< get imap mailboxes"); + Ok(mboxes) } 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> { + 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 { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let range = if page_size > 0 { @@ -255,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))?; - let envelopes: ImapEnvelopes = fetches.try_into()?; - Ok(Box::new(envelopes)) + .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; + + let envelopes = from_imap_fetches(fetches)?; + Ok(envelopes) } fn search_envelopes( @@ -267,15 +291,15 @@ impl<'a> Backend<'a> for ImapBackend<'a> { sort: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { 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 { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let begin = page * page_size; @@ -283,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() @@ -295,56 +316,55 @@ 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() }; if seqs.is_empty() { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let range = seqs[begin..end.min(seqs.len())].join(","); let fetches = self .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") - .context(format!("cannot fetch messages within range {:?}", range))?; - let envelopes: ImapEnvelopes = fetches.try_into()?; - Ok(Box::new(envelopes)) + .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; + + let envelopes = from_imap_fetches(fetches)?; + Ok(envelopes) } - fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result> { - let flags: ImapFlags = flags.into(); + fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result { + let flags: Flags = flags.into(); self.sess()? .append(mbox, msg) - .flags(>>>::into(flags)) + .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(Box::new(last_seq)) + Ok(last_seq.to_string()) } 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; @@ -370,46 +390,52 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + 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(()) } fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + 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(()) } fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + 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/lib/src/backend/imap/imap_envelope.rs b/lib/src/backend/imap/imap_envelope.rs new file mode 100644 index 0000000..639d009 --- /dev/null +++ b/lib/src/backend/imap/imap_envelope.rs @@ -0,0 +1,78 @@ +//! IMAP envelope module. +//! +//! This module provides IMAP types and conversion utilities related +//! to the envelope. + +use rfc2047_decoder; + +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; + +pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { + let envelope = fetch + .envelope() + .ok_or_else(|| Error::GetEnvelopeError(fetch.message))?; + + let id = fetch.message.to_string(); + + let flags = from_imap_flags(fetch.flags()); + + let subject = envelope + .subject + .as_ref() + .map(|subj| { + rfc2047_decoder::decode(subj) + .map_err(|err| Error::DecodeSubjectError(err, fetch.message)) + }) + .unwrap_or_else(|| Ok(String::default()))?; + + let sender = envelope + .sender + .as_ref() + .and_then(|addrs| addrs.get(0)) + .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) + .ok_or_else(|| Error::GetSenderError(fetch.message))?; + let sender = if let Some(ref name) = sender.name { + rfc2047_decoder::decode(&name.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message))? + } else { + let mbox = sender + .mailbox + .as_ref() + .ok_or_else(|| Error::GetSenderError(fetch.message)) + .and_then(|mbox| { + rfc2047_decoder::decode(&mbox.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) + })?; + let host = sender + .host + .as_ref() + .ok_or_else(|| Error::GetSenderError(fetch.message)) + .and_then(|host| { + rfc2047_decoder::decode(&host.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) + })?; + format!("{}@{}", mbox, host) + }; + + let date = fetch + .internal_date() + .map(|date| date.naive_local().to_string()); + + Ok(Envelope { + id: id.clone(), + internal_id: id, + flags, + subject, + sender, + date, + }) +} 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/lib/src/backend/imap/imap_flag.rs b/lib/src/backend/imap/imap_flag.rs new file mode 100644 index 0000000..58ec612 --- /dev/null +++ b/lib/src/backend/imap/imap_flag.rs @@ -0,0 +1,15 @@ +use crate::msg::Flag; + +pub fn from_imap_flag(imap_flag: &imap::types::Flag<'_>) -> Flag { + match imap_flag { + imap::types::Flag::Seen => Flag::Seen, + imap::types::Flag::Answered => Flag::Answered, + imap::types::Flag::Flagged => Flag::Flagged, + imap::types::Flag::Deleted => Flag::Deleted, + imap::types::Flag::Draft => Flag::Draft, + imap::types::Flag::Recent => Flag::Recent, + imap::types::Flag::MayCreate => Flag::Custom(String::from("MayCreate")), + imap::types::Flag::Custom(flag) => Flag::Custom(flag.to_string()), + flag => Flag::Custom(flag.to_string()), + } +} diff --git a/lib/src/backend/imap/imap_flags.rs b/lib/src/backend/imap/imap_flags.rs new file mode 100644 index 0000000..3aa42d5 --- /dev/null +++ b/lib/src/backend/imap/imap_flags.rs @@ -0,0 +1,23 @@ +use crate::{ + backend::from_imap_flag, + msg::{Flag, Flags}, +}; + +pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec> { + flags + .iter() + .map(|flag| match flag { + Flag::Seen => imap::types::Flag::Seen, + Flag::Answered => imap::types::Flag::Answered, + Flag::Flagged => imap::types::Flag::Flagged, + Flag::Deleted => imap::types::Flag::Deleted, + Flag::Draft => imap::types::Flag::Draft, + Flag::Recent => imap::types::Flag::Recent, + Flag::Custom(flag) => imap::types::Flag::Custom(flag.into()), + }) + .collect() +} + +pub fn from_imap_flags(imap_flags: &[imap::types::Flag<'_>]) -> Flags { + imap_flags.iter().map(from_imap_flag).collect() +} 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/lib/src/backend/maildir/maildir_backend.rs b/lib/src/backend/maildir/maildir_backend.rs new file mode 100644 index 0000000..2c50328 --- /dev/null +++ b/lib/src/backend/maildir/maildir_backend.rs @@ -0,0 +1,356 @@ +//! Maildir backend module. +//! +//! This module contains the definition of the maildir backend and its +//! traits implementation. + +use log::{debug, info, trace}; +use std::{env, ffi::OsStr, fs, path::PathBuf}; + +use crate::{ + account::{Account, 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 Account, + mdir: maildir::Maildir, +} + +impl<'a> MaildirBackend<'a> { + pub fn new(account_config: &'a Account, maildir_config: &'a MaildirBackendConfig) -> Self { + Self { + account_config, + mdir: maildir_config.maildir_dir.clone().into(), + } + } + + fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result { + let path = if mdir_path.is_dir() { + Ok(mdir_path) + } else { + Err(MaildirError::ReadDirError(mdir_path.to_owned())) + }?; + Ok(path) + } + + /// Creates a maildir instance from a string slice. + pub fn get_mdir_from_dir(&self, dir: &str) -> Result { + let dir = self.account_config.get_mbox_alias(dir)?; + + // If the dir points to the inbox folder, creates a maildir + // instance from the root folder. + if &dir == "inbox" { + return self + .validate_mdir_path(self.mdir.path().to_owned()) + .map(maildir::Maildir::from); + } + + // If the dir is a valid maildir path, creates a maildir + // instance from it. First checks for absolute path, + self.validate_mdir_path((&dir).into()) + // 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() + .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 + // as described in the [spec]. + // + // [spec]: http://www.courier-mta.org/imap/README.maildirquota.html + self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir))) + }) + .map(maildir::Maildir::from) + } +} + +impl<'a> Backend<'a> for MaildirBackend<'a> { + fn add_mbox(&mut self, subdir: &str) -> Result<()> { + info!(">> add maildir subdir"); + debug!("subdir: {:?}", subdir); + + let path = self.mdir.path().join(format!(".{}", subdir)); + trace!("subdir path: {:?}", path); + + fs::create_dir(&path) + .map_err(|err| MaildirError::CreateSubdirError(err, subdir.to_owned()))?; + + info!("<< add maildir subdir"); + Ok(()) + } + + fn get_mboxes(&mut self) -> Result { + trace!(">> get maildir mailboxes"); + + let mut mboxes = Mboxes::default(); + for (name, desc) in &self.account_config.mailboxes { + mboxes.push(Mbox { + delim: String::from("/"), + name: name.into(), + desc: desc.into(), + }) + } + for entry in self.mdir.list_subdirs() { + 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(|| MaildirError::ParseSubdirError(dir.path().to_owned()))? + .into(), + ..Mbox::default() + }); + } + + trace!("maildir mailboxes: {:?}", mboxes); + trace!("<< get maildir mailboxes"); + Ok(mboxes) + } + + fn del_mbox(&mut self, dir: &str) -> Result<()> { + info!(">> delete maildir dir"); + debug!("dir: {:?}", dir); + + let path = self.mdir.path().join(format!(".{}", dir)); + trace!("dir path: {:?}", path); + + fs::remove_dir_all(&path) + .map_err(|err| MaildirError::DeleteAllDirError(err, path.to_owned()))?; + + info!("<< delete maildir dir"); + Ok(()) + } + + fn get_envelopes(&mut self, dir: &str, page_size: usize, page: usize) -> Result { + info!(">> get maildir envelopes"); + debug!("dir: {:?}", dir); + debug!("page size: {:?}", page_size); + debug!("page: {:?}", page); + + 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())?; + debug!("envelopes len: {:?}", envelopes.len()); + trace!("envelopes: {:?}", envelopes); + + // Calculates pagination boundaries. + let page_begin = page * page_size; + debug!("page begin: {:?}", page_begin); + if page_begin > envelopes.len() { + return Err(MaildirError::GetEnvelopesOutOfBoundsError(page_begin + 1))?; + } + let page_end = envelopes.len().min(page_begin + page_size); + debug!("page end: {:?}", page_end); + + // Sorts envelopes by most recent date. + envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); + + // Applies pagination boundaries. + envelopes.envelopes = envelopes[page_begin..page_end].to_owned(); + + // Appends envelopes hash to the id mapper cache file and + // calculates the new short hash length. The short hash length + // represents the minimum hash length possible to avoid + // conflicts. + let short_hash_len = { + let mut mapper = IdMapper::new(mdir.path())?; + let entries = envelopes + .iter() + .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) + .collect(); + mapper.append(entries)? + }; + debug!("short hash length: {:?}", short_hash_len); + + // Shorten envelopes hash. + envelopes + .iter_mut() + .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); + + info!("<< get maildir envelopes"); + Ok(envelopes) + } + + fn search_envelopes( + &mut self, + _dir: &str, + _query: &str, + _sort: &str, + _page_size: usize, + _page: usize, + ) -> Result { + info!(">> search maildir envelopes"); + info!("<< search maildir envelopes"); + Err(MaildirError::SearchEnvelopesUnimplementedError)? + } + + fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result { + info!(">> add maildir message"); + debug!("dir: {:?}", dir); + debug!("flags: {:?}", flags); + + let flags = Flags::from(flags); + debug!("flags: {:?}", flags); + + let mdir = self.get_mdir_from_dir(dir)?; + let id = mdir + .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())?; + mapper.append(vec![(hash.clone(), id.clone())])?; + + info!("<< add maildir message"); + Ok(hash) + } + + fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result { + info!(">> get maildir message"); + debug!("dir: {:?}", dir); + debug!("short hash: {:?}", short_hash); + + 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(|| 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"); + Ok(msg) + } + + fn copy_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { + info!(">> copy maildir message"); + debug!("source dir: {:?}", dir_src); + debug!("destination dir: {:?}", dir_dst); + + 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) + .map_err(MaildirError::CopyMsgError)?; + + // Appends hash entry to the id mapper cache file. + let mut mapper = IdMapper::new(mdir_dst.path())?; + let hash = format!("{:x}", md5::compute(&id)); + mapper.append(vec![(hash.clone(), id.clone())])?; + + info!("<< copy maildir message"); + Ok(()) + } + + fn move_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { + info!(">> move maildir message"); + debug!("source dir: {:?}", dir_src); + debug!("destination dir: {:?}", dir_dst); + + 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) + .map_err(MaildirError::MoveMsgError)?; + + // Appends hash entry to the id mapper cache file. + let mut mapper = IdMapper::new(mdir_dst.path())?; + let hash = format!("{:x}", md5::compute(&id)); + mapper.append(vec![(hash.clone(), id.clone())])?; + + info!("<< move maildir message"); + Ok(()) + } + + fn del_msg(&mut self, dir: &str, short_hash: &str) -> Result<()> { + info!(">> delete maildir message"); + debug!("dir: {:?}", dir); + debug!("short hash: {:?}", short_hash); + + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; + debug!("id: {:?}", id); + mdir.delete(&id).map_err(MaildirError::DelMsgError)?; + + info!("<< delete maildir message"); + Ok(()) + } + + fn add_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { + 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)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; + debug!("id: {:?}", id); + + mdir.add_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::AddFlagsError)?; + + info!("<< add maildir message flags"); + Ok(()) + } + + fn set_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { + 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)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; + debug!("id: {:?}", id); + mdir.set_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::SetFlagsError)?; + + info!("<< set maildir message flags"); + Ok(()) + } + + fn del_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { + 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)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; + debug!("id: {:?}", id); + mdir.remove_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::DelFlagsError)?; + + info!("<< delete maildir message flags"); + Ok(()) + } +} diff --git a/lib/src/backend/maildir/maildir_envelope.rs b/lib/src/backend/maildir/maildir_envelope.rs new file mode 100644 index 0000000..58966fd --- /dev/null +++ b/lib/src/backend/maildir/maildir_envelope.rs @@ -0,0 +1,72 @@ +use chrono::DateTime; +use log::trace; + +use crate::{ + 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; + +pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { + trace!(">> build envelope from maildir parsed mail"); + + let mut envelope = Envelope::default(); + + envelope.internal_id = entry.id().to_owned(); + envelope.id = format!("{:x}", md5::compute(&envelope.internal_id)); + envelope.flags = maildir_flags::from_maildir_entry(&entry); + + let parsed_mail = entry.parsed().map_err(MaildirError::ParseMsgError)?; + + trace!(">> parse headers"); + for h in parsed_mail.get_headers() { + let k = h.get_key(); + trace!("header key: {:?}", k); + + let v = rfc2047_decoder::decode(h.get_value_raw()) + .map_err(|err| MaildirError::DecodeHeaderError(err, k.to_owned()))?; + trace!("header value: {:?}", v); + + match k.to_lowercase().as_str() { + "date" => { + envelope.date = + DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0) + .map(|date| date.naive_local().to_string()) + .ok() + } + "subject" => { + envelope.subject = v.into(); + } + "from" => { + envelope.sender = from_slice_to_addrs(v) + .map_err(|err| MaildirError::ParseHeaderError(err, k.to_owned()))? + .and_then(|senders| { + if senders.is_empty() { + None + } else { + Some(senders) + } + }) + .map(|senders| match &senders[0] { + Addr::Single(mailparse::SingleInfo { display_name, addr }) => { + display_name.as_ref().unwrap_or_else(|| addr).to_owned() + } + Addr::Group(mailparse::GroupInfo { group_name, .. }) => { + group_name.to_owned() + } + }) + .ok_or_else(|| MaildirError::FindSenderError)?; + } + _ => (), + } + } + trace!("<< parse headers"); + + trace!("envelope: {:?}", envelope); + trace!("<< build envelope from maildir parsed mail"); + Ok(envelope) +} diff --git a/lib/src/backend/maildir/maildir_envelopes.rs b/lib/src/backend/maildir/maildir_envelopes.rs new file mode 100644 index 0000000..ff83a58 --- /dev/null +++ b/lib/src/backend/maildir/maildir_envelopes.rs @@ -0,0 +1,21 @@ +//! Maildir mailbox module. +//! +//! This module provides Maildir types and conversion utilities +//! related to the envelope. + +use crate::{backend::backend::Result, msg::Envelopes}; + +use super::{maildir_envelope, MaildirError}; + +/// 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 { + 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 55% rename from cli/src/backends/notmuch/notmuch_backend.rs rename to lib/src/backend/notmuch/notmuch_backend.rs index 37e559a..e249d46 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/lib/src/backend/notmuch/notmuch_backend.rs @@ -1,18 +1,18 @@ -use std::{convert::TryInto, fs}; - -use anyhow::{anyhow, Context, Result}; use log::{debug, info, trace}; +use std::fs; use crate::{ - backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, - config::{AccountConfig, NotmuchBackendConfig}, - mbox::Mboxes, + account::{Account, NotmuchBackendConfig}, + backend::{ + backend::Result, notmuch_envelopes, Backend, IdMapper, MaildirBackend, NotmuchError, + }, + mbox::{Mbox, Mboxes}, msg::{Envelopes, Msg}, }; /// Represents the Notmuch backend. pub struct NotmuchBackend<'a> { - account_config: &'a AccountConfig, + account_config: &'a Account, notmuch_config: &'a NotmuchBackendConfig, pub mdir: &'a mut MaildirBackend<'a>, db: notmuch::Database, @@ -20,7 +20,7 @@ pub struct NotmuchBackend<'a> { impl<'a> NotmuchBackend<'a> { pub fn new( - account_config: &'a AccountConfig, + account_config: &'a Account, notmuch_config: &'a NotmuchBackendConfig, mdir: &'a mut MaildirBackend<'a>, ) -> Result> { @@ -34,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"); @@ -51,17 +46,17 @@ impl<'a> NotmuchBackend<'a> { query: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { // Gets envelopes matching the given Notmuch query. let query_builder = self .db .create_query(query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let mut envelopes: NotmuchEnvelopes = query_builder - .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))? - .try_into() - .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); @@ -69,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); @@ -91,7 +83,7 @@ impl<'a> NotmuchBackend<'a> { let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?; let entries = envelopes .iter() - .map(|env| (env.hash.to_owned(), env.id.to_owned())) + .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) .collect(); mapper.append(entries)? }; @@ -100,9 +92,9 @@ impl<'a> NotmuchBackend<'a> { // Shorten envelopes hash. envelopes .iter_mut() - .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); - Ok(Box::new(envelopes)) + Ok(envelopes) } } @@ -110,33 +102,31 @@ 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> { - info!(">> get notmuch virtual mailboxes"); + fn get_mboxes(&mut self) -> Result { + trace!(">> get notmuch virtual mailboxes"); - let mut mboxes: Vec<_> = self - .account_config - .mailboxes - .iter() - .map(|(k, v)| NotmuchMbox::new(k, v)) - .collect(); - trace!("virtual mailboxes: {:?}", mboxes); + let mut mboxes = Mboxes::default(); + for (name, desc) in &self.account_config.mailboxes { + mboxes.push(Mbox { + name: name.into(), + desc: desc.into(), + ..Mbox::default() + }) + } mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); - info!("<< get notmuch virtual mailboxes"); - Ok(Box::new(NotmuchMboxes { mboxes })) + trace!("notmuch virtual mailboxes: {:?}", mboxes); + trace!("<< get notmuch virtual mailboxes"); + Ok(mboxes) } 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( @@ -144,7 +134,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { virt_mbox: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { info!(">> get notmuch envelopes"); debug!("virtual mailbox: {:?}", virt_mbox); debug!("page size: {:?}", page_size); @@ -170,7 +160,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { _sort: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { info!(">> search notmuch envelopes"); debug!("virtual mailbox: {:?}", virt_mbox); debug!("query: {:?}", query); @@ -193,61 +183,42 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(envelopes) } - fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result> { + fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result { info!(">> add notmuch envelopes"); debug!("tags: {:?}", tags); 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(Box::new(hash)) + Ok(hash) } fn get_msg(&mut self, _: &str, short_hash: &str) -> Result { @@ -255,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"); @@ -289,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<()> { @@ -307,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(()) @@ -338,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); @@ -354,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)?; } } @@ -375,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); @@ -391,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)?; } } @@ -415,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); @@ -431,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/lib/src/backend/notmuch/notmuch_envelope.rs b/lib/src/backend/notmuch/notmuch_envelope.rs new file mode 100644 index 0000000..6361a9a --- /dev/null +++ b/lib/src/backend/notmuch/notmuch_envelope.rs @@ -0,0 +1,73 @@ +//! Notmuch mailbox module. +//! +//! This module provides Notmuch types and conversion utilities +//! related to the envelope + +use chrono::DateTime; +use log::{info, trace}; + +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; + +pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { + info!("begin: try building envelope from notmuch parsed mail"); + + let internal_id = raw_envelope.id().to_string(); + let id = format!("{:x}", md5::compute(&internal_id)); + let subject = raw_envelope + .header("subject") + .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("subject")))? + .unwrap_or_default() + .to_string(); + let sender = raw_envelope + .header("from") + .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) + .map_err(|err| NotmuchError::ParseSendersError(err, sender.to_owned()))? + .and_then(|senders| { + if senders.is_empty() { + None + } else { + Some(senders) + } + }) + .map(|senders| match &senders[0] { + Addr::Single(mailparse::SingleInfo { display_name, addr }) => { + display_name.as_ref().unwrap_or_else(|| addr).to_owned() + } + Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), + }) + .ok_or_else(|| NotmuchError::FindSenderError)?; + let date = raw_envelope + .header("date") + .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) + .map_err(|err| NotmuchError::ParseMsgDateError(err, date.to_owned())) + .map(|date| date.naive_local().to_string()) + .ok(); + + let envelope = Envelope { + id, + internal_id, + flags: raw_envelope + .tags() + .map(|tag| Flag::Custom(tag.to_string())) + .collect(), + subject, + sender, + date, + }; + trace!("envelope: {:?}", envelope); + + info!("end: try building envelope from notmuch parsed mail"); + Ok(envelope) +} diff --git a/lib/src/backend/notmuch/notmuch_envelopes.rs b/lib/src/backend/notmuch/notmuch_envelopes.rs new file mode 100644 index 0000000..7bf1240 --- /dev/null +++ b/lib/src/backend/notmuch/notmuch_envelopes.rs @@ -0,0 +1,16 @@ +use crate::{backend::backend::Result, msg::Envelopes}; + +use super::notmuch_envelope; + +/// Represents a list of raw envelopees returned by the `notmuch` +/// crate. +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)?; + envelopes.push(envelope); + } + Ok(envelopes) +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 1b4a90c..ab692bc 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,8 +1,6 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} +mod process; + +pub mod account; +pub mod backend; +pub mod mbox; +pub mod msg; diff --git a/lib/src/mbox/mbox.rs b/lib/src/mbox/mbox.rs new file mode 100644 index 0000000..ceab669 --- /dev/null +++ b/lib/src/mbox/mbox.rs @@ -0,0 +1,23 @@ +//! Mailbox module. +//! +//! This module contains the representation of the mailbox. + +use serde::Serialize; +use std::fmt; + +/// Represents the mailbox. +#[derive(Debug, Default, PartialEq, Eq, Serialize)] +pub struct Mbox { + /// Represents the mailbox hierarchie delimiter. + pub delim: String, + /// Represents the mailbox name. + pub name: String, + /// Represents the mailbox description. + pub desc: String, +} + +impl fmt::Display for Mbox { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} diff --git a/lib/src/mbox/mboxes.rs b/lib/src/mbox/mboxes.rs new file mode 100644 index 0000000..0adca85 --- /dev/null +++ b/lib/src/mbox/mboxes.rs @@ -0,0 +1,29 @@ +//! Mailboxes module. +//! +//! This module contains the representation of the mailboxes. + +use serde::Serialize; +use std::ops; + +use super::Mbox; + +/// Represents the list of mailboxes. +#[derive(Debug, Default, Serialize)] +pub struct Mboxes { + #[serde(rename = "response")] + pub mboxes: Vec, +} + +impl ops::Deref for Mboxes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.mboxes + } +} + +impl ops::DerefMut for Mboxes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.mboxes + } +} diff --git a/lib/src/mbox/mod.rs b/lib/src/mbox/mod.rs new file mode 100644 index 0000000..25e70b5 --- /dev/null +++ b/lib/src/mbox/mod.rs @@ -0,0 +1,9 @@ +//! Mailbox module. +//! +//! This module contains everything related to mailboxes. + +mod mbox; +pub use mbox::*; + +mod mboxes; +pub use mboxes::*; 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/envelope.rs b/lib/src/msg/envelope.rs new file mode 100644 index 0000000..cc04ee3 --- /dev/null +++ b/lib/src/msg/envelope.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +use super::Flags; + +/// Represents the message envelope. The envelope is just a message +/// subset, and is mostly used for listings. +#[derive(Debug, Default, Clone, Serialize)] +pub struct Envelope { + /// Represents the message identifier. + pub id: String, + /// Represents the internal message identifier. + pub internal_id: String, + /// Represents the message flags. + pub flags: Flags, + /// Represents the subject of the message. + pub subject: String, + /// Represents the first sender of the message. + pub sender: String, + /// Represents the internal date of the message. + pub date: Option, +} diff --git a/lib/src/msg/envelopes.rs b/lib/src/msg/envelopes.rs new file mode 100644 index 0000000..9cf85c9 --- /dev/null +++ b/lib/src/msg/envelopes.rs @@ -0,0 +1,25 @@ +use serde::Serialize; +use std::ops; + +use super::Envelope; + +/// Represents the list of envelopes. +#[derive(Debug, Default, Serialize)] +pub struct Envelopes { + #[serde(rename = "response")] + pub envelopes: Vec, +} + +impl ops::Deref for Envelopes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.envelopes + } +} + +impl ops::DerefMut for Envelopes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.envelopes + } +} diff --git a/lib/src/msg/error.rs b/lib/src/msg/error.rs new file mode 100644 index 0000000..33d4473 --- /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::AccountError), + + #[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::AccountError), + + #[error("cannot delete local draft: {1}")] + DeleteLocalDraftError(#[source] io::Error, path::PathBuf), +} + +pub type Result = result::Result; diff --git a/lib/src/msg/flag.rs b/lib/src/msg/flag.rs new file mode 100644 index 0000000..1d37e18 --- /dev/null +++ b/lib/src/msg/flag.rs @@ -0,0 +1,27 @@ +use serde::Serialize; + +/// Represents the flag variants. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum Flag { + Seen, + Answered, + Flagged, + Deleted, + Draft, + Recent, + Custom(String), +} + +impl From<&str> for Flag { + fn from(flag_str: &str) -> Self { + match flag_str { + "seen" => Flag::Seen, + "answered" | "replied" => Flag::Answered, + "flagged" => Flag::Flagged, + "deleted" | "trashed" => Flag::Deleted, + "draft" => Flag::Draft, + "recent" => Flag::Recent, + flag => Flag::Custom(flag.into()), + } + } +} diff --git a/lib/src/msg/flags.rs b/lib/src/msg/flags.rs new file mode 100644 index 0000000..28faad1 --- /dev/null +++ b/lib/src/msg/flags.rs @@ -0,0 +1,88 @@ +use serde::Serialize; +use std::{fmt, ops}; + +use super::Flag; + +/// Represents the list of flags. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +pub struct Flags(pub Vec); + +impl Flags { + /// Builds a symbols string. + pub fn to_symbols_string(&self) -> String { + let mut flags = String::new(); + flags.push_str(if self.contains(&Flag::Seen) { + " " + } else { + "✷" + }); + flags.push_str(if self.contains(&Flag::Answered) { + "↵" + } else { + " " + }); + flags.push_str(if self.contains(&Flag::Flagged) { + "⚑" + } else { + " " + }); + flags + } +} + +impl ops::Deref for Flags { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for Flags { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Display for Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut glue = ""; + + for flag in &self.0 { + write!(f, "{}", glue)?; + match flag { + Flag::Seen => write!(f, "\\Seen")?, + Flag::Answered => write!(f, "\\Answered")?, + Flag::Flagged => write!(f, "\\Flagged")?, + Flag::Deleted => write!(f, "\\Deleted")?, + Flag::Draft => write!(f, "\\Draft")?, + Flag::Recent => write!(f, "\\Recent")?, + Flag::Custom(flag) => write!(f, "{}", flag)?, + } + glue = " "; + } + + Ok(()) + } +} + +impl From<&str> for Flags { + fn from(flags: &str) -> Self { + Flags( + flags + .split_whitespace() + .map(|flag| flag.trim().into()) + .collect(), + ) + } +} + +impl FromIterator for Flags { + fn from_iter>(iter: T) -> Self { + let mut flags = Flags::default(); + for flag in iter { + flags.push(flag); + } + flags + } +} diff --git a/lib/src/msg/mod.rs b/lib/src/msg/mod.rs new file mode 100644 index 0000000..9470357 --- /dev/null +++ b/lib/src/msg/mod.rs @@ -0,0 +1,29 @@ +mod error; +pub use error::*; + +mod flag; +pub use flag::*; + +mod flags; +pub use flags::*; + +mod envelope; +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 82% rename from cli/src/msg/msg_entity.rs rename to lib/src/msg/msg.rs index ffe87b6..264624c 100644 --- a/cli/src/msg/msg_entity.rs +++ b/lib/src/msg/msg.rs @@ -1,5 +1,4 @@ use ammonia; -use anyhow::{anyhow, Context, Error, Result}; use chrono::{DateTime, Local, TimeZone, Utc}; use convert_case::{Case, Casing}; use html_escape; @@ -14,20 +13,14 @@ use std::{ fs, path::PathBuf, }; +use tree_magic; use uuid::Uuid; use crate::{ - backends::Backend, - config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM}, + account::{Account, 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, }, }; @@ -173,7 +166,7 @@ impl Msg { } } - pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result { + pub fn into_reply(mut self, all: bool, account: &Account) -> Result { let account_addr = account.address()?; // In-Reply-To @@ -271,7 +264,7 @@ impl Msg { Ok(self) } - pub fn into_forward(mut self, account: &AccountConfig) -> Result { + pub fn into_forward(mut self, account: &Account) -> Result { let account_addr = account.address()?; let prev_subject = self.subject.to_owned(); @@ -327,99 +320,6 @@ impl Msg { Ok(self) } - fn _edit_with_editor(&self, account: &AccountConfig) -> Result { - let tpl = self.to_tpl(TplOverride::default(), 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, - 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(account)?); - break; - } - PreEditChoice::Quit => return Ok(backend), - }, - Err(err) => { - println!("{}", err); - continue; - } - } - } - } else { - self.merge_with(self._edit_with_editor(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(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 @@ -428,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 { @@ -479,7 +380,7 @@ impl Msg { } } - pub fn to_tpl(&self, opts: TplOverride, account: &AccountConfig) -> Result { + pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> Result { let account_addr: Addrs = vec![account.address()?].into(); let mut tpl = String::default(); @@ -559,13 +460,13 @@ 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()) + Self::from_parsed_mail(parsed_mail, &Account::default()) } - pub fn into_sendable_msg(&self, account: &AccountConfig) -> Result { + pub fn into_sendable_msg(&self, account: &Account) -> Result { let mut msg_builder = lettre::Message::builder() .message_id(self.message_id.to_owned()) .subject(self.subject.to_owned()); @@ -610,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 @@ -621,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( @@ -647,12 +546,12 @@ impl Msg { msg_builder .multipart(multipart) - .context("cannot build sendable message") + .map_err(Error::BuildSendableMsgError) } pub fn from_parsed_mail( parsed_mail: mailparse::ParsedMail<'_>, - config: &AccountConfig, + config: &Account, ) -> Result { trace!(">> build message from parsed mail"); trace!("parsed mail: {:?}", parsed_mail); @@ -683,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); @@ -709,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"); @@ -725,7 +623,7 @@ impl Msg { &self, text_mime: &str, headers: Vec<&str>, - config: &AccountConfig, + config: &Account, ) -> Result { let mut all_headers = vec![]; for h in config.read_headers.iter() { @@ -837,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)?) } } @@ -852,10 +750,10 @@ mod tests { #[test] fn test_into_reply() { - let config = AccountConfig { + let config = Account { display_name: "Test".into(), email: "test-account@local".into(), - ..AccountConfig::default() + ..Account::default() }; // Checks that: @@ -991,7 +889,7 @@ mod tests { #[test] fn test_to_readable() { - let config = AccountConfig::default(); + let config = Account::default(); let msg = Msg { parts: Parts(vec![Part::TextPlain(TextPlainPart { content: String::from("hello, world!"), @@ -1054,14 +952,14 @@ mod tests { .unwrap() ); - let config = AccountConfig { + let config = Account { read_headers: vec![ "CusTOM-heaDER".into(), "Subject".into(), "from".into(), "cc".into(), ], - ..AccountConfig::default() + ..Account::default() }; // header present but empty in msg headers, empty config assert_eq!( 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 75% rename from cli/src/msg/parts_entity.rs rename to lib/src/msg/parts.rs index d4d0640..b64c01a 100644 --- a/cli/src/msg/parts_entity.rs +++ b/lib/src/msg/parts.rs @@ -1,4 +1,3 @@ -use anyhow::{anyhow, Context, Result}; use mailparse::MailHeaderMap; use serde::Serialize; use std::{ @@ -7,7 +6,7 @@ use std::{ }; use uuid::Uuid; -use crate::config::AccountConfig; +use crate::{account::Account, msg}; #[derive(Debug, Clone, Default, Serialize)] pub struct TextPlainPart { @@ -51,11 +50,17 @@ impl Parts { } pub fn from_parsed_mail<'a>( - account: &'a AccountConfig, + account: &'a Account, part: &'a mailparse::ParsedMail<'a>, - ) -> Result { + ) -> msg::Result { let mut parts = vec![]; - build_parts_map_rec(account, part, &mut parts)?; + if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() + { + let content = part.get_body().unwrap_or_default(); + parts.push(Part::TextPlain(TextPlainPart { content })) + } else { + build_parts_map_rec(account, part, &mut parts)?; + } Ok(Self(parts)) } } @@ -75,10 +80,10 @@ impl DerefMut for Parts { } fn build_parts_map_rec( - account: &AccountConfig, + account: &Account, 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 { @@ -105,23 +110,22 @@ fn build_parts_map_rec( } else if ctype.starts_with("text/html") { parts.push(Part::TextHtml(TextHtmlPart { content })) } - }; + } } }; } else { 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() { @@ -133,14 +137,14 @@ fn build_parts_map_rec( Ok(()) } -fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result { +fn decrypt_part(account: &Account, 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 new file mode 100644 index 0000000..c9e282c --- /dev/null +++ b/lib/src/process.rs @@ -0,0 +1,34 @@ +//! Process module. +//! +//! This module contains cross platform helpers around the +//! `std::process` crate. + +use log::{debug, trace}; +use std::{io, process::Command, string}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ProcessError { + #[error("cannot run command {1:?}")] + RunCmdError(#[source] io::Error, String), + + #[error("cannot parse command output")] + ParseCmdOutputError(#[source] string::FromUtf8Error), +} + +pub fn run(cmd: &str) -> Result { + debug!(">> run command"); + debug!("command: {}", cmd); + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(&["/C", cmd]).output() + } else { + Command::new("sh").arg("-c").arg(cmd).output() + }; + let output = output.map_err(|err| ProcessError::RunCmdError(err, cmd.to_string()))?; + let output = String::from_utf8(output.stdout).map_err(ProcessError::ParseCmdOutputError)?; + + trace!("command output: {}", output); + debug!("<< 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 81% rename from tests/test_imap_backend.rs rename to lib/tests/test_imap_backend.rs index d0d5035..081d045 100644 --- a/tests/test_imap_backend.rs +++ b/lib/tests/test_imap_backend.rs @@ -1,21 +1,21 @@ #[cfg(feature = "imap-backend")] -use himalaya::{ - backends::{Backend, ImapBackend, ImapEnvelopes}, - config::{AccountConfig, ImapBackendConfig}, +use himalaya_lib::{ + account::{Account, ImapBackendConfig}, + backend::{Backend, ImapBackend}, }; #[cfg(feature = "imap-backend")] #[test] fn test_imap_backend() { // configure accounts - let account_config = AccountConfig { + let account_config = Account { smtp_host: "localhost".into(), smtp_port: 3465, smtp_starttls: false, smtp_insecure: true, smtp_login: "inbox@localhost".into(), smtp_passwd_cmd: "echo 'password'".into(), - ..AccountConfig::default() + ..Account::default() }; let imap_config = ImapBackendConfig { imap_host: "localhost".into(), @@ -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 65% rename from tests/test_maildir_backend.rs rename to lib/tests/test_maildir_backend.rs index 11eaa52..8077aaf 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::{Account, MaildirBackendConfig}, + backend::{Backend, MaildirBackend}, + msg::Flag, }; #[test] @@ -18,9 +19,9 @@ fn test_maildir_backend() { mdir_sub.create_dirs().unwrap(); // configure accounts - let account_config = AccountConfig { + let account_config = Account { mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]), - ..AccountConfig::default() + ..Account::default() }; let mdir_config = MaildirBackendConfig { maildir_dir: mdir.path().to_owned(), @@ -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 65% rename from tests/test_notmuch_backend.rs rename to lib/tests/test_notmuch_backend.rs index 161fb41..dae4e43 100644 --- a/tests/test_notmuch_backend.rs +++ b/lib/tests/test_notmuch_backend.rs @@ -2,14 +2,16 @@ use std::{collections::HashMap, env, fs, iter::FromIterator}; #[cfg(feature = "notmuch-backend")] -use himalaya::{ - backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes}, - config::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}, +use himalaya_lib::{ + account::{Account, MaildirBackendConfig, NotmuchBackendConfig}, + backend::{Backend, MaildirBackend, NotmuchBackend}, }; #[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()) {} @@ -42,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); @@ -50,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(); diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/vim/autoload/himalaya/msg.vim b/vim/autoload/himalaya/msg.vim index 9384c9d..e9e03b8 100644 --- a/vim/autoload/himalaya/msg.vim +++ b/vim/autoload/himalaya/msg.vim @@ -3,7 +3,7 @@ let s:trim = function("himalaya#shared#utils#trim") let s:cli = function("himalaya#shared#cli#call") let s:plain_req = function("himalaya#request#plain") -let s:msg_id = 0 +let s:msg_id = "" let s:draft = "" let s:attachment_paths = [] @@ -47,16 +47,17 @@ function! himalaya#msg#read() try let pos = getpos(".") let s:msg_id = s:get_focused_msg_id() + if empty(s:msg_id) || s:msg_id == "HASH" | return | endif let account = himalaya#account#curr() let mbox = himalaya#mbox#curr_mbox() let msg = s:cli( - \"--account %s --mailbox %s read %d", + \"--account %s --mailbox %s read %s", \[shellescape(account), shellescape(mbox), s:msg_id], - \printf("Fetching message %d", s:msg_id), + \printf("Fetching message %s", s:msg_id), \1, \) call s:close_open_buffers('Himalaya read message') - execute printf("silent! botright new Himalaya read message [%d]", s:msg_id) + execute printf("silent! botright new Himalaya read message [%s]", s:msg_id) setlocal modifiable silent execute "%d" call append(0, split(substitute(msg, "\r", "", "g"), "\n")) @@ -98,12 +99,12 @@ function! himalaya#msg#reply() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s template reply %d", + \"--account %s --mailbox %s template reply %s", \[shellescape(account), shellescape(mbox), msg_id], \"Fetching reply template", \0, \) - execute printf("silent! edit Himalaya reply [%d]", msg_id) + execute printf("silent! edit Himalaya reply [%s]", msg_id) call append(0, split(substitute(msg, "\r", "", "g"), "\n")) silent execute "$d" setlocal filetype=himalaya-msg-write @@ -124,12 +125,12 @@ function! himalaya#msg#reply_all() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s template reply %d --all", + \"--account %s --mailbox %s template reply %s --all", \[shellescape(account), shellescape(mbox), msg_id], \"Fetching reply all template", \0 \) - execute printf("silent! edit Himalaya reply all [%d]", msg_id) + execute printf("silent! edit Himalaya reply all [%s]", msg_id) call append(0, split(substitute(msg, "\r", "", "g"), "\n")) silent execute "$d" setlocal filetype=himalaya-msg-write @@ -150,12 +151,12 @@ function! himalaya#msg#forward() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s template forward %d", + \"--account %s --mailbox %s template forward %s", \[shellescape(account), shellescape(mbox), msg_id], \"Fetching forward template", \0 \) - execute printf("silent! edit Himalaya forward [%d]", msg_id) + execute printf("silent! edit Himalaya forward [%s]", msg_id) call append(0, split(substitute(msg, "\r", "", "g"), "\n")) silent execute "$d" setlocal filetype=himalaya-msg-write @@ -180,7 +181,7 @@ function! himalaya#msg#_copy(target_mbox) let account = himalaya#account#curr() let source_mbox = himalaya#mbox#curr_mbox() let msg = s:cli( - \"--account %s --mailbox %s copy %d %s", + \"--account %s --mailbox %s copy %s %s", \[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)], \"Copying message", \1, @@ -201,14 +202,14 @@ endfunction function! himalaya#msg#_move(target_mbox) try let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let choice = input(printf("Are you sure you want to move the message %d? (y/N) ", msg_id)) + let choice = input(printf("Are you sure you want to move the message %s? (y/N) ", msg_id)) redraw | echo if choice != "y" | return | endif let pos = getpos(".") let account = himalaya#account#curr() let source_mbox = himalaya#mbox#curr_mbox() let msg = s:cli( - \"--account %s --mailbox %s move %d %s", + \"--account %s --mailbox %s move %s %s", \[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)], \"Moving message", \1, @@ -294,7 +295,7 @@ function! himalaya#msg#attachments() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s attachments %d", + \"--account %s --mailbox %s attachments %s", \[shellescape(account), shellescape(mbox), msg_id], \"Downloading attachments", \0 @@ -375,7 +376,7 @@ function! s:bufwidth() endfunction function! s:get_msg_id(line) - return matchstr(a:line, '[0-9]*') + return matchstr(a:line, '[0-9a-zA-Z]*') endfunction function! s:get_focused_msg_id()