add mailbox handler tests (#225)

* make imap.fetch_mboxes return a Mboxes struct

* add mbox handler tests
This commit is contained in:
Clément DOUIN 2021-10-14 12:52:30 +02:00 committed by GitHub
parent 8af14734c3
commit 85f3ce8976
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 199 additions and 60 deletions

View file

@ -7,7 +7,7 @@ use anyhow::Result;
use crate::{config::Config, domain::imap::ImapServiceInterface};
/// Notify handler.
pub fn notify<ImapService: ImapServiceInterface>(
pub fn notify<'a, ImapService: ImapServiceInterface<'a>>(
keepalive: u64,
config: &Config,
imap: &mut ImapService,
@ -16,7 +16,7 @@ pub fn notify<ImapService: ImapServiceInterface>(
}
/// Watch handler.
pub fn watch<ImapService: ImapServiceInterface>(
pub fn watch<'a, ImapService: ImapServiceInterface<'a>>(
keepalive: u64,
imap: &mut ImapService,
) -> Result<()> {

View file

@ -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<TlsStream<TcpStream>>;
pub(crate) type RawMboxes = imap::types::ZeroCopy<Vec<imap::types::Name>>;
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<RawMboxes>;
fn fetch_mboxes(&'a mut self) -> Result<Mboxes>;
fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes>;
fn find_msgs(&mut self, query: &str, page_size: &usize, page: &usize) -> Result<Envelopes>;
fn find_msg(&mut self, seq: &str) -> Result<Msg>;
@ -45,6 +44,10 @@ pub struct ImapService<'a> {
account: &'a Account,
mbox: &'a Mbox<'a>,
sess: Option<ImapSession>,
/// 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<RawMboxes>,
}
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<RawMboxes> {
self.sess()?
impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
fn fetch_mboxes(&'a mut self) -> Result<Mboxes> {
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<Envelopes> {
@ -381,6 +387,7 @@ impl<'a> From<(&'a Account, &'a Mbox<'a>)> for ImapService<'a> {
account,
mbox,
sess: None,
_raw_mboxes_cache: None,
}
}
}

View file

@ -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<AttrRemote<'a>> 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()
};
}

View file

@ -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<Attr<'a>>);
pub struct Attrs<'a>(Vec<Attr<'a>>);
/// 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<Vec<AttrRemote<'a>>> for Attrs<'a> {
fn from(attrs: Vec<AttrRemote<'a>>) -> 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<Attr<'a>>;
type Target = Vec<Attr<'a>>;
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()
};
}

View file

@ -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());
}

View file

@ -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<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
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<T: Serialize + Display>(&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<Mboxes> {
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<Envelopes> {
unimplemented!()
}
fn find_msgs(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
unimplemented!()
}
fn find_msg(&mut self, _: &str) -> Result<Msg> {
unimplemented!()
}
fn find_raw_msg(&mut self, _: &str) -> Result<Vec<u8>> {
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());
}
}

View file

@ -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<Vec<RawMbox>>;
/// Represents a list of mailboxes.
#[derive(Debug, Serialize)]
#[derive(Debug, Default, Serialize)]
pub struct Mboxes<'a>(pub Vec<Mbox<'a>>);
/// 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<Vec<imap::types::Name>>> for Mboxes<'a> {
fn from(names: &'a imap::types::ZeroCopy<Vec<imap::types::Name>>) -> 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())
}
}

View file

@ -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,

View file

@ -296,8 +296,9 @@ impl Msg {
}
pub fn edit_with_editor<
'a,
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
mut self,

View file

@ -26,7 +26,11 @@ use crate::{
};
/// Download all message attachments to the user account downloads directory.
pub fn attachments<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
pub fn attachments<
'a,
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface<'a>,
>(
seq: &str,
account: &Account,
output: &OutputService,
@ -53,7 +57,7 @@ pub fn attachments<OutputService: OutputServiceInterface, ImapService: ImapServi
}
/// Copy a message from a mailbox to another.
pub fn copy<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
pub fn copy<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
seq: &str,
mbox: &str,
output: &OutputService,
@ -70,7 +74,7 @@ pub fn copy<OutputService: OutputServiceInterface, ImapService: ImapServiceInter
}
/// Delete messages matching the given sequence range.
pub fn delete<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
pub fn delete<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
seq: &str,
output: &OutputService,
imap: &mut ImapService,
@ -83,8 +87,9 @@ pub fn delete<OutputService: OutputServiceInterface, ImapService: ImapServiceInt
/// Forward the given message UID from the selected mailbox.
pub fn forward<
'a,
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
seq: &str,
@ -101,7 +106,7 @@ pub fn forward<
}
/// List paginated messages from the selected mailbox.
pub fn list<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
page_size: Option<usize>,
page: usize,
account: &Account,
@ -120,8 +125,9 @@ pub fn list<OutputService: OutputServiceInterface, ImapService: ImapServiceInter
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<
'a,
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
url: &Url,
@ -172,7 +178,7 @@ pub fn mailto<
}
/// Move a message from a mailbox to another.
pub fn move_<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
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_<OutputService: OutputServiceInterface, ImapService: ImapServiceInte
}
/// Read a message by its sequence number.
pub fn read<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
pub fn read<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
seq: &str,
text_mime: &str,
raw: bool,
@ -216,8 +222,9 @@ pub fn read<OutputService: OutputServiceInterface, ImapService: ImapServiceInter
/// Reply to the given message UID.
pub fn reply<
'a,
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
seq: &str,
@ -237,7 +244,7 @@ pub fn reply<
}
/// Save a raw message to the targetted mailbox.
pub fn save<ImapService: ImapServiceInterface>(
pub fn save<'a, ImapService: ImapServiceInterface<'a>>(
mbox: &str,
msg: &str,
imap: &mut ImapService,
@ -248,7 +255,7 @@ pub fn save<ImapService: ImapServiceInterface>(
}
/// Paginate messages from the selected mailbox matching the specified query.
pub fn search<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
pub fn search<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
query: String,
page_size: Option<usize>,
page: usize,
@ -266,8 +273,9 @@ pub fn search<OutputService: OutputServiceInterface, ImapService: ImapServiceInt
/// Send a raw message.
pub fn send<
'a,
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
ImapService: ImapServiceInterface<'a>,
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>,

View file

@ -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,