From 85f3ce89769af0342ca68f075df1ea02b11f0f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 14 Oct 2021 12:52:30 +0200 Subject: [PATCH] add mailbox handler tests (#225) * make imap.fetch_mboxes return a Mboxes struct * add mbox handler tests --- src/domain/imap/imap_handler.rs | 4 +- src/domain/imap/imap_service.rs | 23 ++++-- src/domain/mbox/attr_entity.rs | 13 +++- src/domain/mbox/attrs_entity.rs | 16 ++-- src/domain/mbox/mbox_entity.rs | 15 ++-- src/domain/mbox/mbox_handler.rs | 124 +++++++++++++++++++++++++++++-- src/domain/mbox/mboxes_entity.rs | 16 ++-- src/domain/msg/flag_handler.rs | 6 +- src/domain/msg/msg_entity.rs | 3 +- src/domain/msg/msg_handler.rs | 35 +++++---- src/domain/msg/tpl_handler.rs | 4 +- 11 files changed, 199 insertions(+), 60 deletions(-) diff --git a/src/domain/imap/imap_handler.rs b/src/domain/imap/imap_handler.rs index a543600..2a43684 100644 --- a/src/domain/imap/imap_handler.rs +++ b/src/domain/imap/imap_handler.rs @@ -7,7 +7,7 @@ use anyhow::Result; use crate::{config::Config, domain::imap::ImapServiceInterface}; /// Notify handler. -pub fn notify( +pub fn notify<'a, ImapService: ImapServiceInterface<'a>>( keepalive: u64, config: &Config, imap: &mut ImapService, @@ -16,7 +16,7 @@ pub fn notify( } /// Watch handler. -pub fn watch( +pub fn watch<'a, ImapService: ImapServiceInterface<'a>>( keepalive: u64, imap: &mut ImapService, ) -> Result<()> { diff --git a/src/domain/imap/imap_service.rs b/src/domain/imap/imap_service.rs index 6633788..5d93721 100644 --- a/src/domain/imap/imap_service.rs +++ b/src/domain/imap/imap_service.rs @@ -14,16 +14,15 @@ use std::{ use crate::{ config::{Account, Config}, - domain::{Envelopes, Flags, Mbox, Msg}, + domain::{Envelopes, Flags, Mbox, Mboxes, Msg, RawMboxes}, }; type ImapSession = imap::Session>; -pub(crate) type RawMboxes = imap::types::ZeroCopy>; -pub trait ImapServiceInterface { +pub trait ImapServiceInterface<'a> { fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>; fn watch(&mut self, keepalive: u64) -> Result<()>; - fn fetch_raw_mboxes(&mut self) -> Result; + fn fetch_mboxes(&'a mut self) -> Result; fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result; fn find_msgs(&mut self, query: &str, page_size: &usize, page: &usize) -> Result; fn find_msg(&mut self, seq: &str) -> Result; @@ -45,6 +44,10 @@ pub struct ImapService<'a> { account: &'a Account, mbox: &'a Mbox<'a>, sess: Option, + /// Holds raw mailboxes fetched by the `imap` crate in order to extend mailboxes lifetime + /// outside of handlers. Without that, it would be impossible for handlers to return a `Mbox` + /// struct or a `Mboxes` struct due to the `ZeroCopy` constraint. + _raw_mboxes_cache: Option, } impl<'a> ImapService<'a> { @@ -102,11 +105,14 @@ impl<'a> ImapService<'a> { } } -impl<'a> ImapServiceInterface for ImapService<'a> { - fn fetch_raw_mboxes(&mut self) -> Result { - self.sess()? +impl<'a> ImapServiceInterface<'a> for ImapService<'a> { + fn fetch_mboxes(&'a mut self) -> Result { + let raw_mboxes = self + .sess()? .list(Some(""), Some("*")) - .context("cannot list mailboxes") + .context("cannot list mailboxes")?; + self._raw_mboxes_cache = Some(raw_mboxes); + Ok(Mboxes::from(self._raw_mboxes_cache.as_ref().unwrap())) } fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result { @@ -381,6 +387,7 @@ impl<'a> From<(&'a Account, &'a Mbox<'a>)> for ImapService<'a> { account, mbox, sess: None, + _raw_mboxes_cache: None, } } } diff --git a/src/domain/mbox/attr_entity.rs b/src/domain/mbox/attr_entity.rs index 1da88d0..fa1fb43 100644 --- a/src/domain/mbox/attr_entity.rs +++ b/src/domain/mbox/attr_entity.rs @@ -24,7 +24,7 @@ pub enum AttrWrap<'a> { /// Represents the mailbox attribute. /// See https://serde.rs/remote-derive.html. #[derive(Debug, PartialEq, Eq, Hash, Serialize)] -pub struct Attr<'a>(#[serde(with = "AttrWrap")] pub &'a AttrRemote<'a>); +pub struct Attr<'a>(#[serde(with = "AttrWrap")] pub AttrRemote<'a>); /// Makes the attribute displayable. impl<'a> Display for Attr<'a> { @@ -39,6 +39,13 @@ impl<'a> Display for Attr<'a> { } } +/// Converts an `imap::types::NameAttribute` into an attribute. +impl<'a> From> for Attr<'a> { + fn from(attr: AttrRemote<'a>) -> Self { + Self(attr) + } +} + #[cfg(test)] mod tests { use super::*; @@ -47,10 +54,10 @@ mod tests { fn it_should_display_attr() { macro_rules! attr_from { ($attr:ident) => { - Attr(&AttrRemote::$attr).to_string() + Attr(AttrRemote::$attr).to_string() }; ($custom:literal) => { - Attr(&AttrRemote::Custom($custom.into())).to_string() + Attr(AttrRemote::Custom($custom.into())).to_string() }; } diff --git a/src/domain/mbox/attrs_entity.rs b/src/domain/mbox/attrs_entity.rs index 1e86fbb..2d9469e 100644 --- a/src/domain/mbox/attrs_entity.rs +++ b/src/domain/mbox/attrs_entity.rs @@ -4,7 +4,6 @@ use serde::Serialize; use std::{ - collections::HashSet, fmt::{self, Display}, ops::Deref, }; @@ -12,20 +11,19 @@ use std::{ use crate::domain::{Attr, AttrRemote}; /// Represents the attributes of the mailbox. -/// A HashSet is used in order to avoid duplicates. #[derive(Debug, Default, PartialEq, Eq, Serialize)] -pub struct Attrs<'a>(HashSet>); +pub struct Attrs<'a>(Vec>); -/// Converts a slice of `imap::types::NameAttribute` into attributes. -impl<'a> From<&'a [AttrRemote<'a>]> for Attrs<'a> { - fn from(attrs: &'a [AttrRemote<'a>]) -> Self { - Self(attrs.iter().map(Attr).collect()) +/// Converts a vector of `imap::types::NameAttribute` into attributes. +impl<'a> From>> for Attrs<'a> { + fn from(attrs: Vec>) -> Self { + Self(attrs.into_iter().map(Attr::from).collect()) } } /// Derefs the attributes to its inner hashset. impl<'a> Deref for Attrs<'a> { - type Target = HashSet>; + type Target = Vec>; fn deref(&self) -> &Self::Target { &self.0 @@ -52,7 +50,7 @@ mod tests { fn it_should_display_attrs() { macro_rules! attrs_from { ($($attr:expr),*) => { - Attrs::from(&[$($attr,)*] as &[AttrRemote]).to_string() + Attrs::from(vec![$($attr,)*]).to_string() }; } diff --git a/src/domain/mbox/mbox_entity.rs b/src/domain/mbox/mbox_entity.rs index 0ff176b..d951fe3 100644 --- a/src/domain/mbox/mbox_entity.rs +++ b/src/domain/mbox/mbox_entity.rs @@ -13,7 +13,10 @@ use crate::{ ui::{Cell, Row, Table}, }; -/// Represents the mailbox. +/// Represents a raw mailbox returned by the `imap` crate. +pub(crate) type RawMbox = imap::types::Name; + +/// Represents a mailbox. #[derive(Debug, Default, PartialEq, Eq, Serialize)] pub struct Mbox<'a> { /// Represents the mailbox hierarchie delimiter. @@ -68,11 +71,11 @@ impl<'a> Table for Mbox<'a> { /// Converts an `imap::types::Name` into a mailbox. impl<'a> From<&'a imap::types::Name> for Mbox<'a> { - fn from(name: &'a imap::types::Name) -> Self { + fn from(raw_mbox: &'a imap::types::Name) -> Self { Self { - delim: name.delimiter().unwrap_or_default().into(), - name: name.name().into(), - attrs: Attrs::from(name.attributes()), + delim: raw_mbox.delimiter().unwrap_or_default().into(), + name: raw_mbox.name().into(), + attrs: Attrs::from(raw_mbox.attributes().to_vec()), } } } @@ -106,7 +109,7 @@ mod tests { let full_mbox = Mbox { delim: ".".into(), name: "Sent".into(), - attrs: Attrs::from(&[AttrRemote::NoSelect] as &[AttrRemote]), + attrs: Attrs::from(vec![AttrRemote::NoSelect]), }; assert_eq!("Sent", full_mbox.to_string()); } diff --git a/src/domain/mbox/mbox_handler.rs b/src/domain/mbox/mbox_handler.rs index 45fe452..a299600 100644 --- a/src/domain/mbox/mbox_handler.rs +++ b/src/domain/mbox/mbox_handler.rs @@ -5,18 +5,126 @@ use anyhow::Result; use log::trace; -use crate::{ - domain::{ImapServiceInterface, Mboxes}, - output::OutputServiceInterface, -}; +use crate::{domain::ImapServiceInterface, output::OutputServiceInterface}; /// List all mailboxes. -pub fn list( +pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( output: &OutputService, - imap: &mut ImapService, + imap: &'a mut ImapService, ) -> Result<()> { - let raw_mboxes = imap.fetch_raw_mboxes()?; - let mboxes = Mboxes::from(&raw_mboxes); + let mboxes = imap.fetch_mboxes()?; trace!("mailboxes: {:#?}", mboxes); output.print(mboxes) } + +#[cfg(test)] +mod tests { + use serde::Serialize; + use std::fmt::Display; + + use super::*; + use crate::{ + config::Config, + domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg}, + output::OutputJson, + }; + + #[test] + fn it_should_list_mboxes() { + struct OutputServiceTest; + + impl OutputServiceInterface for OutputServiceTest { + fn print(&self, data: T) -> Result<()> { + let data = serde_json::to_string(&OutputJson::new(data))?; + assert_eq!( + data, + r#"{"response":[{"delim":"/","name":"INBOX","attrs":["NoSelect"]},{"delim":"/","name":"Sent","attrs":["NoInferiors",{"Custom":"HasNoChildren"}]}]}"# + ); + Ok(()) + } + + fn is_json(&self) -> bool { + unimplemented!() + } + } + + struct ImapServiceTest; + + impl<'a> ImapServiceInterface<'a> for ImapServiceTest { + fn fetch_mboxes(&'a mut self) -> Result { + Ok(Mboxes(vec![ + Mbox { + delim: "/".into(), + name: "INBOX".into(), + attrs: Attrs::from(vec![AttrRemote::NoSelect]), + }, + Mbox { + delim: "/".into(), + name: "Sent".into(), + attrs: Attrs::from(vec![ + AttrRemote::NoInferiors, + AttrRemote::Custom("HasNoChildren".into()), + ]), + }, + ])) + } + + fn notify(&mut self, _: &Config, _: u64) -> Result<()> { + unimplemented!() + } + + fn watch(&mut self, _: u64) -> Result<()> { + unimplemented!() + } + + fn get_msgs(&mut self, _: &usize, _: &usize) -> Result { + unimplemented!() + } + + fn find_msgs(&mut self, _: &str, _: &usize, _: &usize) -> Result { + unimplemented!() + } + + fn find_msg(&mut self, _: &str) -> Result { + unimplemented!() + } + + fn find_raw_msg(&mut self, _: &str) -> Result> { + unimplemented!() + } + + fn append_msg(&mut self, _: &Mbox, _: Msg) -> Result<()> { + unimplemented!() + } + + fn append_raw_msg_with_flags(&mut self, _: &Mbox, _: &[u8], _: Flags) -> Result<()> { + unimplemented!() + } + + fn expunge(&mut self) -> Result<()> { + unimplemented!() + } + + fn logout(&mut self) -> Result<()> { + unimplemented!() + } + + fn add_flags(&mut self, _: &str, _: &Flags) -> Result<()> { + unimplemented!() + } + + fn set_flags(&mut self, _: &str, _: &Flags) -> Result<()> { + unimplemented!() + } + + fn remove_flags(&mut self, _: &str, _: &Flags) -> Result<()> { + unimplemented!() + } + } + + let output = OutputServiceTest {}; + let mut imap = ImapServiceTest {}; + + assert!(list(&output, &mut imap).is_ok()); + } +} diff --git a/src/domain/mbox/mboxes_entity.rs b/src/domain/mbox/mboxes_entity.rs index 5e5afdd..8139f80 100644 --- a/src/domain/mbox/mboxes_entity.rs +++ b/src/domain/mbox/mboxes_entity.rs @@ -8,10 +8,16 @@ use std::{ ops::Deref, }; -use crate::{domain::Mbox, ui::Table}; +use crate::{ + domain::{Mbox, RawMbox}, + ui::Table, +}; + +/// Represents a list of raw mailboxes returned by the `imap` crate. +pub(crate) type RawMboxes = imap::types::ZeroCopy>; /// Represents a list of mailboxes. -#[derive(Debug, Serialize)] +#[derive(Debug, Default, Serialize)] pub struct Mboxes<'a>(pub Vec>); /// Derefs the mailboxes to its inner vector. @@ -31,8 +37,8 @@ impl<'a> Display for Mboxes<'a> { } /// Converts a list of `imap::types::Name` into mailboxes. -impl<'a> From<&'a imap::types::ZeroCopy>> for Mboxes<'a> { - fn from(names: &'a imap::types::ZeroCopy>) -> Self { - Self(names.iter().map(Mbox::from).collect()) +impl<'a> From<&'a RawMboxes> for Mboxes<'a> { + fn from(raw_mboxes: &'a RawMboxes) -> Mboxes<'a> { + Self(raw_mboxes.iter().map(Mbox::from).collect()) } } diff --git a/src/domain/msg/flag_handler.rs b/src/domain/msg/flag_handler.rs index d62792c..8591fa8 100644 --- a/src/domain/msg/flag_handler.rs +++ b/src/domain/msg/flag_handler.rs @@ -11,7 +11,7 @@ use crate::{ /// Add flags to all messages within the given sequence range. /// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>( +pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq_range: &'a str, flags: Vec<&'a str>, output: &'a OutputService, @@ -27,7 +27,7 @@ pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceIn /// Remove flags from all messages within the given sequence range. /// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>( +pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq_range: &'a str, flags: Vec<&'a str>, output: &'a OutputService, @@ -43,7 +43,7 @@ pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServic /// Replace flags of all messages within the given sequence range. /// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>( +pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq_range: &'a str, flags: Vec<&'a str>, output: &'a OutputService, diff --git a/src/domain/msg/msg_entity.rs b/src/domain/msg/msg_entity.rs index c8cbdfe..edab4d6 100644 --- a/src/domain/msg/msg_entity.rs +++ b/src/domain/msg/msg_entity.rs @@ -296,8 +296,9 @@ impl Msg { } pub fn edit_with_editor< + 'a, OutputService: OutputServiceInterface, - ImapService: ImapServiceInterface, + ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( mut self, diff --git a/src/domain/msg/msg_handler.rs b/src/domain/msg/msg_handler.rs index b5e6f6a..a09ed46 100644 --- a/src/domain/msg/msg_handler.rs +++ b/src/domain/msg/msg_handler.rs @@ -26,7 +26,11 @@ use crate::{ }; /// Download all message attachments to the user account downloads directory. -pub fn attachments( +pub fn attachments< + 'a, + OutputService: OutputServiceInterface, + ImapService: ImapServiceInterface<'a>, +>( seq: &str, account: &Account, output: &OutputService, @@ -53,7 +57,7 @@ pub fn attachments( +pub fn copy<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq: &str, mbox: &str, output: &OutputService, @@ -70,7 +74,7 @@ pub fn copy( +pub fn delete<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq: &str, output: &OutputService, imap: &mut ImapService, @@ -83,8 +87,9 @@ pub fn delete, SmtpService: SmtpServiceInterface, >( seq: &str, @@ -101,7 +106,7 @@ pub fn forward< } /// List paginated messages from the selected mailbox. -pub fn list( +pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( page_size: Option, page: usize, account: &Account, @@ -120,8 +125,9 @@ pub fn list, SmtpService: SmtpServiceInterface, >( url: &Url, @@ -172,7 +178,7 @@ pub fn mailto< } /// Move a message from a mailbox to another. -pub fn move_( +pub fn move_<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( // The sequence number of the message to move seq: &str, // The mailbox to move the message in @@ -198,7 +204,7 @@ pub fn move_( +pub fn read<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq: &str, text_mime: &str, raw: bool, @@ -216,8 +222,9 @@ pub fn read, SmtpService: SmtpServiceInterface, >( seq: &str, @@ -237,7 +244,7 @@ pub fn reply< } /// Save a raw message to the targetted mailbox. -pub fn save( +pub fn save<'a, ImapService: ImapServiceInterface<'a>>( mbox: &str, msg: &str, imap: &mut ImapService, @@ -248,7 +255,7 @@ pub fn save( } /// Paginate messages from the selected mailbox matching the specified query. -pub fn search( +pub fn search<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( query: String, page_size: Option, page: usize, @@ -266,8 +273,9 @@ pub fn search, SmtpService: SmtpServiceInterface, >( raw_msg: &str, @@ -301,8 +309,9 @@ pub fn send< /// Compose a new message. pub fn write< + 'a, OutputService: OutputServiceInterface, - ImapService: ImapServiceInterface, + ImapService: ImapServiceInterface<'a>, SmtpService: SmtpServiceInterface, >( attachments_paths: Vec<&str>, diff --git a/src/domain/msg/tpl_handler.rs b/src/domain/msg/tpl_handler.rs index b7c557e..614ff5d 100644 --- a/src/domain/msg/tpl_handler.rs +++ b/src/domain/msg/tpl_handler.rs @@ -25,7 +25,7 @@ pub fn new<'a, OutputService: OutputServiceInterface>( } /// Generate a reply message template. -pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>( +pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq: &str, all: bool, opts: TplOverride<'a>, @@ -39,7 +39,7 @@ pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapService } /// Generate a forward message template. -pub fn forward<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>( +pub fn forward<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq: &str, opts: TplOverride<'a>, account: &'a Account,