diff --git a/Cargo.lock b/Cargo.lock index 3f41163..3d0a101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", ] @@ -183,6 +183,41 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -281,6 +316,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "from_variants" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221a1eb1a3c98980bc1b740f462b3dcf73f4e371cda294986bac72497995a4e3" +dependencies = [ + "from_variants_impl", +] + +[[package]] +name = "from_variants_impl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e08079fa3c89edec9160ceaa9e7172785468c26c053d12924cce0d5a55c241a" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "funty" version = "1.1.0" @@ -397,6 +453,7 @@ dependencies = [ "maildir", "mailparse", "native-tls", + "notmuch", "regex", "rfc2047-decoder", "serde", @@ -457,6 +514,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -732,6 +795,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "notmuch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0941fd9af5b8529e3d42494f56efafb909b76190a7a454cde9d6e397390cf9" +dependencies = [ + "from_variants", + "libc", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1241,6 +1314,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "syn" version = "1.0.81" diff --git a/Cargo.toml b/Cargo.toml index ba79a24..240dd9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ log = "0.4.14" maildir = "0.6.0" mailparse = "0.13.6" native-tls = "0.2.8" +notmuch = "0.7.1" regex = "1.5.4" rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs new file mode 100644 index 0000000..30ed63e --- /dev/null +++ b/src/backends/notmuch/notmuch_backend.rs @@ -0,0 +1,101 @@ +use std::convert::TryInto; + +use anyhow::{Context, Result}; + +use crate::{ + backends::Backend, + config::{AccountConfig, NotmuchBackendConfig}, + mbox::Mboxes, + msg::{Envelopes, Msg}, +}; + +use super::NotmuchEnvelopes; + +pub struct NotmuchBackend<'a> { + account_config: &'a AccountConfig, + db: notmuch::Database, +} + +impl<'a> NotmuchBackend<'a> { + pub fn new( + account_config: &'a AccountConfig, + notmuch_config: &'a NotmuchBackendConfig, + ) -> Result { + Ok(Self { + account_config, + db: notmuch::Database::open( + notmuch_config.notmuch_database_dir.clone(), + notmuch::DatabaseMode::ReadWrite, + ) + .context(format!( + "cannot open notmuch database at {:?}", + notmuch_config.notmuch_database_dir + ))?, + }) + } +} + +impl<'a> Backend<'a> for NotmuchBackend<'a> { + fn add_mbox(&mut self, mdir: &str) -> Result<()> { + unimplemented!(); + } + + fn get_mboxes(&mut self) -> Result> { + unimplemented!(); + } + + fn del_mbox(&mut self, mdir: &str) -> Result<()> { + unimplemented!(); + } + + fn get_envelopes( + &mut self, + mdir: &str, + _sort: &str, + filter: &str, + page_size: usize, + page: usize, + ) -> Result> { + let query = self + .db + .create_query(filter) + .context("cannot create query")?; + let msgs: NotmuchEnvelopes = query + .search_messages() + .context("cannot get messages")? + .try_into()?; + Ok(Box::new(msgs)) + } + + fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result> { + unimplemented!(); + } + + fn get_msg(&mut self, mdir: &str, id: &str) -> Result { + unimplemented!(); + } + + fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + unimplemented!(); + } + + fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + unimplemented!(); + } + + fn del_msg(&mut self, mdir: &str, id: &str) -> Result<()> { + unimplemented!(); + } + + fn add_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + unimplemented!(); + } + + fn set_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + unimplemented!(); + } + + fn del_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + unimplemented!(); + } +} diff --git a/src/backends/notmuch/notmuch_envelope.rs b/src/backends/notmuch/notmuch_envelope.rs new file mode 100644 index 0000000..559191c --- /dev/null +++ b/src/backends/notmuch/notmuch_envelope.rs @@ -0,0 +1,172 @@ +//! 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(pub Vec); + +impl Deref for NotmuchEnvelopes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for NotmuchEnvelopes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl PrintTable for NotmuchEnvelopes { + fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writter)?; + Table::print(writter, self, opts)?; + writeln!(writter)?; + 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 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("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 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(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 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().trim().to_string(); + 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, + 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/src/config/account_config.rs b/src/config/account_config.rs index 03bcc6a..81c1fa6 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -73,6 +73,9 @@ impl<'a> AccountConfig { DeserializedAccountConfig::Maildir(account) => { account.default.unwrap_or_default() } + DeserializedAccountConfig::Notmuch(account) => { + account.default.unwrap_or_default() + } }) .map(|(name, account)| (name.to_owned(), account)) .ok_or_else(|| anyhow!("cannot find default account")), @@ -194,6 +197,13 @@ impl<'a> AccountConfig { maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(), }) } + DeserializedAccountConfig::Notmuch(config) => { + BackendConfig::Notmuch(NotmuchBackendConfig { + notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)? + .to_string() + .into(), + }) + } }; trace!("backend config: {:?}", backend_config); @@ -321,6 +331,7 @@ impl<'a> AccountConfig { pub enum BackendConfig { Imap(ImapBackendConfig), Maildir(MaildirBackendConfig), + Notmuch(NotmuchBackendConfig), } /// Represents the IMAP backend. @@ -358,6 +369,13 @@ pub struct MaildirBackendConfig { pub maildir_dir: PathBuf, } +/// Represents the Notmuch backend. +#[derive(Debug, Default, Clone)] +pub struct NotmuchBackendConfig { + /// Represents the Notmuch database path. + pub notmuch_database_dir: PathBuf, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index becfa81..024cfca 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -11,6 +11,7 @@ pub trait ToDeserializedBaseAccountConfig { pub enum DeserializedAccountConfig { Imap(DeserializedImapAccountConfig), Maildir(DeserializedMaildirAccountConfig), + Notmuch(DeserializedNotmuchAccountConfig), } impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig { @@ -18,6 +19,7 @@ impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig { match self { Self::Imap(config) => config.to_base(), Self::Maildir(config) => config.to_base(), + Self::Notmuch(config) => config.to_base(), } } } @@ -122,3 +124,8 @@ make_account_config!( ); make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String); + +make_account_config!( + DeserializedNotmuchAccountConfig, + notmuch_database_dir: String +); diff --git a/src/lib.rs b/src/lib.rs index 625792f..898f536 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,15 @@ pub mod backends { pub mod maildir_flag; pub use maildir_flag::*; } + + pub use self::notmuch::*; + pub mod notmuch { + pub mod notmuch_backend; + pub use notmuch_backend::*; + + pub mod notmuch_envelope; + pub use notmuch_envelope::*; + } } pub mod smtp { diff --git a/src/main.rs b/src/main.rs index c11fee0..e880ee3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::{convert::TryFrom, env}; use url::Url; use himalaya::{ - backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend}, + backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend, NotmuchBackend}, compl::{compl_arg, compl_handler}, config::{account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig}, mbox::{mbox_arg, mbox_handler}, @@ -45,6 +45,7 @@ fn main() -> Result<()> { let mut imap; let mut maildir; + let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { BackendConfig::Imap(ref imap_config) => { imap = ImapBackend::new(&account_config, imap_config); @@ -54,6 +55,10 @@ fn main() -> Result<()> { maildir = MaildirBackend::new(&account_config, maildir_config); Box::new(&mut maildir) } + BackendConfig::Notmuch(ref notmuch_config) => { + notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; + Box::new(&mut notmuch) + } }; return msg_handler::mailto(&url, &account_config, &mut printer, backend, &mut smtp); @@ -81,6 +86,7 @@ fn main() -> Result<()> { let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut imap; let mut maildir; + let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { BackendConfig::Imap(ref imap_config) => { imap = ImapBackend::new(&account_config, imap_config); @@ -90,6 +96,10 @@ fn main() -> Result<()> { maildir = MaildirBackend::new(&account_config, maildir_config); Box::new(&mut maildir) } + BackendConfig::Notmuch(ref notmuch_config) => { + notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; + Box::new(&mut notmuch) + } }; let mut smtp = LettreService::from(&account_config);