Aerogramme refactoring #57

Merged
quentin merged 16 commits from feat/more-imap-qol into main 2024-01-06 10:38:37 +00:00
6 changed files with 143 additions and 259 deletions
Showing only changes of commit a84ba4d42f - Show all commits

80
src/imap/index.rs Normal file
View file

@ -0,0 +1,80 @@
use std::num::NonZeroU32;
use anyhow::{anyhow, bail, Result};
use imap_codec::imap_types::sequence::{self, SequenceSet};
use crate::mail::uidindex::{ImapUid, UidIndex};
use crate::mail::unique_ident::UniqueIdent;
pub struct Index<'a>(pub &'a UidIndex);
impl<'a> Index<'a> {
pub fn fetch(self: &Index<'a>, sequence_set: &SequenceSet, by_uid: bool) -> Result<Vec<MailIndex<'a>>> {
let mail_vec = self
.0
.idx_by_uid
.iter()
.map(|(uid, uuid)| (*uid, *uuid))
.collect::<Vec<_>>();
let mut mails = vec![];
if by_uid {
if mail_vec.is_empty() {
return Ok(vec![]);
}
let iter_strat = sequence::Strategy::Naive {
largest: mail_vec.last().unwrap().0,
};
let mut i = 0;
for uid in sequence_set.iter(iter_strat) {
while mail_vec.get(i).map(|mail| mail.0 < uid).unwrap_or(false) {
i += 1;
}
if let Some(mail) = mail_vec.get(i) {
if mail.0 == uid {
mails.push(MailIndex {
i: NonZeroU32::try_from(i as u32 + 1).unwrap(),
uid: mail.0,
uuid: mail.1,
flags: self.0.table.get(&mail.1).ok_or(anyhow!("mail is missing from index"))?.1.as_ref(),
});
}
} else {
break;
}
}
} else {
if mail_vec.is_empty() {
bail!("No such message (mailbox is empty)");
}
let iter_strat = sequence::Strategy::Naive {
largest: NonZeroU32::try_from((mail_vec.len()) as u32).unwrap(),
};
for i in sequence_set.iter(iter_strat) {
if let Some(mail) = mail_vec.get(i.get() as usize - 1) {
mails.push(MailIndex {
i,
uid: mail.0,
uuid: mail.1,
flags: self.0.table.get(&mail.1).ok_or(anyhow!("mail is missing from index"))?.1.as_ref(),
});
} else {
bail!("No such mail: {}", i);
}
}
}
Ok(mails)
}
}
pub struct MailIndex<'a> {
pub i: NonZeroU32,
pub uid: ImapUid,
pub uuid: UniqueIdent,
pub flags: &'a Vec<String>
}

View file

@ -1,9 +1,9 @@
use std::num::NonZeroU32;
use anyhow::{anyhow, bail, Result, Context};
use anyhow::{anyhow, bail, Result};
use chrono::{Offset, TimeZone, Utc};
use imap_codec::imap_types::core::{IString, NString};
use imap_codec::imap_types::core::NString;
use imap_codec::imap_types::datetime::DateTime;
use imap_codec::imap_types::fetch::{
MessageDataItem, MessageDataItemName, Section as FetchSection,
@ -16,87 +16,73 @@ use eml_codec::{
part::{composite::Message, AnyPart},
};
use crate::mail::query::QueryResult;
use crate::imap::attributes::AttributesProxy;
use crate::imap::flags;
use crate::imap::imf_view::message_envelope;
use crate::imap::mailbox_view::MailIdentifiers;
use crate::imap::mime_view;
use crate::imap::response::Body;
use crate::mail::query::QueryResult;
use crate::imap::index::MailIndex;
pub struct MailView<'a> {
pub in_idx: MailIndex<'a>,
pub query_result: &'a QueryResult<'a>,
pub content: FetchedMail<'a>,
}
impl<'a> MailView<'a> {
pub fn new(query_result: &'a QueryResult<'a>) -> Result<Self> {
pub fn new(query_result: &'a QueryResult<'a>, in_idx: MailIndex<'a>) -> Result<MailView<'a>> {
Ok(Self {
in_idx,
query_result,
content: match query_result {
QueryResult::FullResult { content, .. } => {
let (_, parsed) = eml_codec::parse_message(content).context("Invalid mail body")?;
let (_, parsed) = eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?;
FetchedMail::new_from_message(parsed)
},
QueryResult::PartialResult { metadata, .. } => {
let (_, parsed) = eml_codec::parse_imf(&metadata.headers).context("Invalid mail headers")?;
let (_, parsed) = eml_codec::parse_imf(&metadata.headers).or(Err(anyhow!("unable to parse email headers")))?;
FetchedMail::Partial(parsed)
}
QueryResult::IndexResult { .. } => FetchedMail::None,
QueryResult::IndexResult { .. } => FetchedMail::IndexOnly,
}
})
}
fn uid(&self) -> MessageDataItem<'static> {
MessageDataItem::Uid(self.ids.uid.clone())
MessageDataItem::Uid(self.in_idx.uid.clone())
}
fn flags(&self) -> MessageDataItem<'static> {
MessageDataItem::Flags(
self.flags
self.in_idx
.flags
.iter()
.filter_map(|f| flags::from_str(f))
.collect(),
)
}
fn rfc_822_size(&self) -> MessageDataItem<'static> {
MessageDataItem::Rfc822Size(self.meta.rfc822_size as u32)
fn rfc_822_size(&self) -> Result<MessageDataItem<'static>> {
let sz = self.query_result.metadata().ok_or(anyhow!("mail metadata are required"))?.rfc822_size;
Ok(MessageDataItem::Rfc822Size(sz as u32))
}
fn rfc_822_header(&self) -> MessageDataItem<'static> {
MessageDataItem::Rfc822Header(NString(
self.meta
.headers
.to_vec()
.try_into()
.ok()
.map(IString::Literal),
))
fn rfc_822_header(&self) -> Result<MessageDataItem<'static>> {
let hdrs: NString = self.query_result.metadata().ok_or(anyhow!("mail metadata are required"))?.headers.to_vec().try_into()?;
Ok(MessageDataItem::Rfc822Header(hdrs))
}
fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> {
Ok(MessageDataItem::Rfc822Text(NString(
self.content
.as_full()?
.raw_body
.to_vec()
.try_into()
.ok()
.map(IString::Literal),
)))
let txt: NString = self.content.as_full()?.raw_body.to_vec().try_into()?;
Ok(MessageDataItem::Rfc822Text(txt))
}
fn rfc822(&self) -> Result<MessageDataItem<'static>> {
Ok(MessageDataItem::Rfc822(NString(
self.content
.as_full()?
.raw_part
.to_vec()
.try_into()
.ok()
.map(IString::Literal),
)))
let full: NString = self.content.as_full()?.raw_part.to_vec().try_into()?;
Ok(MessageDataItem::Rfc822(full))
}
fn envelope(&self) -> MessageDataItem<'static> {
@ -119,16 +105,16 @@ impl<'a> MailView<'a> {
/// peek does not implicitly set the \Seen flag
/// eg. BODY[HEADER.FIELDS (DATE FROM)]
/// eg. BODY[]<0.2048>
fn body_ext<'b>(
fn body_ext(
&self,
section: &Option<FetchSection<'b>>,
section: &Option<FetchSection<'static>>,
partial: &Option<(u32, NonZeroU32)>,
peek: &bool,
) -> Result<(MessageDataItem<'b>, SeenFlag)> {
) -> Result<(MessageDataItem<'static>, SeenFlag)> {
// Manage Seen flag
let mut seen = SeenFlag::DoNothing;
let seen_flag = Flag::Seen.to_string();
if !peek && !self.flags.iter().any(|x| *x == seen_flag) {
if !peek && !self.in_idx.flags.iter().any(|x| *x == seen_flag) {
// Add \Seen flag
//self.mailbox.add_flags(uuid, &[seen_flag]).await?;
seen = SeenFlag::MustAdd;
@ -141,7 +127,7 @@ impl<'a> MailView<'a> {
mime_view::BodySection::Slice { body, origin_octet } => (body, Some(origin_octet)),
};
let data = NString(text.to_vec().try_into().ok().map(IString::Literal));
let data: NString = text.to_vec().try_into()?;
return Ok((
MessageDataItem::BodyExt {
@ -156,13 +142,13 @@ impl<'a> MailView<'a> {
fn internal_date(&self) -> Result<MessageDataItem<'static>> {
let dt = Utc
.fix()
.timestamp_opt(i64::try_from(self.meta.internaldate / 1000)?, 0)
.timestamp_opt(i64::try_from(self.query_result.metadata().ok_or(anyhow!("mail metadata were not fetched"))?.internaldate / 1000)?, 0)
.earliest()
.ok_or(anyhow!("Unable to parse internal date"))?;
Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt)))
}
pub fn filter<'b>(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> {
pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> {
let mut seen = SeenFlag::DoNothing;
let res_attrs = ap
.attrs
@ -170,8 +156,8 @@ impl<'a> MailView<'a> {
.map(|attr| match attr {
MessageDataItemName::Uid => Ok(self.uid()),
MessageDataItemName::Flags => Ok(self.flags()),
MessageDataItemName::Rfc822Size => Ok(self.rfc_822_size()),
MessageDataItemName::Rfc822Header => Ok(self.rfc_822_header()),
MessageDataItemName::Rfc822Size => self.rfc_822_size(),
MessageDataItemName::Rfc822Header => self.rfc_822_header(),
MessageDataItemName::Rfc822Text => self.rfc_822_text(),
MessageDataItemName::Rfc822 => self.rfc822(),
MessageDataItemName::Envelope => Ok(self.envelope()),
@ -192,7 +178,7 @@ impl<'a> MailView<'a> {
Ok((
Body::Data(Data::Fetch {
seq: self.ids.i,
seq: self.in_idx.i,
items: res_attrs.try_into()?,
}),
seen,
@ -208,19 +194,15 @@ pub enum SeenFlag {
// -------------------
pub enum FetchedMail<'a> {
None,
IndexOnly,
Partial(imf::Imf<'a>),
Full(AnyPart<'a>),
}
impl<'a> FetchedMail<'a> {
pub fn new_from_message(msg: Message<'a>) -> Self {
FetchedMail::Full(AnyPart::Msg(msg))
Self::Full(AnyPart::Msg(msg))
}
/*fn new_from_header(hdr: imf::Imf<'a>) -> Self {
FetchedMail::Partial(hdr)
}*/
fn as_anypart(&self) -> Result<&AnyPart<'a>> {
match self {
FetchedMail::Full(x) => Ok(&x),

View file

@ -1,7 +1,7 @@
use std::num::NonZeroU32;
use std::sync::Arc;
use anyhow::{anyhow, bail, Error, Result};
use anyhow::{anyhow, Error, Result};
use futures::stream::{FuturesOrdered, StreamExt};
@ -10,19 +10,19 @@ use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItem
use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType};
use imap_codec::imap_types::response::{Code, Data, Status};
use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::{self, SequenceSet};
use imap_codec::imap_types::sequence::SequenceSet;
use crate::mail::mailbox::Mailbox;
use crate::mail::snapshot::FrozenMailbox;
use crate::mail::query::QueryScope;
use crate::mail::uidindex::{ImapUid, ImapUidvalidity};
use crate::mail::unique_ident::UniqueIdent;
use crate::imap::attributes::AttributesProxy;
use crate::imap::flags;
use crate::imap::mail_view::{MailView, SeenFlag};
use crate::imap::response::Body;
use crate::imap::search;
//use crate::imap::search;
use crate::imap::index::Index;
const DEFAULT_FLAGS: [Flag; 5] = [
@ -147,7 +147,7 @@ impl MailboxView {
let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>();
let mails = self.get_mail_ids(sequence_set, *is_uid_store)?;
let mails = self.index().fetch(sequence_set, *is_uid_store)?;
for mi in mails.iter() {
match kind {
StoreType::Add => {
@ -190,7 +190,7 @@ impl MailboxView {
to: Arc<Mailbox>,
is_uid_copy: &bool,
) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>)> {
let mails = self.get_mail_ids(sequence_set, *is_uid_copy)?;
let mails = self.index().fetch(sequence_set, *is_uid_copy)?;
let mut new_uuids = vec![];
for mi in mails.iter() {
@ -217,7 +217,7 @@ impl MailboxView {
to: Arc<Mailbox>,
is_uid_copy: &bool,
) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec<Body<'static>>)> {
let mails = self.get_mail_ids(sequence_set, *is_uid_copy)?;
let mails = self.index().fetch(sequence_set, *is_uid_copy)?;
for mi in mails.iter() {
to.move_from(&self.0.mailbox, mi.uuid).await?;
@ -255,16 +255,17 @@ impl MailboxView {
true => QueryScope::Full,
_ => QueryScope::Partial,
};
let mids = MailIdentifiersList(self.get_mail_ids(sequence_set, *is_uid_fetch)?);
let uuids = mids.uuids();
let mail_idx_list = self.index().fetch(sequence_set, *is_uid_fetch)?;
// [2/6] Fetch the emails
let uuids = mail_idx_list.iter().map(|midx| midx.uuid).collect::<Vec<_>>();
let query = self.0.query(&uuids, query_scope);
let query_result = query.fetch().await?;
// [3/6] Derive an IMAP-specific view from the results, apply the filters
let views = query_result.iter()
.map(MailView::new)
.zip(mail_idx_list.into_iter())
.map(|(qr, midx)| MailView::new(qr, midx))
.collect::<Result<Vec<_>, _>>()?;
// [4/6] Apply the IMAP transformation to keep only relevant fields
@ -296,9 +297,10 @@ impl MailboxView {
pub async fn search<'a>(
&self,
_charset: &Option<Charset<'a>>,
search_key: &SearchKey<'a>,
uid: bool,
_search_key: &SearchKey<'a>,
_uid: bool,
) -> Result<Vec<Body<'static>>> {
/*
// 1. Compute the subset of sequence identifiers we need to fetch
let query = search::Criteria(search_key);
let (seq_set, seq_type) = query.to_sequence_set();
@ -313,79 +315,15 @@ impl MailboxView {
let _need_body = query.need_body();
Ok(vec![Body::Data(Data::Search(mail_u32))])
*/
unimplemented!()
}
// ----
// Gets the IMAP ID, the IMAP UIDs and, the Aerogramme UUIDs of mails identified by a SequenceSet of
// sequence numbers (~ IMAP selector)
fn get_mail_ids(
&self,
sequence_set: &SequenceSet,
by_uid: bool,
) -> Result<Vec<MailIdentifiers>> {
let mail_vec = self
.0
.snapshot
.idx_by_uid
.iter()
.map(|(uid, uuid)| (*uid, *uuid))
.collect::<Vec<_>>();
let mut mails = vec![];
if by_uid {
if mail_vec.is_empty() {
return Ok(vec![]);
}
let iter_strat = sequence::Strategy::Naive {
largest: mail_vec.last().unwrap().0,
};
let mut i = 0;
for uid in sequence_set.iter(iter_strat) {
while mail_vec.get(i).map(|mail| mail.0 < uid).unwrap_or(false) {
i += 1;
}
if let Some(mail) = mail_vec.get(i) {
if mail.0 == uid {
mails.push(MailIdentifiers {
i: NonZeroU32::try_from(i as u32 + 1).unwrap(),
uid: mail.0,
uuid: mail.1,
});
}
} else {
break;
}
}
} else {
if mail_vec.is_empty() {
bail!("No such message (mailbox is empty)");
}
let iter_strat = sequence::Strategy::Naive {
largest: NonZeroU32::try_from((mail_vec.len()) as u32).unwrap(),
};
for i in sequence_set.iter(iter_strat) {
if let Some(mail) = mail_vec.get(i.get() as usize - 1) {
mails.push(MailIdentifiers {
i,
uid: mail.0,
uuid: mail.1,
});
} else {
bail!("No such mail: {}", i);
}
}
}
Ok(mails)
fn index<'a>(&'a self) -> Index<'a> {
Index(&self.0.snapshot)
}
// ----
/// Produce an OK [UIDVALIDITY _] message corresponding to `known_state`
fn uidvalidity_status(&self) -> Result<Body<'static>> {
let uid_validity = Status::ok(
@ -501,25 +439,6 @@ impl MailboxView {
}
}
pub struct MailIdentifiers {
pub i: NonZeroU32,
pub uid: ImapUid,
pub uuid: UniqueIdent,
}
pub struct MailIdentifiersList(Vec<MailIdentifiers>);
impl MailIdentifiersList {
fn ids(&self) -> Vec<NonZeroU32> {
self.0.iter().map(|mi| mi.i).collect()
}
fn uids(&self) -> Vec<ImapUid> {
self.0.iter().map(|mi| mi.uid).collect()
}
fn uuids(&self) -> Vec<UniqueIdent> {
self.0.iter().map(|mi| mi.uuid).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -558,7 +477,7 @@ mod tests {
message_key: key,
rfc822_size: 8usize,
};
let ids = MailIdentifiers {
let ids = MailIndex {
i: NonZeroU32::MIN,
uid: NonZeroU32::MIN,
uuid: unique_ident::gen_ident(),

View file

@ -4,12 +4,12 @@ mod command;
mod flags;
mod flow;
mod imf_view;
mod index;
mod mail_view;
mod mailbox_view;
mod mime_view;
mod response;
mod search;
mod selectors;
mod session;
use std::net::SocketAddr;

View file

@ -1,100 +0,0 @@
use std::iter::zip;
use anyhow::{anyhow, Result};
use crate::cryptoblob::Key;
use crate::imap::mail_view::{FetchedMail, MailView};
use crate::imap::mailbox_view::MailIdentifiers;
use crate::mail::mailbox::MailMeta;
use crate::mail::unique_ident::UniqueIdent;
pub struct BodyIdentifier<'a> {
pub msg_uuid: &'a UniqueIdent,
pub msg_key: &'a Key,
}
#[derive(Default)]
pub struct MailSelectionBuilder<'a> {
//attrs: AttributeProxy,
mail_count: usize,
need_body: bool,
mi: &'a [MailIdentifiers],
meta: &'a [MailMeta],
flags: &'a [&'a Vec<String>],
bodies: &'a [Vec<u8>],
}
impl<'a> MailSelectionBuilder<'a> {
pub fn new(need_body: bool, mail_count: usize) -> Self {
Self {
mail_count,
need_body,
..MailSelectionBuilder::default()
}
}
pub fn with_mail_identifiers(&mut self, mi: &'a [MailIdentifiers]) -> &mut Self {
self.mi = mi;
self
}
pub fn with_metadata(&mut self, meta: &'a [MailMeta]) -> &mut Self {
self.meta = meta;
self
}
pub fn with_flags(&mut self, flags: &'a [&'a Vec<String>]) -> &mut Self {
self.flags = flags;
self
}
pub fn bodies_to_collect(&self) -> Vec<BodyIdentifier> {
if !self.need_body {
return vec![];
}
zip(self.mi, self.meta)
.map(|(mi, meta)| BodyIdentifier {
msg_uuid: &mi.uuid,
msg_key: &meta.message_key,
})
.collect::<Vec<_>>()
}
pub fn with_bodies(&mut self, rbodies: &'a [Vec<u8>]) -> &mut Self {
self.bodies = rbodies;
self
}
pub fn build(&self) -> Result<Vec<MailView<'a>>> {
let mut bodies = vec![];
if !self.need_body {
for m in self.meta.iter() {
let (_, hdrs) =
eml_codec::parse_imf(&m.headers).or(Err(anyhow!("Invalid mail headers")))?;
bodies.push(FetchedMail::Partial(hdrs));
}
} else {
for rb in self.bodies.iter() {
let (_, p) = eml_codec::parse_message(&rb).or(Err(anyhow!("Invalid mail body")))?;
bodies.push(FetchedMail::new_from_message(p));
}
}
if self.mi.len() != self.mail_count && self.meta.len() != self.mail_count
|| self.flags.len() != self.mail_count
|| bodies.len() != self.mail_count
{
return Err(anyhow!("Can't build a mail view selection as parts were not correctly registered into the builder."));
}
Ok(zip(self.mi, zip(self.meta, zip(self.flags, bodies)))
.map(|(ids, (meta, (flags, content)))| MailView {
ids,
meta,
flags,
content,
})
.collect())
}
}

View file

@ -13,6 +13,7 @@ pub struct Query<'a,'b> {
pub scope: QueryScope,
}
#[allow(dead_code)]
pub enum QueryScope {
Index,
Partial,
@ -106,6 +107,7 @@ impl<'a> QueryResult<'a> {
}
}
#[allow(dead_code)]
pub fn index(&self) -> &IndexEntry {
match self {
Self::IndexResult { index, .. } => index,
@ -114,7 +116,7 @@ impl<'a> QueryResult<'a> {
}
}
pub fn metadata(&self) -> Option<&MailMeta> {
pub fn metadata(&'a self) -> Option<&'a MailMeta> {
match self {
Self::IndexResult { .. } => None,
Self::PartialResult { metadata, .. } => Some(metadata),
@ -122,7 +124,8 @@ impl<'a> QueryResult<'a> {
}
}
pub fn content(&self) -> Option<&[u8]> {
#[allow(dead_code)]
pub fn content(&'a self) -> Option<&'a [u8]> {
match self {
Self::FullResult { content, .. } => Some(content),
_ => None,