/* * meli - backends module * * Copyright 2017 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ pub mod utf7; use std::{ any::Any, borrow::Cow, collections::{BTreeSet, HashMap}, future::Future, ops::Deref, pin::Pin, sync::Arc, }; use futures::stream::Stream; use smallvec::SmallVec; use super::email::{Envelope, EnvelopeHash, Flag}; use crate::{ conf::AccountSettings, error::{Error, ErrorKind, Result}, HeaderName, LogLevel, }; #[macro_export] macro_rules! get_path_hash { ($path:expr) => {{ use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, }; let mut hasher = DefaultHasher::new(); $path.hash(&mut hasher); hasher.finish() }}; } pub type BackendCreator = Box< dyn Fn( &AccountSettings, Box bool + Send + Sync>, BackendEventConsumer, ) -> Result>, >; pub type BackendValidateConfigFn = Box Result<()>>; /// A hashmap containing all available mail backends. /// An abstraction over any available backends. pub struct Backends { map: HashMap, } pub struct Backend { pub create_fn: Box BackendCreator>, pub validate_conf_fn: BackendValidateConfigFn, } impl Default for Backends { fn default() -> Self { Self::new() } } #[cfg(feature = "notmuch")] pub const NOTMUCH_ERROR_MSG: &str = "libnotmuch5 was not found in your system. Make sure it is \ installed and in the library paths. For a custom file path, \ use `library_file_path` setting in your notmuch account.\n"; #[cfg(not(feature = "notmuch"))] pub const NOTMUCH_ERROR_MSG: &str = "this version of meli is not compiled with notmuch support. \ Use an appropriate version and make sure libnotmuch5 is \ installed and in the library paths.\n"; #[cfg(not(feature = "notmuch"))] pub const NOTMUCH_ERROR_DETAILS: &str = ""; #[cfg(all(feature = "notmuch", target_os = "unix"))] pub const NOTMUCH_ERROR_DETAILS: &str = r#"If you have installed the library manually, try setting the `LD_LIBRARY_PATH` environment variable to its `lib` directory. Otherwise, set it to the location of libnotmuch.5.so. Example: LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/path/to/notmuch/lib" meli or, put this in your shell init script (.bashenv, .zshenv, .bashrc, .zshrc, .profile): export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/path/to/notmuch/lib" You can also set any location by specifying the library file path with the configuration flag `library_file_path`."#; #[cfg(all(feature = "notmuch", target_os = "macos"))] pub const NOTMUCH_ERROR_DETAILS: &str = r#"If you have installed the library via homebrew, try setting the `DYLD_LIBRARY_PATH` environment variable to its `lib` directory. Otherwise, set it to the location of libnotmuch.5.dylib. Example: DYLD_LIBRARY_PATH="$(brew --prefix)/lib" meli or, put this in your shell init script (.bashenv, .zshenv, .bashrc, .zshrc, .profile): export DYLD_LIBRARY_PATH="$(brew --prefix)/lib" Make sure to append to DYLD_LIBRARY_PATH if it's not empty, by prepending a colon to the libnotmuch5.dylib location: export DYLD_LIBRARY_PATH="$DYLD_LIBRARY_PATH:$(brew --prefix)/lib" You can also set any location by specifying the library file path with the configuration flag `library_file_path`."#; #[cfg(all(feature = "notmuch", not(any(target_os = "unix", target_os = "macos"))))] pub const NOTMUCH_ERROR_DETAILS: &str = r#"If notmuch is installed but the library isn't found, consult your system's documentation on how to make dynamic libraries discoverable."#; impl Backends { pub fn new() -> Self { let mut b = Self { map: HashMap::with_capacity_and_hasher(1, Default::default()), }; #[cfg(feature = "maildir")] { use crate::maildir::MaildirType; b.register( "maildir".to_string(), Backend { create_fn: Box::new(|| Box::new(|f, i, ev| MaildirType::new(f, i, ev))), validate_conf_fn: Box::new(MaildirType::validate_config), }, ); } #[cfg(feature = "mbox")] { use crate::mbox::MboxType; b.register( "mbox".to_string(), Backend { create_fn: Box::new(|| Box::new(|f, i, ev| MboxType::new(f, i, ev))), validate_conf_fn: Box::new(MboxType::validate_config), }, ); } #[cfg(feature = "imap")] { use crate::imap::ImapType; b.register( "imap".to_string(), Backend { create_fn: Box::new(|| Box::new(|f, i, ev| ImapType::new(f, i, ev))), validate_conf_fn: Box::new(ImapType::validate_config), }, ); } #[cfg(feature = "nntp")] { use crate::nntp::NntpType; b.register( "nntp".to_string(), Backend { create_fn: Box::new(|| Box::new(|f, i, ev| NntpType::new(f, i, ev))), validate_conf_fn: Box::new(NntpType::validate_config), }, ); } #[cfg(feature = "notmuch")] { use crate::notmuch::NotmuchDb; b.register( "notmuch".to_string(), Backend { create_fn: Box::new(|| Box::new(|f, i, ev| NotmuchDb::new(f, i, ev))), validate_conf_fn: Box::new(NotmuchDb::validate_config), }, ); } #[cfg(feature = "jmap")] { use crate::jmap::JmapType; b.register( "jmap".to_string(), Backend { create_fn: Box::new(|| Box::new(|f, i, ev| JmapType::new(f, i, ev))), validate_conf_fn: Box::new(JmapType::validate_config), }, ); } b } pub fn get(&self, key: &str) -> BackendCreator { if !self.map.contains_key(key) { if key == "notmuch" { eprint!("{}", NOTMUCH_ERROR_MSG); #[cfg(feature = "notmuch")] { eprint!("{}", NOTMUCH_ERROR_DETAILS); } } panic!("{} is not a valid mail backend", key); } (self.map[key].create_fn)() } pub fn register(&mut self, key: String, backend: Backend) { if self.map.contains_key(&key) { panic!("{} is an already registered backend", key); } self.map.insert(key, backend); } pub fn validate_config(&self, key: &str, s: &mut AccountSettings) -> Result<()> { (self .map .get(key) .ok_or_else(|| { Error::new(format!( "{}{} is not a valid mail backend. {}", if key == "notmuch" { NOTMUCH_ERROR_MSG } else { "" }, key, if cfg!(feature = "notmuch") && key == "notmuch" { NOTMUCH_ERROR_DETAILS } else { "" }, )) })? .validate_conf_fn)(s) } } #[derive(Clone, Debug)] pub enum BackendEvent { Notice { description: String, content: Option, level: LogLevel, }, Refresh(RefreshEvent), AccountStateChange { message: Cow<'static, str>, }, //Job(Box> + Send + 'static>) } impl From for BackendEvent { fn from(val: Error) -> Self { Self::Notice { description: val.summary.to_string(), content: Some(val.to_string()), level: LogLevel::ERROR, } } } #[derive(Clone, Debug)] pub enum RefreshEventKind { Update(EnvelopeHash, Box), /// Rename(old_hash, new_hash) Rename(EnvelopeHash, EnvelopeHash), Create(Box), Remove(EnvelopeHash), NewFlags(EnvelopeHash, (Flag, Vec)), Rescan, Failure(Error), MailboxCreate(Mailbox), MailboxDelete(MailboxHash), MailboxRename { old_mailbox_hash: MailboxHash, new_mailbox: Mailbox, }, MailboxSubscribe(MailboxHash), MailboxUnsubscribe(MailboxHash), } #[derive(Clone, Debug)] pub struct RefreshEvent { pub mailbox_hash: MailboxHash, pub account_hash: AccountHash, pub kind: RefreshEventKind, } #[derive(Clone)] pub struct BackendEventConsumer(Arc); impl BackendEventConsumer { pub fn new(b: Arc) -> Self { Self(b) } } impl std::fmt::Debug for BackendEventConsumer { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "BackendEventConsumer") } } impl Deref for BackendEventConsumer { type Target = dyn Fn(AccountHash, BackendEvent) + Send + Sync; fn deref(&self) -> &Self::Target { &(*self.0) } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum FlagOp { Set(Flag), SetTag(String), UnSet(Flag), UnSetTag(String), } impl From<&FlagOp> for bool { fn from(val: &FlagOp) -> Self { matches!(val, FlagOp::Set(_) | FlagOp::SetTag(_)) } } impl FlagOp { #[inline] pub fn is_flag(&self) -> bool { matches!(self, Self::Set(_) | Self::UnSet(_)) } #[inline] pub fn is_tag(&self) -> bool { matches!(self, Self::SetTag(_) | Self::UnSetTag(_)) } #[inline] pub fn as_bool(&self) -> bool { self.into() } } #[derive(Clone, Debug)] pub struct MailBackendCapabilities { pub is_async: bool, pub is_remote: bool, pub extensions: Option>, pub supports_search: bool, pub supports_tags: bool, pub supports_submission: bool, pub extra_submission_headers: &'static [HeaderName], } #[derive(Clone, Copy, Debug)] pub enum MailBackendExtensionStatus { Unsupported { comment: Option<&'static str> }, Supported { comment: Option<&'static str> }, Enabled { comment: Option<&'static str> }, } pub type ResultFuture = Result> + Send + 'static>>>; pub trait MailBackend: ::std::fmt::Debug + Send + Sync { fn capabilities(&self) -> MailBackendCapabilities; fn is_online(&self) -> ResultFuture<()> { Ok(Box::pin(async { Ok(()) })) } #[allow(clippy::type_complexity)] fn fetch( &mut self, mailbox_hash: MailboxHash, ) -> Result>> + Send + 'static>>>; fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()>; fn watch(&self) -> ResultFuture<()>; fn mailboxes(&self) -> ResultFuture>; fn operation(&self, hash: EnvelopeHash) -> Result>; fn save( &self, bytes: Vec, mailbox_hash: MailboxHash, flags: Option, ) -> ResultFuture<()>; fn copy_messages( &mut self, env_hashes: EnvelopeHashBatch, source_mailbox_hash: MailboxHash, destination_mailbox_hash: MailboxHash, move_: bool, ) -> ResultFuture<()>; fn set_flags( &mut self, env_hashes: EnvelopeHashBatch, mailbox_hash: MailboxHash, flags: SmallVec<[FlagOp; 8]>, ) -> ResultFuture<()>; fn delete_messages( &mut self, env_hashes: EnvelopeHashBatch, mailbox_hash: MailboxHash, ) -> ResultFuture<()>; fn collection(&self) -> crate::Collection; fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn create_mailbox( &mut self, path: String, ) -> ResultFuture<(MailboxHash, HashMap)>; fn delete_mailbox( &mut self, mailbox_hash: MailboxHash, ) -> ResultFuture>; fn set_mailbox_subscription( &mut self, mailbox_hash: MailboxHash, val: bool, ) -> ResultFuture<()>; fn rename_mailbox( &mut self, mailbox_hash: MailboxHash, new_path: String, ) -> ResultFuture; fn set_mailbox_permissions( &mut self, mailbox_hash: MailboxHash, val: MailboxPermissions, ) -> ResultFuture<()>; fn search( &self, query: crate::search::Query, mailbox_hash: Option, ) -> ResultFuture>; fn submit( &self, _bytes: Vec, _mailbox_hash: Option, _flags: Option, ) -> ResultFuture<()> { Err(Error::new("Submission not supported in this backend.") .set_kind(ErrorKind::NotSupported)) } } /// A `BackendOp` manages common operations for the various mail backends. They /// only live for the duration of the operation. They are generated by the /// `operation` method of `Mailbackend` trait. /// /// # Motivation /// /// We need a way to do various operations on individual mails regardless of /// what backend they come from (eg local or imap). /// /// # Creation /// ```ignore /// /* Create operation from Backend */ /// /// let op = backend.operation(message.hash(), mailbox.hash()); /// ``` /// /// # Example /// ```ignore /// use melib::backends::{BackendOp}; /// use melib::Result; /// use melib::{Envelope, Flag}; /// /// #[derive(Debug)] /// struct FooOp {} /// /// impl BackendOp for FooOp { /// fn as_bytes(&mut self) -> Result<&[u8]> { /// unimplemented!() /// } /// } /// /// let operation = Box::new(FooOp {}); /// ``` pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send { fn as_bytes(&mut self) -> ResultFuture>; } /// Wrapper for [`BackendOp`] that are to be set read-only. /// /// Warning: Backend implementations may still cause side-effects (for example /// IMAP can set the Seen flag when fetching an envelope) #[derive(Debug)] pub struct ReadOnlyOp { op: Box, } impl ReadOnlyOp { #[allow(clippy::new_ret_no_self)] pub fn new(op: Box) -> Box { Box::new(Self { op }) } } impl BackendOp for ReadOnlyOp { fn as_bytes(&mut self) -> ResultFuture> { self.op.as_bytes() } } #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum SpecialUsageMailbox { #[default] Normal, Inbox, Archive, Drafts, Flagged, Junk, Sent, Trash, } impl std::fmt::Display for SpecialUsageMailbox { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { use SpecialUsageMailbox::*; write!( f, "{}", match self { Normal => "Normal", Inbox => "Inbox", Archive => "Archive", Drafts => "Drafts", Flagged => "Flagged", Junk => "Junk", Sent => "Sent", Trash => "Trash", } ) } } impl SpecialUsageMailbox { pub fn detect_usage(name: &str) -> Option { if name.eq_ignore_ascii_case("inbox") { Some(Self::Inbox) } else if name.eq_ignore_ascii_case("archive") { Some(Self::Archive) } else if name.eq_ignore_ascii_case("drafts") { Some(Self::Drafts) } else if name.eq_ignore_ascii_case("junk") || name.eq_ignore_ascii_case("spam") { Some(Self::Junk) } else if name.eq_ignore_ascii_case("sent") { Some(Self::Sent) } else if name.eq_ignore_ascii_case("trash") { Some(Self::Trash) } else { Some(Self::Normal) } } } pub trait BackendMailbox: std::fmt::Debug { fn hash(&self) -> MailboxHash; /// Final component of `path`. fn name(&self) -> &str; /// Path of mailbox within the mailbox hierarchy, with `/` as separator. fn path(&self) -> &str; fn clone(&self) -> Mailbox; fn children(&self) -> &[MailboxHash]; fn parent(&self) -> Option; fn is_subscribed(&self) -> bool; fn set_is_subscribed(&mut self, new_val: bool) -> Result<()>; fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()>; fn special_usage(&self) -> SpecialUsageMailbox; fn permissions(&self) -> MailboxPermissions; fn count(&self) -> Result<(usize, usize)>; } crate::declare_u64_hash!(AccountHash); crate::declare_u64_hash!(MailboxHash); crate::declare_u64_hash!(TagHash); pub type Mailbox = Box; impl Clone for Mailbox { fn clone(&self) -> Self { BackendMailbox::clone(self.deref()) } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct MailboxPermissions { pub create_messages: bool, pub remove_messages: bool, pub set_flags: bool, pub create_child: bool, pub rename_messages: bool, pub delete_messages: bool, pub delete_mailbox: bool, pub change_permissions: bool, } impl Default for MailboxPermissions { fn default() -> Self { Self { create_messages: false, remove_messages: false, set_flags: false, create_child: false, rename_messages: false, delete_messages: false, delete_mailbox: true, change_permissions: false, } } } impl std::fmt::Display for MailboxPermissions { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { write!(fmt, "{:#?}", self) } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct EnvelopeHashBatch { pub first: EnvelopeHash, pub rest: SmallVec<[EnvelopeHash; 64]>, } impl From for EnvelopeHashBatch { fn from(value: EnvelopeHash) -> Self { Self { first: value, rest: SmallVec::new(), } } } impl std::convert::TryFrom<&[EnvelopeHash]> for EnvelopeHashBatch { type Error = (); fn try_from(value: &[EnvelopeHash]) -> std::result::Result { if value.is_empty() { return Err(()); } Ok(Self { first: value[0], rest: value[1..].iter().cloned().collect(), }) } } impl From<&EnvelopeHashBatch> for BTreeSet { fn from(val: &EnvelopeHashBatch) -> Self { val.iter().collect() } } impl EnvelopeHashBatch { pub fn iter(&self) -> impl std::iter::Iterator + '_ { std::iter::once(self.first).chain(self.rest.iter().cloned()) } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn len(&self) -> usize { 1 + self.rest.len() } pub fn to_set(&self) -> BTreeSet { self.into() } } #[derive(Clone, Default)] pub struct LazyCountSet { pub not_yet_seen: usize, pub set: BTreeSet, } impl std::fmt::Debug for LazyCountSet { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("LazyCountSet") .field("not_yet_seen", &self.not_yet_seen) .field("set", &self.set.len()) .field("total_len", &self.len()) .finish() } } impl LazyCountSet { pub fn new() -> Self { Self::default() } pub fn set_not_yet_seen(&mut self, new_val: usize) { self.not_yet_seen = new_val; } pub fn insert_existing(&mut self, new_val: EnvelopeHash) -> bool { if self.not_yet_seen == 0 { false } else { if !self.set.contains(&new_val) { self.not_yet_seen -= 1; } self.set.insert(new_val); true } } pub fn insert_existing_set(&mut self, set: BTreeSet) { let old_len = self.set.len(); self.set.extend(set); self.not_yet_seen = self.not_yet_seen.saturating_sub(self.set.len() - old_len); } pub fn is_empty(&self) -> bool { self.len() == 0 } #[inline(always)] pub fn len(&self) -> usize { self.set.len() + self.not_yet_seen } #[inline(always)] pub fn clear(&mut self) { self.set.clear(); self.not_yet_seen = 0; } pub fn insert_new(&mut self, new_val: EnvelopeHash) { self.set.insert(new_val); } pub fn insert_set(&mut self, set: BTreeSet) { self.set.extend(set); } pub fn remove(&mut self, env_hash: EnvelopeHash) -> bool { self.set.remove(&env_hash) } #[inline(always)] pub fn contains(&self, value: &EnvelopeHash) -> bool { self.set.contains(value) } } pub struct IsSubscribedFn(pub Box bool + Send + Sync>); impl std::fmt::Debug for IsSubscribedFn { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "IsSubscribedFn Box") } } impl std::ops::Deref for IsSubscribedFn { type Target = Box bool + Send + Sync>; fn deref(&self) -> &Box bool + Send + Sync> { &self.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_lazy_count_set() { let mut new = LazyCountSet::default(); assert_eq!(new.len(), 0); new.set_not_yet_seen(10); assert_eq!(new.len(), 10); for i in 0..10 { assert!(new.insert_existing(EnvelopeHash(i))); } assert_eq!(new.len(), 10); assert!(!new.insert_existing(EnvelopeHash(10))); assert_eq!(new.len(), 10); } }