diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
commit | 1a43ce5ac7033c148f64a033f2b1d335e95e11d5 (patch) | |
tree | 60b234604170fe207248458a9c4cdd3f4b7c36f2 /src/imap | |
parent | bb9cb386b65834c44cae86bd100f800883022062 (diff) | |
download | aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.tar.gz aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.zip |
WIP refactor
Diffstat (limited to 'src/imap')
-rw-r--r-- | src/imap/attributes.rs | 77 | ||||
-rw-r--r-- | src/imap/capability.rs | 159 | ||||
-rw-r--r-- | src/imap/command/anonymous.rs | 83 | ||||
-rw-r--r-- | src/imap/command/anystate.rs | 54 | ||||
-rw-r--r-- | src/imap/command/authenticated.rs | 683 | ||||
-rw-r--r-- | src/imap/command/mod.rs | 20 | ||||
-rw-r--r-- | src/imap/command/selected.rs | 424 | ||||
-rw-r--r-- | src/imap/flags.rs | 30 | ||||
-rw-r--r-- | src/imap/flow.rs | 114 | ||||
-rw-r--r-- | src/imap/imf_view.rs | 109 | ||||
-rw-r--r-- | src/imap/index.rs | 211 | ||||
-rw-r--r-- | src/imap/mail_view.rs | 306 | ||||
-rw-r--r-- | src/imap/mailbox_view.rs | 772 | ||||
-rw-r--r-- | src/imap/mime_view.rs | 580 | ||||
-rw-r--r-- | src/imap/mod.rs | 421 | ||||
-rw-r--r-- | src/imap/request.rs | 9 | ||||
-rw-r--r-- | src/imap/response.rs | 124 | ||||
-rw-r--r-- | src/imap/search.rs | 477 | ||||
-rw-r--r-- | src/imap/session.rs | 173 |
19 files changed, 0 insertions, 4826 deletions
diff --git a/src/imap/attributes.rs b/src/imap/attributes.rs deleted file mode 100644 index 89446a8..0000000 --- a/src/imap/attributes.rs +++ /dev/null @@ -1,77 +0,0 @@ -use imap_codec::imap_types::command::FetchModifier; -use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section}; - -/// Internal decisions based on fetched attributes -/// passed by the client - -pub struct AttributesProxy { - pub attrs: Vec<MessageDataItemName<'static>>, -} -impl AttributesProxy { - pub fn new( - attrs: &MacroOrMessageDataItemNames<'static>, - modifiers: &[FetchModifier], - is_uid_fetch: bool, - ) -> Self { - // Expand macros - let mut fetch_attrs = match attrs { - MacroOrMessageDataItemNames::Macro(m) => { - use imap_codec::imap_types::fetch::Macro; - use MessageDataItemName::*; - match m { - Macro::All => vec![Flags, InternalDate, Rfc822Size, Envelope], - Macro::Fast => vec![Flags, InternalDate, Rfc822Size], - Macro::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body], - _ => { - tracing::error!("unimplemented macro"); - vec![] - } - } - } - MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(), - }; - - // Handle uids - if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) { - fetch_attrs.push(MessageDataItemName::Uid); - } - - // Handle inferred MODSEQ tag - let is_changed_since = modifiers - .iter() - .any(|m| matches!(m, FetchModifier::ChangedSince(..))); - if is_changed_since && !fetch_attrs.contains(&MessageDataItemName::ModSeq) { - fetch_attrs.push(MessageDataItemName::ModSeq); - } - - Self { attrs: fetch_attrs } - } - - pub fn is_enabling_condstore(&self) -> bool { - self.attrs - .iter() - .any(|x| matches!(x, MessageDataItemName::ModSeq)) - } - - pub fn need_body(&self) -> bool { - self.attrs.iter().any(|x| match x { - MessageDataItemName::Body - | MessageDataItemName::Rfc822 - | MessageDataItemName::Rfc822Text - | MessageDataItemName::BodyStructure => true, - - MessageDataItemName::BodyExt { - section: Some(section), - partial: _, - peek: _, - } => match section { - Section::Header(None) - | Section::HeaderFields(None, _) - | Section::HeaderFieldsNot(None, _) => false, - _ => true, - }, - MessageDataItemName::BodyExt { .. } => true, - _ => false, - }) - } -} diff --git a/src/imap/capability.rs b/src/imap/capability.rs deleted file mode 100644 index c76b51c..0000000 --- a/src/imap/capability.rs +++ /dev/null @@ -1,159 +0,0 @@ -use imap_codec::imap_types::command::{FetchModifier, SelectExamineModifier, StoreModifier}; -use imap_codec::imap_types::core::Vec1; -use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; -use imap_codec::imap_types::response::Capability; -use std::collections::HashSet; - -use crate::imap::attributes::AttributesProxy; - -fn capability_unselect() -> Capability<'static> { - Capability::try_from("UNSELECT").unwrap() -} - -fn capability_condstore() -> Capability<'static> { - Capability::try_from("CONDSTORE").unwrap() -} - -fn capability_uidplus() -> Capability<'static> { - Capability::try_from("UIDPLUS").unwrap() -} - -fn capability_liststatus() -> Capability<'static> { - Capability::try_from("LIST-STATUS").unwrap() -} - -/* -fn capability_qresync() -> Capability<'static> { - Capability::try_from("QRESYNC").unwrap() -} -*/ - -#[derive(Debug, Clone)] -pub struct ServerCapability(HashSet<Capability<'static>>); - -impl Default for ServerCapability { - fn default() -> Self { - Self(HashSet::from([ - Capability::Imap4Rev1, - Capability::Enable, - Capability::Move, - Capability::LiteralPlus, - Capability::Idle, - capability_unselect(), - capability_condstore(), - capability_uidplus(), - capability_liststatus(), - //capability_qresync(), - ])) - } -} - -impl ServerCapability { - pub fn to_vec(&self) -> Vec1<Capability<'static>> { - self.0 - .iter() - .map(|v| v.clone()) - .collect::<Vec<_>>() - .try_into() - .unwrap() - } - - #[allow(dead_code)] - pub fn support(&self, cap: &Capability<'static>) -> bool { - self.0.contains(cap) - } -} - -#[derive(Clone)] -pub enum ClientStatus { - NotSupportedByServer, - Disabled, - Enabled, -} -impl ClientStatus { - pub fn is_enabled(&self) -> bool { - matches!(self, Self::Enabled) - } - - pub fn enable(&self) -> Self { - match self { - Self::Disabled => Self::Enabled, - other => other.clone(), - } - } -} - -pub struct ClientCapability { - pub condstore: ClientStatus, - pub utf8kind: Option<Utf8Kind>, -} - -impl ClientCapability { - pub fn new(sc: &ServerCapability) -> Self { - Self { - condstore: match sc.0.contains(&capability_condstore()) { - true => ClientStatus::Disabled, - _ => ClientStatus::NotSupportedByServer, - }, - utf8kind: None, - } - } - - pub fn enable_condstore(&mut self) { - self.condstore = self.condstore.enable(); - } - - pub fn attributes_enable(&mut self, ap: &AttributesProxy) { - if ap.is_enabling_condstore() { - self.enable_condstore() - } - } - - pub fn fetch_modifiers_enable(&mut self, mods: &[FetchModifier]) { - if mods - .iter() - .any(|x| matches!(x, FetchModifier::ChangedSince(..))) - { - self.enable_condstore() - } - } - - pub fn store_modifiers_enable(&mut self, mods: &[StoreModifier]) { - if mods - .iter() - .any(|x| matches!(x, StoreModifier::UnchangedSince(..))) - { - self.enable_condstore() - } - } - - pub fn select_enable(&mut self, mods: &[SelectExamineModifier]) { - for m in mods.iter() { - match m { - SelectExamineModifier::Condstore => self.enable_condstore(), - } - } - } - - pub fn try_enable( - &mut self, - caps: &[CapabilityEnable<'static>], - ) -> Vec<CapabilityEnable<'static>> { - let mut enabled = vec![]; - for cap in caps { - match cap { - CapabilityEnable::CondStore if matches!(self.condstore, ClientStatus::Disabled) => { - self.condstore = ClientStatus::Enabled; - enabled.push(cap.clone()); - } - CapabilityEnable::Utf8(kind) if Some(kind) != self.utf8kind.as_ref() => { - self.utf8kind = Some(kind.clone()); - enabled.push(cap.clone()); - } - _ => (), - } - } - - enabled - } -} diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs deleted file mode 100644 index 811d1e4..0000000 --- a/src/imap/command/anonymous.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::AString; -use imap_codec::imap_types::response::Code; -use imap_codec::imap_types::secret::Secret; - -use crate::imap::capability::ServerCapability; -use crate::imap::command::anystate; -use crate::imap::flow; -use crate::imap::response::Response; -use crate::login::ArcLoginProvider; -use crate::user::User; - -//--- dispatching - -pub struct AnonymousContext<'a> { - pub req: &'a Command<'static>, - pub server_capabilities: &'a ServerCapability, - pub login_provider: &'a ArcLoginProvider, -} - -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any State - CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to anonymous context (3 commands) - CommandBody::Login { username, password } => ctx.login(username, password).await, - CommandBody::Authenticate { .. } => { - anystate::not_implemented(ctx.req.tag.clone(), "authenticate") - } - //StartTLS is not implemented for now, we will probably go full TLS. - - // Collect other commands - _ => anystate::wrong_state(ctx.req.tag.clone()), - } -} - -//--- Command controllers, private - -impl<'a> AnonymousContext<'a> { - async fn login( - self, - username: &AString<'a>, - password: &Secret<AString<'a>>, - ) -> Result<(Response<'static>, flow::Transition)> { - let (u, p) = ( - std::str::from_utf8(username.as_ref())?, - std::str::from_utf8(password.declassify().as_ref())?, - ); - tracing::info!(user = %u, "command.login"); - - let creds = match self.login_provider.login(&u, &p).await { - Err(e) => { - tracing::debug!(error=%e, "authentication failed"); - return Ok(( - Response::build() - .to_req(self.req) - .message("Authentication failed") - .no()?, - flow::Transition::None, - )); - } - Ok(c) => c, - }; - - let user = User::new(u.to_string(), creds).await?; - - tracing::info!(username=%u, "connected"); - Ok(( - Response::build() - .to_req(self.req) - .code(Code::Capability(self.server_capabilities.to_vec())) - .message("Completed") - .ok()?, - flow::Transition::Authenticate(user), - )) - } -} diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs deleted file mode 100644 index 718ba3f..0000000 --- a/src/imap/command/anystate.rs +++ /dev/null @@ -1,54 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::core::Tag; -use imap_codec::imap_types::response::Data; - -use crate::imap::capability::ServerCapability; -use crate::imap::flow; -use crate::imap::response::Response; - -pub(crate) fn capability( - tag: Tag<'static>, - cap: &ServerCapability, -) -> Result<(Response<'static>, flow::Transition)> { - let res = Response::build() - .tag(tag) - .message("Server capabilities") - .data(Data::Capability(cap.to_vec())) - .ok()?; - - Ok((res, flow::Transition::None)) -} - -pub(crate) fn noop_nothing(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build().tag(tag).message("Noop completed.").ok()?, - flow::Transition::None, - )) -} - -pub(crate) fn logout() -> Result<(Response<'static>, flow::Transition)> { - Ok((Response::bye()?, flow::Transition::Logout)) -} - -pub(crate) fn not_implemented<'a>( - tag: Tag<'a>, - what: &str, -) -> Result<(Response<'a>, flow::Transition)> { - Ok(( - Response::build() - .tag(tag) - .message(format!("Command not implemented {}", what)) - .bad()?, - flow::Transition::None, - )) -} - -pub(crate) fn wrong_state(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .tag(tag) - .message("Command not authorized in this state") - .bad()?, - flow::Transition::None, - )) -} diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs deleted file mode 100644 index 3d332ec..0000000 --- a/src/imap/command/authenticated.rs +++ /dev/null @@ -1,683 +0,0 @@ -use std::collections::BTreeMap; -use std::sync::Arc; -use thiserror::Error; - -use anyhow::{anyhow, bail, Result}; -use imap_codec::imap_types::command::{ - Command, CommandBody, ListReturnItem, SelectExamineModifier, -}; -use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, Vec1}; -use imap_codec::imap_types::datetime::DateTime; -use imap_codec::imap_types::extensions::enable::CapabilityEnable; -use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; -use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; -use imap_codec::imap_types::response::{Code, CodeOther, Data}; -use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; - -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anystate, MailboxName}; -use crate::imap::flow; -use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; -use crate::imap::response::Response; -use crate::imap::Body; - -use crate::mail::uidindex::*; -use crate::user::User; -use crate::mail::IMF; -use crate::mail::namespace::MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW; - -pub struct AuthenticatedContext<'a> { - pub req: &'a Command<'static>, - pub server_capabilities: &'a ServerCapability, - pub client_capabilities: &'a mut ClientCapability, - pub user: &'a Arc<User>, -} - -pub async fn dispatch<'a>( - mut ctx: AuthenticatedContext<'a>, -) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any state - CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to this state (11 commands) - CommandBody::Create { mailbox } => ctx.create(mailbox).await, - CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, - CommandBody::Rename { from, to } => ctx.rename(from, to).await, - CommandBody::Lsub { - reference, - mailbox_wildcard, - } => ctx.list(reference, mailbox_wildcard, &[], true).await, - CommandBody::List { - reference, - mailbox_wildcard, - r#return, - } => ctx.list(reference, mailbox_wildcard, r#return, false).await, - CommandBody::Status { - mailbox, - item_names, - } => ctx.status(mailbox, item_names).await, - CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, - CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, - CommandBody::Select { mailbox, modifiers } => ctx.select(mailbox, modifiers).await, - CommandBody::Examine { mailbox, modifiers } => ctx.examine(mailbox, modifiers).await, - CommandBody::Append { - mailbox, - flags, - date, - message, - } => ctx.append(mailbox, flags, date, message).await, - - // rfc5161 ENABLE - CommandBody::Enable { capabilities } => ctx.enable(capabilities), - - // Collect other commands - _ => anystate::wrong_state(ctx.req.tag.clone()), - } -} - -// --- PRIVATE --- -impl<'a> AuthenticatedContext<'a> { - async fn create( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name = match mailbox { - MailboxCodec::Inbox => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Cannot create INBOX") - .bad()?, - flow::Transition::None, - )); - } - MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?, - }; - - match self.user.create_mailbox(&name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("CREATE complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(&e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn delete( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - match self.user.delete_mailbox(&name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("DELETE complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn rename( - self, - from: &MailboxCodec<'a>, - to: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(from).try_into()?; - let new_name: &str = MailboxName(to).try_into()?; - - match self.user.rename_mailbox(&name, &new_name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("RENAME complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn list( - &mut self, - reference: &MailboxCodec<'a>, - mailbox_wildcard: &ListMailbox<'a>, - must_return: &[ListReturnItem], - is_lsub: bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); - - let reference: &str = MailboxName(reference).try_into()?; - if !reference.is_empty() { - return Ok(( - Response::build() - .to_req(self.req) - .message("References not supported") - .bad()?, - flow::Transition::None, - )); - } - - let status_item_names = must_return.iter().find_map(|m| match m { - ListReturnItem::Status(v) => Some(v), - _ => None, - }); - - // @FIXME would probably need a rewrite to better use the imap_codec library - let wildcard = match mailbox_wildcard { - ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?, - ListMailbox::String(v) => std::str::from_utf8(v.as_ref())?, - }; - if wildcard.is_empty() { - if is_lsub { - return Ok(( - Response::build() - .to_req(self.req) - .message("LSUB complete") - .data(Data::Lsub { - items: vec![], - delimiter: Some(mbx_hier_delim), - mailbox: "".try_into().unwrap(), - }) - .ok()?, - flow::Transition::None, - )); - } else { - return Ok(( - Response::build() - .to_req(self.req) - .message("LIST complete") - .data(Data::List { - items: vec![], - delimiter: Some(mbx_hier_delim), - mailbox: "".try_into().unwrap(), - }) - .ok()?, - flow::Transition::None, - )); - } - } - - let mailboxes = self.user.list_mailboxes().await?; - let mut vmailboxes = BTreeMap::new(); - for mb in mailboxes.iter() { - for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) { - if i > 0 { - let smb = &mb[..i]; - vmailboxes.entry(smb).or_insert(false); - } - } - vmailboxes.insert(mb, true); - } - - let mut ret = vec![]; - for (mb, is_real) in vmailboxes.iter() { - if matches_wildcard(&wildcard, mb) { - let mailbox: MailboxCodec = mb - .to_string() - .try_into() - .map_err(|_| anyhow!("invalid mailbox name"))?; - let mut items = vec![FlagNameAttribute::from(Atom::unvalidated("Subscribed"))]; - - // Decoration - if !*is_real { - items.push(FlagNameAttribute::Noselect); - } else { - match *mb { - "Drafts" => items.push(Atom::unvalidated("Drafts").into()), - "Archive" => items.push(Atom::unvalidated("Archive").into()), - "Sent" => items.push(Atom::unvalidated("Sent").into()), - "Trash" => items.push(Atom::unvalidated("Trash").into()), - _ => (), - }; - } - - // Result type - if is_lsub { - ret.push(Data::Lsub { - items, - delimiter: Some(mbx_hier_delim), - mailbox: mailbox.clone(), - }); - } else { - ret.push(Data::List { - items, - delimiter: Some(mbx_hier_delim), - mailbox: mailbox.clone(), - }); - } - - // Also collect status - if let Some(sin) = status_item_names { - let ret_attrs = match self.status_items(mb, sin).await { - Ok(a) => a, - Err(e) => { - tracing::error!(err=?e, mailbox=%mb, "Unable to fetch status for mailbox"); - continue; - } - }; - - let data = Data::Status { - mailbox, - items: ret_attrs.into(), - }; - - ret.push(data); - } - } - } - - let msg = if is_lsub { - "LSUB completed" - } else { - "LIST completed" - }; - Ok(( - Response::build() - .to_req(self.req) - .message(msg) - .many_data(ret) - .ok()?, - flow::Transition::None, - )) - } - - async fn status( - &mut self, - mailbox: &MailboxCodec<'static>, - attributes: &[StatusDataItemName], - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - let ret_attrs = match self.status_items(name, attributes).await { - Ok(v) => v, - Err(e) => match e.downcast_ref::<CommandError>() { - Some(CommandError::MailboxNotFound) => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - _ => return Err(e.into()), - }, - }; - - let data = Data::Status { - mailbox: mailbox.clone(), - items: ret_attrs.into(), - }; - - Ok(( - Response::build() - .to_req(self.req) - .message("STATUS completed") - .data(data) - .ok()?, - flow::Transition::None, - )) - } - - async fn status_items( - &mut self, - name: &str, - attributes: &[StatusDataItemName], - ) -> Result<Vec<StatusDataItem>> { - let mb_opt = self.user.open_mailbox(name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => return Err(CommandError::MailboxNotFound.into()), - }; - - let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - - let mut ret_attrs = vec![]; - for attr in attributes.iter() { - ret_attrs.push(match attr { - StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?), - StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32), - StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?), - StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()), - StatusDataItemName::UidValidity => { - StatusDataItem::UidValidity(view.uidvalidity()) - } - StatusDataItemName::Deleted => { - bail!("quota not implemented, can't return deleted elements waiting for EXPUNGE"); - }, - StatusDataItemName::DeletedStorage => { - bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); - }, - StatusDataItemName::HighestModSeq => { - self.client_capabilities.enable_condstore(); - StatusDataItem::HighestModSeq(view.highestmodseq().get()) - }, - }); - } - Ok(ret_attrs) - } - - async fn subscribe( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - if self.user.has_mailbox(&name).await? { - Ok(( - Response::build() - .to_req(self.req) - .message("SUBSCRIBE complete") - .ok()?, - flow::Transition::None, - )) - } else { - Ok(( - Response::build() - .to_req(self.req) - .message(format!("Mailbox {} does not exist", name)) - .bad()?, - flow::Transition::None, - )) - } - } - - async fn unsubscribe( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - if self.user.has_mailbox(&name).await? { - Ok(( - Response::build() - .to_req(self.req) - .message(format!( - "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", - name - )) - .bad()?, - flow::Transition::None, - )) - } else { - Ok(( - Response::build() - .to_req(self.req) - .message(format!("Mailbox {} does not exist", name)) - .no()?, - flow::Transition::None, - )) - } - } - - /* - * TRACE BEGIN --- - - - Example: C: A142 SELECT INBOX - S: * 172 EXISTS - S: * 1 RECENT - S: * OK [UNSEEN 12] Message 12 is first unseen - S: * OK [UIDVALIDITY 3857529045] UIDs valid - S: * OK [UIDNEXT 4392] Predicted next UID - S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) - S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited - S: A142 OK [READ-WRITE] SELECT completed - - --- a mailbox with no unseen message -> no unseen entry - NOTES: - RFC3501 (imap4rev1) says if there is no OK [UNSEEN] response, client must make no assumption, - it is therefore correct to not return it even if there are unseen messages - RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE - For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications. - - - 20 select "INBOX.achats" - * FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1) - * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1 \*)] Flags permitted. - * 88 EXISTS - * 0 RECENT - * OK [UIDVALIDITY 1347986788] UIDs valid - * OK [UIDNEXT 91] Predicted next UID - * OK [HIGHESTMODSEQ 72] Highest - 20 OK [READ-WRITE] Select completed (0.001 + 0.000 secs). - - * TRACE END --- - */ - async fn select( - self, - mailbox: &MailboxCodec<'a>, - modifiers: &[SelectExamineModifier], - ) -> Result<(Response<'static>, flow::Transition)> { - self.client_capabilities.select_enable(modifiers); - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - }; - tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); - - let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - let data = mb.summary()?; - - Ok(( - Response::build() - .message("Select completed") - .to_req(self.req) - .code(Code::ReadWrite) - .set_body(data) - .ok()?, - flow::Transition::Select(mb, flow::MailboxPerm::ReadWrite), - )) - } - - async fn examine( - self, - mailbox: &MailboxCodec<'a>, - modifiers: &[SelectExamineModifier], - ) -> Result<(Response<'static>, flow::Transition)> { - self.client_capabilities.select_enable(modifiers); - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - }; - tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); - - let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - let data = mb.summary()?; - - Ok(( - Response::build() - .to_req(self.req) - .message("Examine completed") - .code(Code::ReadOnly) - .set_body(data) - .ok()?, - flow::Transition::Select(mb, flow::MailboxPerm::ReadOnly), - )) - } - - //@FIXME we should write a specific version for the "selected" state - //that returns some unsollicited responses - async fn append( - self, - mailbox: &MailboxCodec<'a>, - flags: &[Flag<'a>], - date: &Option<DateTime>, - message: &Literal<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let append_tag = self.req.tag.clone(); - match self.append_internal(mailbox, flags, date, message).await { - Ok((_mb_view, uidvalidity, uid, _modseq)) => Ok(( - Response::build() - .tag(append_tag) - .message("APPEND completed") - .code(Code::Other(CodeOther::unvalidated( - format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), - ))) - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .tag(append_tag) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - fn enable( - self, - cap_enable: &Vec1<CapabilityEnable<'static>>, - ) -> Result<(Response<'static>, flow::Transition)> { - let mut response_builder = Response::build().to_req(self.req); - let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref()); - if capabilities.len() > 0 { - response_builder = response_builder.data(Data::Enabled { capabilities }); - } - Ok(( - response_builder.message("ENABLE completed").ok()?, - flow::Transition::None, - )) - } - - //@FIXME should be refactored and integrated to the mailbox view - pub(crate) async fn append_internal( - self, - mailbox: &MailboxCodec<'a>, - flags: &[Flag<'a>], - date: &Option<DateTime>, - message: &Literal<'a>, - ) -> Result<(MailboxView, ImapUidvalidity, ImapUid, ModSeq)> { - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => bail!("Mailbox does not exist"), - }; - let mut view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - - if date.is_some() { - tracing::warn!("Cannot set date when appending message"); - } - - let msg = - IMF::try_from(message.data()).map_err(|_| anyhow!("Could not parse e-mail message"))?; - let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); - // TODO: filter allowed flags? ping @Quentin - - let (uidvalidity, uid, modseq) = - view.internal.mailbox.append(msg, None, &flags[..]).await?; - //let unsollicited = view.update(UpdateParameters::default()).await?; - - Ok((view, uidvalidity, uid, modseq)) - } -} - -fn matches_wildcard(wildcard: &str, name: &str) -> bool { - let wildcard = wildcard.chars().collect::<Vec<char>>(); - let name = name.chars().collect::<Vec<char>>(); - - let mut matches = vec![vec![false; wildcard.len() + 1]; name.len() + 1]; - - for i in 0..=name.len() { - for j in 0..=wildcard.len() { - matches[i][j] = (i == 0 && j == 0) - || (j > 0 - && matches[i][j - 1] - && (wildcard[j - 1] == '%' || wildcard[j - 1] == '*')) - || (i > 0 - && j > 0 - && matches[i - 1][j - 1] - && wildcard[j - 1] == name[i - 1] - && wildcard[j - 1] != '%' - && wildcard[j - 1] != '*') - || (i > 0 - && j > 0 - && matches[i - 1][j] - && (wildcard[j - 1] == '*' - || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW))); - } - } - - matches[name.len()][wildcard.len()] -} - -#[derive(Error, Debug)] -pub enum CommandError { - #[error("Mailbox not found")] - MailboxNotFound, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_wildcard_matches() { - assert!(matches_wildcard("INBOX", "INBOX")); - assert!(matches_wildcard("*", "INBOX")); - assert!(matches_wildcard("%", "INBOX")); - assert!(!matches_wildcard("%", "Test.Azerty")); - assert!(!matches_wildcard("INBOX.*", "INBOX")); - assert!(matches_wildcard("Sent.*", "Sent.A")); - assert!(matches_wildcard("Sent.*", "Sent.A.B")); - assert!(!matches_wildcard("Sent.%", "Sent.A.B")); - } -} diff --git a/src/imap/command/mod.rs b/src/imap/command/mod.rs deleted file mode 100644 index f201eb6..0000000 --- a/src/imap/command/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod anonymous; -pub mod anystate; -pub mod authenticated; -pub mod selected; - -use crate::mail::namespace::INBOX; -use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; - -/// Convert an IMAP mailbox name/identifier representation -/// to an utf-8 string that is used internally in Aerogramme -struct MailboxName<'a>(&'a MailboxCodec<'a>); -impl<'a> TryInto<&'a str> for MailboxName<'a> { - type Error = std::str::Utf8Error; - fn try_into(self) -> Result<&'a str, Self::Error> { - match self.0 { - MailboxCodec::Inbox => Ok(INBOX), - MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), - } - } -} diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs deleted file mode 100644 index eedfbd6..0000000 --- a/src/imap/command/selected.rs +++ /dev/null @@ -1,424 +0,0 @@ -use std::num::NonZeroU64; -use std::sync::Arc; - -use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier}; -use imap_codec::imap_types::core::{Charset, Vec1}; -use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; -use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; -use imap_codec::imap_types::response::{Code, CodeOther}; -use imap_codec::imap_types::search::SearchKey; -use imap_codec::imap_types::sequence::SequenceSet; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anystate, authenticated, MailboxName}; -use crate::imap::flow; -use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; -use crate::imap::response::Response; -use crate::user::User; - -pub struct SelectedContext<'a> { - pub req: &'a Command<'static>, - pub user: &'a Arc<User>, - pub mailbox: &'a mut MailboxView, - pub server_capabilities: &'a ServerCapability, - pub client_capabilities: &'a mut ClientCapability, - pub perm: &'a flow::MailboxPerm, -} - -pub async fn dispatch<'a>( - ctx: SelectedContext<'a>, -) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any State - // noop is specific to this state - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to this state (7 commands + NOOP) - CommandBody::Close => match ctx.perm { - flow::MailboxPerm::ReadWrite => ctx.close().await, - flow::MailboxPerm::ReadOnly => ctx.examine_close().await, - }, - CommandBody::Noop | CommandBody::Check => ctx.noop().await, - CommandBody::Fetch { - sequence_set, - macro_or_item_names, - modifiers, - uid, - } => { - ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid) - .await - } - //@FIXME SearchKey::And is a legacy hack, should be refactored - CommandBody::Search { - charset, - criteria, - uid, - } => { - ctx.search(charset, &SearchKey::And(criteria.clone()), uid) - .await - } - CommandBody::Expunge { - // UIDPLUS (rfc4315) - uid_sequence_set, - } => ctx.expunge(uid_sequence_set).await, - CommandBody::Store { - sequence_set, - kind, - response, - flags, - modifiers, - uid, - } => { - ctx.store(sequence_set, kind, response, flags, modifiers, uid) - .await - } - CommandBody::Copy { - sequence_set, - mailbox, - uid, - } => ctx.copy(sequence_set, mailbox, uid).await, - CommandBody::Move { - sequence_set, - mailbox, - uid, - } => ctx.r#move(sequence_set, mailbox, uid).await, - - // UNSELECT extension (rfc3691) - CommandBody::Unselect => ctx.unselect().await, - - // In selected mode, we fallback to authenticated when needed - _ => { - authenticated::dispatch(authenticated::AuthenticatedContext { - req: ctx.req, - server_capabilities: ctx.server_capabilities, - client_capabilities: ctx.client_capabilities, - user: ctx.user, - }) - .await - } - } -} - -// --- PRIVATE --- - -impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response<'static>, flow::Transition)> { - // We expunge messages, - // but we don't send the untagged EXPUNGE responses - let tag = self.req.tag.clone(); - self.expunge(&None).await?; - Ok(( - Response::build().tag(tag).message("CLOSE completed").ok()?, - flow::Transition::Unselect, - )) - } - - /// CLOSE in examined state is not the same as in selected state - /// (in selected state it also does an EXPUNGE, here it doesn't) - async fn examine_close(self) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .to_req(self.req) - .message("CLOSE completed") - .ok()?, - flow::Transition::Unselect, - )) - } - - async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .to_req(self.req) - .message("UNSELECT completed") - .ok()?, - flow::Transition::Unselect, - )) - } - - pub async fn fetch( - self, - sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'static>, - modifiers: &[FetchModifier], - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let ap = AttributesProxy::new(attributes, modifiers, *uid); - let mut changed_since: Option<NonZeroU64> = None; - modifiers.iter().for_each(|m| match m { - FetchModifier::ChangedSince(val) => { - changed_since = Some(*val); - } - }); - - match self - .mailbox - .fetch(sequence_set, &ap, changed_since, uid) - .await - { - Ok(resp) => { - // Capabilities enabling logic only on successful command - // (according to my understanding of the spec) - self.client_capabilities.attributes_enable(&ap); - self.client_capabilities.fetch_modifiers_enable(modifiers); - - // Response to the client - Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )) - } - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - pub async fn search( - self, - charset: &Option<Charset<'a>>, - criteria: &SearchKey<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?; - if enable_condstore { - self.client_capabilities.enable_condstore(); - } - Ok(( - Response::build() - .to_req(self.req) - .set_body(found) - .message("SEARCH completed") - .ok()?, - flow::Transition::None, - )) - } - - pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { - self.mailbox.internal.mailbox.force_sync().await?; - - let updates = self.mailbox.update(UpdateParameters::default()).await?; - Ok(( - Response::build() - .to_req(self.req) - .message("NOOP completed.") - .set_body(updates) - .ok()?, - flow::Transition::None, - )) - } - - async fn expunge( - self, - uid_sequence_set: &Option<SequenceSet>, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let tag = self.req.tag.clone(); - let data = self.mailbox.expunge(uid_sequence_set).await?; - - Ok(( - Response::build() - .tag(tag) - .message("EXPUNGE completed") - .set_body(data) - .ok()?, - flow::Transition::None, - )) - } - - async fn store( - self, - sequence_set: &SequenceSet, - kind: &StoreType, - response: &StoreResponse, - flags: &[Flag<'a>], - modifiers: &[StoreModifier], - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let mut unchanged_since: Option<NonZeroU64> = None; - modifiers.iter().for_each(|m| match m { - StoreModifier::UnchangedSince(val) => { - unchanged_since = Some(*val); - } - }); - - let (data, modified) = self - .mailbox - .store(sequence_set, kind, response, flags, unchanged_since, uid) - .await?; - - let mut ok_resp = Response::build() - .to_req(self.req) - .message("STORE completed") - .set_body(data); - - match modified[..] { - [] => (), - [_head, ..] => { - let modified_str = format!( - "MODIFIED {}", - modified - .into_iter() - .map(|x| x.to_string()) - .collect::<Vec<_>>() - .join(",") - ); - ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated( - modified_str.into_bytes(), - ))); - } - }; - - self.client_capabilities.store_modifiers_enable(modifiers); - - Ok((ok_resp.ok()?, flow::Transition::None)) - } - - async fn copy( - self, - sequence_set: &SequenceSet, - mailbox: &MailboxCodec<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - //@FIXME Could copy be valid in EXAMINE mode? - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Destination mailbox does not exist") - .code(Code::TryCreate) - .no()?, - flow::Transition::None, - )) - } - }; - - let (uidval, uid_map) = self.mailbox.copy(sequence_set, mb, uid).await?; - - let copyuid_str = format!( - "{} {} {}", - uidval, - uid_map - .iter() - .map(|(sid, _)| format!("{}", sid)) - .collect::<Vec<_>>() - .join(","), - uid_map - .iter() - .map(|(_, tuid)| format!("{}", tuid)) - .collect::<Vec<_>>() - .join(",") - ); - - Ok(( - Response::build() - .to_req(self.req) - .message("COPY completed") - .code(Code::Other(CodeOther::unvalidated( - format!("COPYUID {}", copyuid_str).into_bytes(), - ))) - .ok()?, - flow::Transition::None, - )) - } - - async fn r#move( - self, - sequence_set: &SequenceSet, - mailbox: &MailboxCodec<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Destination mailbox does not exist") - .code(Code::TryCreate) - .no()?, - flow::Transition::None, - )) - } - }; - - let (uidval, uid_map, data) = self.mailbox.r#move(sequence_set, mb, uid).await?; - - // compute code - let copyuid_str = format!( - "{} {} {}", - uidval, - uid_map - .iter() - .map(|(sid, _)| format!("{}", sid)) - .collect::<Vec<_>>() - .join(","), - uid_map - .iter() - .map(|(_, tuid)| format!("{}", tuid)) - .collect::<Vec<_>>() - .join(",") - ); - - Ok(( - Response::build() - .to_req(self.req) - .message("COPY completed") - .code(Code::Other(CodeOther::unvalidated( - format!("COPYUID {}", copyuid_str).into_bytes(), - ))) - .set_body(data) - .ok()?, - flow::Transition::None, - )) - } - - fn fail_read_only(&self) -> Option<Response<'static>> { - match self.perm { - flow::MailboxPerm::ReadWrite => None, - flow::MailboxPerm::ReadOnly => Some( - Response::build() - .to_req(self.req) - .message("Write command are forbidden while exmining mailbox") - .no() - .unwrap(), - ), - } - } -} diff --git a/src/imap/flags.rs b/src/imap/flags.rs deleted file mode 100644 index 0f6ec64..0000000 --- a/src/imap/flags.rs +++ /dev/null @@ -1,30 +0,0 @@ -use imap_codec::imap_types::core::Atom; -use imap_codec::imap_types::flag::{Flag, FlagFetch}; - -pub fn from_str(f: &str) -> Option<FlagFetch<'static>> { - match f.chars().next() { - Some('\\') => match f { - "\\Seen" => Some(FlagFetch::Flag(Flag::Seen)), - "\\Answered" => Some(FlagFetch::Flag(Flag::Answered)), - "\\Flagged" => Some(FlagFetch::Flag(Flag::Flagged)), - "\\Deleted" => Some(FlagFetch::Flag(Flag::Deleted)), - "\\Draft" => Some(FlagFetch::Flag(Flag::Draft)), - "\\Recent" => Some(FlagFetch::Recent), - _ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) { - Err(_) => { - tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); - None - } - Ok(a) => Some(FlagFetch::Flag(Flag::system(a))), - }, - }, - Some(_) => match Atom::try_from(f.to_string()) { - Err(_) => { - tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); - None - } - Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))), - }, - None => None, - } -} diff --git a/src/imap/flow.rs b/src/imap/flow.rs deleted file mode 100644 index 86eb12e..0000000 --- a/src/imap/flow.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::error::Error as StdError; -use std::fmt; -use std::sync::Arc; - -use imap_codec::imap_types::core::Tag; -use tokio::sync::Notify; - -use crate::imap::mailbox_view::MailboxView; -use crate::user::User; - -#[derive(Debug)] -pub enum Error { - ForbiddenTransition, -} -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Forbidden Transition") - } -} -impl StdError for Error {} - -pub enum State { - NotAuthenticated, - Authenticated(Arc<User>), - Selected(Arc<User>, MailboxView, MailboxPerm), - Idle( - Arc<User>, - MailboxView, - MailboxPerm, - Tag<'static>, - Arc<Notify>, - ), - Logout, -} -impl State { - pub fn notify(&self) -> Option<Arc<Notify>> { - match self { - Self::Idle(_, _, _, _, anotif) => Some(anotif.clone()), - _ => None, - } - } -} -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use State::*; - match self { - NotAuthenticated => write!(f, "NotAuthenticated"), - Authenticated(..) => write!(f, "Authenticated"), - Selected(..) => write!(f, "Selected"), - Idle(..) => write!(f, "Idle"), - Logout => write!(f, "Logout"), - } - } -} - -#[derive(Clone)] -pub enum MailboxPerm { - ReadOnly, - ReadWrite, -} - -pub enum Transition { - None, - Authenticate(Arc<User>), - Select(MailboxView, MailboxPerm), - Idle(Tag<'static>, Notify), - UnIdle, - Unselect, - Logout, -} -impl fmt::Display for Transition { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Transition::*; - match self { - None => write!(f, "None"), - Authenticate(..) => write!(f, "Authenticated"), - Select(..) => write!(f, "Selected"), - Idle(..) => write!(f, "Idle"), - UnIdle => write!(f, "UnIdle"), - Unselect => write!(f, "Unselect"), - Logout => write!(f, "Logout"), - } - } -} - -// See RFC3501 section 3. -// https://datatracker.ietf.org/doc/html/rfc3501#page-13 -impl State { - pub fn apply(&mut self, tr: Transition) -> Result<(), Error> { - tracing::debug!(state=%self, transition=%tr, "try change state"); - - let new_state = match (std::mem::replace(self, State::Logout), tr) { - (s, Transition::None) => s, - (State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u), - (State::Authenticated(u) | State::Selected(u, _, _), Transition::Select(m, p)) => { - State::Selected(u, m, p) - } - (State::Selected(u, _, _), Transition::Unselect) => State::Authenticated(u.clone()), - (State::Selected(u, m, p), Transition::Idle(t, s)) => { - State::Idle(u, m, p, t, Arc::new(s)) - } - (State::Idle(u, m, p, _, _), Transition::UnIdle) => State::Selected(u, m, p), - (_, Transition::Logout) => State::Logout, - (s, t) => { - tracing::error!(state=%s, transition=%t, "forbidden transition"); - return Err(Error::ForbiddenTransition); - } - }; - *self = new_state; - tracing::debug!(state=%self, "transition succeeded"); - - Ok(()) - } -} diff --git a/src/imap/imf_view.rs b/src/imap/imf_view.rs deleted file mode 100644 index a4ca2e8..0000000 --- a/src/imap/imf_view.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anyhow::{anyhow, Result}; -use chrono::naive::NaiveDate; - -use imap_codec::imap_types::core::{IString, NString}; -use imap_codec::imap_types::envelope::{Address, Envelope}; - -use eml_codec::imf; - -pub struct ImfView<'a>(pub &'a imf::Imf<'a>); - -impl<'a> ImfView<'a> { - pub fn naive_date(&self) -> Result<NaiveDate> { - Ok(self.0.date.ok_or(anyhow!("date is not set"))?.date_naive()) - } - - /// Envelope rules are defined in RFC 3501, section 7.4.2 - /// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2 - /// - /// Some important notes: - /// - /// If the Sender or Reply-To lines are absent in the [RFC-2822] - /// header, or are present but empty, the server sets the - /// corresponding member of the envelope to be the same value as - /// the from member (the client is not expected to know to do - /// this). Note: [RFC-2822] requires that all messages have a valid - /// From header. Therefore, the from, sender, and reply-to - /// members in the envelope can not be NIL. - /// - /// If the Date, Subject, In-Reply-To, and Message-ID header lines - /// are absent in the [RFC-2822] header, the corresponding member - /// of the envelope is NIL; if these header lines are present but - /// empty the corresponding member of the envelope is the empty - /// string. - - //@FIXME return an error if the envelope is invalid instead of panicking - //@FIXME some fields must be defaulted if there are not set. - pub fn message_envelope(&self) -> Envelope<'static> { - let msg = self.0; - let from = msg.from.iter().map(convert_mbx).collect::<Vec<_>>(); - - Envelope { - date: NString( - msg.date - .as_ref() - .map(|d| IString::try_from(d.to_rfc3339()).unwrap()), - ), - subject: NString( - msg.subject - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - sender: msg - .sender - .as_ref() - .map(|v| vec![convert_mbx(v)]) - .unwrap_or(from.clone()), - reply_to: if msg.reply_to.is_empty() { - from.clone() - } else { - convert_addresses(&msg.reply_to) - }, - from, - to: convert_addresses(&msg.to), - cc: convert_addresses(&msg.cc), - bcc: convert_addresses(&msg.bcc), - in_reply_to: NString( - msg.in_reply_to - .iter() - .next() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - message_id: NString( - msg.msg_id - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - } - } -} - -pub fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address<'static>> { - let mut acc = vec![]; - for item in addrlist { - match item { - imf::address::AddressRef::Single(a) => acc.push(convert_mbx(a)), - imf::address::AddressRef::Many(l) => acc.extend(l.participants.iter().map(convert_mbx)), - } - } - return acc; -} - -pub fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address<'static> { - Address { - name: NString( - addr.name - .as_ref() - .map(|x| IString::try_from(x.to_string()).unwrap()), - ), - // SMTP at-domain-list (source route) seems obsolete since at least 1991 - // https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html - adl: NString(None), - mailbox: NString(Some( - IString::try_from(addr.addrspec.local_part.to_string()).unwrap(), - )), - host: NString(Some( - IString::try_from(addr.addrspec.domain.to_string()).unwrap(), - )), - } -} diff --git a/src/imap/index.rs b/src/imap/index.rs deleted file mode 100644 index 9b794b8..0000000 --- a/src/imap/index.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::num::{NonZeroU32, NonZeroU64}; - -use anyhow::{anyhow, Result}; -use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; - -use crate::mail::uidindex::{ImapUid, ModSeq, UidIndex}; -use crate::mail::unique_ident::UniqueIdent; - -pub struct Index<'a> { - pub imap_index: Vec<MailIndex<'a>>, - pub internal: &'a UidIndex, -} -impl<'a> Index<'a> { - pub fn new(internal: &'a UidIndex) -> Result<Self> { - let imap_index = internal - .idx_by_uid - .iter() - .enumerate() - .map(|(i_enum, (&uid, &uuid))| { - let (_, modseq, flags) = internal - .table - .get(&uuid) - .ok_or(anyhow!("mail is missing from index"))?; - let i_int: u32 = (i_enum + 1).try_into()?; - let i: NonZeroU32 = i_int.try_into()?; - - Ok(MailIndex { - i, - uid, - uuid, - modseq: *modseq, - flags, - }) - }) - .collect::<Result<Vec<_>>>()?; - - Ok(Self { - imap_index, - internal, - }) - } - - pub fn last(&'a self) -> Option<&'a MailIndex<'a>> { - self.imap_index.last() - } - - /// Fetch mail descriptors based on a sequence of UID - /// - /// Complexity analysis: - /// - Sort is O(n * log n) where n is the number of uid generated by the sequence - /// - Finding the starting point in the index O(log m) where m is the size of the mailbox - /// While n =< m, it's not clear if the difference is big or not. - /// - /// For now, the algorithm tries to be fast for small values of n, - /// as it is what is expected by clients. - /// - /// So we assume for our implementation that : n << m. - /// It's not true for full mailbox searches for example... - pub fn fetch_on_uid(&'a self, sequence_set: &SequenceSet) -> Vec<&'a MailIndex<'a>> { - if self.imap_index.is_empty() { - return vec![]; - } - let largest = self.last().expect("The mailbox is not empty").uid; - let mut unroll_seq = sequence_set.iter(largest).collect::<Vec<_>>(); - unroll_seq.sort(); - - let start_seq = match unroll_seq.iter().next() { - Some(elem) => elem, - None => return vec![], - }; - - // Quickly jump to the right point in the mailbox vector O(log m) instead - // of iterating one by one O(m). Works only because both unroll_seq & imap_index are sorted per uid. - let mut imap_idx = { - let start_idx = self - .imap_index - .partition_point(|mail_idx| &mail_idx.uid < start_seq); - &self.imap_index[start_idx..] - }; - - let mut acc = vec![]; - for wanted_uid in unroll_seq.iter() { - // Slide the window forward as long as its first element is lower than our wanted uid. - let start_idx = match imap_idx.iter().position(|midx| &midx.uid >= wanted_uid) { - Some(v) => v, - None => break, - }; - imap_idx = &imap_idx[start_idx..]; - - // If the beginning of our new window is the uid we want, we collect it - if &imap_idx[0].uid == wanted_uid { - acc.push(&imap_idx[0]); - } - } - - acc - } - - pub fn fetch_on_id(&'a self, sequence_set: &SequenceSet) -> Result<Vec<&'a MailIndex<'a>>> { - if self.imap_index.is_empty() { - return Ok(vec![]); - } - let largest = NonZeroU32::try_from(self.imap_index.len() as u32)?; - let mut acc = sequence_set - .iter(largest) - .map(|wanted_id| { - self.imap_index - .get((wanted_id.get() as usize) - 1) - .ok_or(anyhow!("Mail not found")) - }) - .collect::<Result<Vec<_>>>()?; - - // Sort the result to be consistent with UID - acc.sort_by(|a, b| a.i.cmp(&b.i)); - - Ok(acc) - } - - pub fn fetch( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - by_uid: bool, - ) -> Result<Vec<&'a MailIndex<'a>>> { - match by_uid { - true => Ok(self.fetch_on_uid(sequence_set)), - _ => self.fetch_on_id(sequence_set), - } - } - - pub fn fetch_changed_since( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - maybe_modseq: Option<NonZeroU64>, - by_uid: bool, - ) -> Result<Vec<&'a MailIndex<'a>>> { - let raw = self.fetch(sequence_set, by_uid)?; - let res = match maybe_modseq { - Some(pit) => raw.into_iter().filter(|midx| midx.modseq > pit).collect(), - None => raw, - }; - - Ok(res) - } - - pub fn fetch_unchanged_since( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - maybe_modseq: Option<NonZeroU64>, - by_uid: bool, - ) -> Result<(Vec<&'a MailIndex<'a>>, Vec<&'a MailIndex<'a>>)> { - let raw = self.fetch(sequence_set, by_uid)?; - let res = match maybe_modseq { - Some(pit) => raw.into_iter().partition(|midx| midx.modseq <= pit), - None => (raw, vec![]), - }; - - Ok(res) - } -} - -#[derive(Clone, Debug)] -pub struct MailIndex<'a> { - pub i: NonZeroU32, - pub uid: ImapUid, - pub uuid: UniqueIdent, - pub modseq: ModSeq, - pub flags: &'a Vec<String>, -} - -impl<'a> MailIndex<'a> { - // The following functions are used to implement the SEARCH command - pub fn is_in_sequence_i(&self, seq: &Sequence) -> bool { - match seq { - Sequence::Single(SeqOrUid::Asterisk) => true, - Sequence::Single(SeqOrUid::Value(target)) => target == &self.i, - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) - | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.i, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - if x1 < x2 { - x1 <= &self.i && &self.i <= x2 - } else { - x1 >= &self.i && &self.i >= x2 - } - } - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, - } - } - - pub fn is_in_sequence_uid(&self, seq: &Sequence) -> bool { - match seq { - Sequence::Single(SeqOrUid::Asterisk) => true, - Sequence::Single(SeqOrUid::Value(target)) => target == &self.uid, - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) - | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.uid, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - if x1 < x2 { - x1 <= &self.uid && &self.uid <= x2 - } else { - x1 >= &self.uid && &self.uid >= x2 - } - } - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, - } - } - - pub fn is_flag_set(&self, flag: &str) -> bool { - self.flags - .iter() - .any(|candidate| candidate.as_str() == flag) - } -} diff --git a/src/imap/mail_view.rs b/src/imap/mail_view.rs deleted file mode 100644 index a8db733..0000000 --- a/src/imap/mail_view.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::num::NonZeroU32; - -use anyhow::{anyhow, bail, Result}; -use chrono::{naive::NaiveDate, DateTime as ChronoDateTime, Local, Offset, TimeZone, Utc}; - -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, -}; -use imap_codec::imap_types::flag::Flag; -use imap_codec::imap_types::response::Data; - -use eml_codec::{ - imf, - part::{composite::Message, AnyPart}, -}; - -use crate::mail::query::QueryResult; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::flags; -use crate::imap::imf_view::ImfView; -use crate::imap::index::MailIndex; -use crate::imap::mime_view; -use crate::imap::response::Body; - -pub struct MailView<'a> { - pub in_idx: &'a MailIndex<'a>, - pub query_result: &'a QueryResult, - pub content: FetchedMail<'a>, -} - -impl<'a> MailView<'a> { - pub fn new(query_result: &'a QueryResult, in_idx: &'a 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).or(Err(anyhow!("Invalid mail body")))?; - FetchedMail::full_from_message(parsed) - } - QueryResult::PartialResult { metadata, .. } => { - let (_, parsed) = eml_codec::parse_message(&metadata.headers) - .or(Err(anyhow!("unable to parse email headers")))?; - FetchedMail::partial_from_message(parsed) - } - QueryResult::IndexResult { .. } => FetchedMail::IndexOnly, - }, - }) - } - - pub fn imf(&self) -> Option<ImfView> { - self.content.as_imf().map(ImfView) - } - - pub fn selected_mime(&'a self) -> Option<mime_view::SelectedMime<'a>> { - self.content.as_anypart().ok().map(mime_view::SelectedMime) - } - - pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { - let mut seen = SeenFlag::DoNothing; - let res_attrs = ap - .attrs - .iter() - .map(|attr| match attr { - MessageDataItemName::Uid => Ok(self.uid()), - MessageDataItemName::Flags => Ok(self.flags()), - MessageDataItemName::Rfc822Size => self.rfc_822_size(), - MessageDataItemName::Rfc822Header => self.rfc_822_header(), - MessageDataItemName::Rfc822Text => self.rfc_822_text(), - MessageDataItemName::Rfc822 => { - if self.is_not_yet_seen() { - seen = SeenFlag::MustAdd; - } - self.rfc822() - } - MessageDataItemName::Envelope => Ok(self.envelope()), - MessageDataItemName::Body => self.body(), - MessageDataItemName::BodyStructure => self.body_structure(), - MessageDataItemName::BodyExt { - section, - partial, - peek, - } => { - let (body, has_seen) = self.body_ext(section, partial, peek)?; - seen = has_seen; - Ok(body) - } - MessageDataItemName::InternalDate => self.internal_date(), - MessageDataItemName::ModSeq => Ok(self.modseq()), - }) - .collect::<Result<Vec<_>, _>>()?; - - Ok(( - Body::Data(Data::Fetch { - seq: self.in_idx.i, - items: res_attrs.try_into()?, - }), - seen, - )) - } - - pub fn stored_naive_date(&self) -> Result<NaiveDate> { - let mail_meta = self.query_result.metadata().expect("metadata were fetched"); - let mail_ts: i64 = mail_meta.internaldate.try_into()?; - let msg_date: ChronoDateTime<Local> = ChronoDateTime::from_timestamp(mail_ts, 0) - .ok_or(anyhow!("unable to parse timestamp"))? - .with_timezone(&Local); - - Ok(msg_date.date_naive()) - } - - pub fn is_header_contains_pattern(&self, hdr: &[u8], pattern: &[u8]) -> bool { - let mime = match self.selected_mime() { - None => return false, - Some(x) => x, - }; - - let val = match mime.header_value(hdr) { - None => return false, - Some(x) => x, - }; - - val.windows(pattern.len()).any(|win| win == pattern) - } - - // Private function, mainly for filter! - fn uid(&self) -> MessageDataItem<'static> { - MessageDataItem::Uid(self.in_idx.uid.clone()) - } - - fn flags(&self) -> MessageDataItem<'static> { - MessageDataItem::Flags( - self.in_idx - .flags - .iter() - .filter_map(|f| flags::from_str(f)) - .collect(), - ) - } - - 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) -> 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>> { - let txt: NString = self.content.as_msg()?.raw_body.to_vec().try_into()?; - Ok(MessageDataItem::Rfc822Text(txt)) - } - - fn rfc822(&self) -> Result<MessageDataItem<'static>> { - let full: NString = self.content.as_msg()?.raw_part.to_vec().try_into()?; - Ok(MessageDataItem::Rfc822(full)) - } - - fn envelope(&self) -> MessageDataItem<'static> { - MessageDataItem::Envelope( - self.imf() - .expect("an imf object is derivable from fetchedmail") - .message_envelope(), - ) - } - - fn body(&self) -> Result<MessageDataItem<'static>> { - Ok(MessageDataItem::Body(mime_view::bodystructure( - self.content.as_msg()?.child.as_ref(), - false, - )?)) - } - - fn body_structure(&self) -> Result<MessageDataItem<'static>> { - Ok(MessageDataItem::BodyStructure(mime_view::bodystructure( - self.content.as_msg()?.child.as_ref(), - true, - )?)) - } - - fn is_not_yet_seen(&self) -> bool { - let seen_flag = Flag::Seen.to_string(); - !self.in_idx.flags.iter().any(|x| *x == seen_flag) - } - - /// maps to BODY[<section>]<<partial>> and BODY.PEEK[<section>]<<partial>> - /// peek does not implicitly set the \Seen flag - /// eg. BODY[HEADER.FIELDS (DATE FROM)] - /// eg. BODY[]<0.2048> - fn body_ext( - &self, - section: &Option<FetchSection<'static>>, - partial: &Option<(u32, NonZeroU32)>, - peek: &bool, - ) -> Result<(MessageDataItem<'static>, SeenFlag)> { - // Manage Seen flag - let mut seen = SeenFlag::DoNothing; - if !peek && self.is_not_yet_seen() { - // Add \Seen flag - //self.mailbox.add_flags(uuid, &[seen_flag]).await?; - seen = SeenFlag::MustAdd; - } - - // Process message - let (text, origin) = - match mime_view::body_ext(self.content.as_anypart()?, section, partial)? { - mime_view::BodySection::Full(body) => (body, None), - mime_view::BodySection::Slice { body, origin_octet } => (body, Some(origin_octet)), - }; - - let data: NString = text.to_vec().try_into()?; - - return Ok(( - MessageDataItem::BodyExt { - section: section.as_ref().map(|fs| fs.clone()), - origin, - data, - }, - seen, - )); - } - - fn internal_date(&self) -> Result<MessageDataItem<'static>> { - let dt = Utc - .fix() - .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))) - } - - fn modseq(&self) -> MessageDataItem<'static> { - MessageDataItem::ModSeq(self.in_idx.modseq) - } -} - -pub enum SeenFlag { - DoNothing, - MustAdd, -} - -// ------------------- - -pub enum FetchedMail<'a> { - IndexOnly, - Partial(AnyPart<'a>), - Full(AnyPart<'a>), -} -impl<'a> FetchedMail<'a> { - pub fn full_from_message(msg: Message<'a>) -> Self { - Self::Full(AnyPart::Msg(msg)) - } - - pub fn partial_from_message(msg: Message<'a>) -> Self { - Self::Partial(AnyPart::Msg(msg)) - } - - pub fn as_anypart(&self) -> Result<&AnyPart<'a>> { - match self { - FetchedMail::Full(x) => Ok(&x), - FetchedMail::Partial(x) => Ok(&x), - _ => bail!("The full message must be fetched, not only its headers"), - } - } - - pub fn as_msg(&self) -> Result<&Message<'a>> { - match self { - FetchedMail::Full(AnyPart::Msg(x)) => Ok(&x), - FetchedMail::Partial(AnyPart::Msg(x)) => Ok(&x), - _ => bail!("The full message must be fetched, not only its headers AND it must be an AnyPart::Msg."), - } - } - - pub fn as_imf(&self) -> Option<&imf::Imf<'a>> { - match self { - FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf), - FetchedMail::Partial(AnyPart::Msg(x)) => Some(&x.imf), - _ => None, - } - } -} diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs deleted file mode 100644 index 1c53b93..0000000 --- a/src/imap/mailbox_view.rs +++ /dev/null @@ -1,772 +0,0 @@ -use std::collections::HashSet; -use std::num::{NonZeroU32, NonZeroU64}; -use std::sync::Arc; - -use anyhow::{anyhow, Error, Result}; - -use futures::stream::{StreamExt, TryStreamExt}; - -use imap_codec::imap_types::core::{Charset, Vec1}; -use imap_codec::imap_types::fetch::MessageDataItem; -use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; -use imap_codec::imap_types::response::{Code, CodeOther, Data, Status}; -use imap_codec::imap_types::search::SearchKey; -use imap_codec::imap_types::sequence::SequenceSet; - -use crate::mail::mailbox::Mailbox; -use crate::mail::query::QueryScope; -use crate::mail::snapshot::FrozenMailbox; -use crate::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq}; -use crate::mail::unique_ident::UniqueIdent; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::flags; -use crate::imap::index::Index; -use crate::imap::mail_view::{MailView, SeenFlag}; -use crate::imap::response::Body; -use crate::imap::search; - -const DEFAULT_FLAGS: [Flag; 5] = [ - Flag::Seen, - Flag::Answered, - Flag::Flagged, - Flag::Deleted, - Flag::Draft, -]; - -pub struct UpdateParameters { - pub silence: HashSet<UniqueIdent>, - pub with_modseq: bool, - pub with_uid: bool, -} -impl Default for UpdateParameters { - fn default() -> Self { - Self { - silence: HashSet::new(), - with_modseq: false, - with_uid: false, - } - } -} - -/// A MailboxView is responsible for giving the client the information -/// it needs about a mailbox, such as an initial summary of the mailbox's -/// content and continuous updates indicating when the content -/// of the mailbox has been changed. -/// To do this, it keeps a variable `known_state` that corresponds to -/// what the client knows, and produces IMAP messages to be sent to the -/// client that go along updates to `known_state`. -pub struct MailboxView { - pub internal: FrozenMailbox, - pub is_condstore: bool, -} - -impl MailboxView { - /// Creates a new IMAP view into a mailbox. - pub async fn new(mailbox: Arc<Mailbox>, is_cond: bool) -> Self { - Self { - internal: mailbox.frozen().await, - is_condstore: is_cond, - } - } - - /// Create an updated view, useful to make a diff - /// between what the client knows and new stuff - /// Produces a set of IMAP responses describing the change between - /// what the client knows and what is actually in the mailbox. - /// This does NOT trigger a sync, it bases itself on what is currently - /// loaded in RAM by Bayou. - pub async fn update(&mut self, params: UpdateParameters) -> Result<Vec<Body<'static>>> { - let old_snapshot = self.internal.update().await; - let new_snapshot = &self.internal.snapshot; - - let mut data = Vec::<Body>::new(); - - // Calculate diff between two mailbox states - // See example in IMAP RFC in section on NOOP command: - // we want to produce something like this: - // C: a047 NOOP - // S: * 22 EXPUNGE - // S: * 23 EXISTS - // S: * 14 FETCH (UID 1305 FLAGS (\Seen \Deleted)) - // S: a047 OK Noop completed - // In other words: - // - notify client of expunged mails - // - if new mails arrived, notify client of number of existing mails - // - if flags changed for existing mails, tell client - // (for this last step: if uidvalidity changed, do nothing, - // just notify of new uidvalidity and they will resync) - - // - notify client of expunged mails - let mut n_expunge = 0; - for (i, (_uid, uuid)) in old_snapshot.idx_by_uid.iter().enumerate() { - if !new_snapshot.table.contains_key(uuid) { - data.push(Body::Data(Data::Expunge( - NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), - ))); - n_expunge += 1; - } - } - - // - if new mails arrived, notify client of number of existing mails - if new_snapshot.table.len() != old_snapshot.table.len() - n_expunge - || new_snapshot.uidvalidity != old_snapshot.uidvalidity - { - data.push(self.exists_status()?); - } - - if new_snapshot.uidvalidity != old_snapshot.uidvalidity { - // TODO: do we want to push less/more info than this? - data.push(self.uidvalidity_status()?); - data.push(self.uidnext_status()?); - } else { - // - if flags changed for existing mails, tell client - for (i, (_uid, uuid)) in new_snapshot.idx_by_uid.iter().enumerate() { - if params.silence.contains(uuid) { - continue; - } - - let old_mail = old_snapshot.table.get(uuid); - let new_mail = new_snapshot.table.get(uuid); - if old_mail.is_some() && old_mail != new_mail { - if let Some((uid, modseq, flags)) = new_mail { - let mut items = vec![MessageDataItem::Flags( - flags.iter().filter_map(|f| flags::from_str(f)).collect(), - )]; - - if params.with_uid { - items.push(MessageDataItem::Uid(*uid)); - } - - if params.with_modseq { - items.push(MessageDataItem::ModSeq(*modseq)); - } - - data.push(Body::Data(Data::Fetch { - seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), - items: items.try_into()?, - })); - } - } - } - } - Ok(data) - } - - /// Generates the necessary IMAP messages so that the client - /// has a satisfactory summary of the current mailbox's state. - /// These are the messages that are sent in response to a SELECT command. - pub fn summary(&self) -> Result<Vec<Body<'static>>> { - let mut data = Vec::<Body>::new(); - data.push(self.exists_status()?); - data.push(self.recent_status()?); - data.extend(self.flags_status()?.into_iter()); - data.push(self.uidvalidity_status()?); - data.push(self.uidnext_status()?); - if self.is_condstore { - data.push(self.highestmodseq_status()?); - } - /*self.unseen_first_status()? - .map(|unseen_status| data.push(unseen_status));*/ - - Ok(data) - } - - pub async fn store<'a>( - &mut self, - sequence_set: &SequenceSet, - kind: &StoreType, - response: &StoreResponse, - flags: &[Flag<'a>], - unchanged_since: Option<NonZeroU64>, - is_uid_store: &bool, - ) -> Result<(Vec<Body<'static>>, Vec<NonZeroU32>)> { - self.internal.sync().await?; - - let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); - - let idx = self.index()?; - let (editable, in_conflict) = - idx.fetch_unchanged_since(sequence_set, unchanged_since, *is_uid_store)?; - - for mi in editable.iter() { - match kind { - StoreType::Add => { - self.internal.mailbox.add_flags(mi.uuid, &flags[..]).await?; - } - StoreType::Remove => { - self.internal.mailbox.del_flags(mi.uuid, &flags[..]).await?; - } - StoreType::Replace => { - self.internal.mailbox.set_flags(mi.uuid, &flags[..]).await?; - } - } - } - - let silence = match response { - StoreResponse::Answer => HashSet::new(), - StoreResponse::Silent => editable.iter().map(|midx| midx.uuid).collect(), - }; - - let conflict_id_or_uid = match is_uid_store { - true => in_conflict.into_iter().map(|midx| midx.uid).collect(), - _ => in_conflict.into_iter().map(|midx| midx.i).collect(), - }; - - let summary = self - .update(UpdateParameters { - with_uid: *is_uid_store, - with_modseq: unchanged_since.is_some(), - silence, - }) - .await?; - - Ok((summary, conflict_id_or_uid)) - } - - pub async fn idle_sync(&mut self) -> Result<Vec<Body<'static>>> { - self.internal - .mailbox - .notify() - .await - .upgrade() - .ok_or(anyhow!("test"))? - .notified() - .await; - self.internal.mailbox.opportunistic_sync().await?; - self.update(UpdateParameters::default()).await - } - - pub async fn expunge( - &mut self, - maybe_seq_set: &Option<SequenceSet>, - ) -> Result<Vec<Body<'static>>> { - // Get a recent view to apply our change - self.internal.sync().await?; - let state = self.internal.peek().await; - let idx = Index::new(&state)?; - - // Build a default sequence set for the default case - use imap_codec::imap_types::sequence::{SeqOrUid, Sequence}; - let seq = match maybe_seq_set { - Some(s) => s.clone(), - None => SequenceSet( - vec![Sequence::Range( - SeqOrUid::Value(NonZeroU32::MIN), - SeqOrUid::Asterisk, - )] - .try_into() - .unwrap(), - ), - }; - - let deleted_flag = Flag::Deleted.to_string(); - let msgs = idx - .fetch_on_uid(&seq) - .into_iter() - .filter(|midx| midx.flags.iter().any(|x| *x == deleted_flag)) - .map(|midx| midx.uuid); - - for msg in msgs { - self.internal.mailbox.delete(msg).await?; - } - - self.update(UpdateParameters::default()).await - } - - pub async fn copy( - &self, - sequence_set: &SequenceSet, - to: Arc<Mailbox>, - is_uid_copy: &bool, - ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>)> { - let idx = self.index()?; - let mails = idx.fetch(sequence_set, *is_uid_copy)?; - - let mut new_uuids = vec![]; - for mi in mails.iter() { - new_uuids.push(to.copy_from(&self.internal.mailbox, mi.uuid).await?); - } - - let mut ret = vec![]; - let to_state = to.current_uid_index().await; - for (mi, new_uuid) in mails.iter().zip(new_uuids.iter()) { - let dest_uid = to_state - .table - .get(new_uuid) - .ok_or(anyhow!("copied mail not in destination mailbox"))? - .0; - ret.push((mi.uid, dest_uid)); - } - - Ok((to_state.uidvalidity, ret)) - } - - pub async fn r#move( - &mut self, - sequence_set: &SequenceSet, - to: Arc<Mailbox>, - is_uid_copy: &bool, - ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec<Body<'static>>)> { - let idx = self.index()?; - let mails = idx.fetch(sequence_set, *is_uid_copy)?; - - for mi in mails.iter() { - to.move_from(&self.internal.mailbox, mi.uuid).await?; - } - - let mut ret = vec![]; - let to_state = to.current_uid_index().await; - for mi in mails.iter() { - let dest_uid = to_state - .table - .get(&mi.uuid) - .ok_or(anyhow!("moved mail not in destination mailbox"))? - .0; - ret.push((mi.uid, dest_uid)); - } - - let update = self - .update(UpdateParameters { - with_uid: *is_uid_copy, - ..UpdateParameters::default() - }) - .await?; - - Ok((to_state.uidvalidity, ret, update)) - } - - /// Looks up state changes in the mailbox and produces a set of IMAP - /// responses describing the new state. - pub async fn fetch<'b>( - &self, - sequence_set: &SequenceSet, - ap: &AttributesProxy, - changed_since: Option<NonZeroU64>, - is_uid_fetch: &bool, - ) -> Result<Vec<Body<'static>>> { - // [1/6] Pre-compute data - // a. what are the uuids of the emails we want? - // b. do we need to fetch the full body? - //let ap = AttributesProxy::new(attributes, *is_uid_fetch); - let query_scope = match ap.need_body() { - true => QueryScope::Full, - _ => QueryScope::Partial, - }; - tracing::debug!("Query scope {:?}", query_scope); - let idx = self.index()?; - let mail_idx_list = idx.fetch_changed_since(sequence_set, changed_since, *is_uid_fetch)?; - - // [2/6] Fetch the emails - let uuids = mail_idx_list - .iter() - .map(|midx| midx.uuid) - .collect::<Vec<_>>(); - - let query = self.internal.query(&uuids, query_scope); - //let query_result = self.internal.query(&uuids, query_scope).fetch().await?; - - let query_stream = query - .fetch() - .zip(futures::stream::iter(mail_idx_list)) - // [3/6] Derive an IMAP-specific view from the results, apply the filters - .map(|(maybe_qr, midx)| match maybe_qr { - Ok(qr) => Ok((MailView::new(&qr, midx)?.filter(&ap)?, midx)), - Err(e) => Err(e), - }) - // [4/6] Apply the IMAP transformation - .then(|maybe_ret| async move { - let ((body, seen), midx) = maybe_ret?; - - // [5/6] Register the \Seen flags - if matches!(seen, SeenFlag::MustAdd) { - let seen_flag = Flag::Seen.to_string(); - self.internal - .mailbox - .add_flags(midx.uuid, &[seen_flag]) - .await?; - } - - Ok::<_, anyhow::Error>(body) - }); - - // [6/6] Build the final result that will be sent to the client. - query_stream.try_collect().await - } - - /// A naive search implementation... - pub async fn search<'a>( - &self, - _charset: &Option<Charset<'a>>, - search_key: &SearchKey<'a>, - uid: bool, - ) -> Result<(Vec<Body<'static>>, bool)> { - // 1. Compute the subset of sequence identifiers we need to fetch - // based on the search query - let crit = search::Criteria(search_key); - let (seq_set, seq_type) = crit.to_sequence_set(); - - // 2. Get the selection - let idx = self.index()?; - let selection = idx.fetch(&seq_set, seq_type.is_uid())?; - - // 3. Filter the selection based on the ID / UID / Flags - let (kept_idx, to_fetch) = crit.filter_on_idx(&selection); - - // 4.a Fetch additional info about the emails - let query_scope = crit.query_scope(); - let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::<Vec<_>>(); - let query = self.internal.query(&uuids, query_scope); - - // 4.b We don't want to keep all data in memory, so we do the computing in a stream - let query_stream = query - .fetch() - .zip(futures::stream::iter(&to_fetch)) - // 5.a Build a mailview with the body, might fail with an error - // 5.b If needed, filter the selection based on the body, but keep the errors - // 6. Drop the query+mailbox, keep only the mail index - // Here we release a lot of memory, this is the most important part ^^ - .filter_map(|(maybe_qr, midx)| { - let r = match maybe_qr { - Ok(qr) => match MailView::new(&qr, midx).map(|mv| crit.is_keep_on_query(&mv)) { - Ok(true) => Some(Ok(*midx)), - Ok(_) => None, - Err(e) => Some(Err(e)), - }, - Err(e) => Some(Err(e)), - }; - futures::future::ready(r) - }); - - // 7. Chain both streams (part resolved from index, part resolved from metadata+body) - let main_stream = futures::stream::iter(kept_idx) - .map(Ok) - .chain(query_stream) - .map_ok(|idx| match uid { - true => (idx.uid, idx.modseq), - _ => (idx.i, idx.modseq), - }); - - // 8. Do the actual computation - let internal_result: Vec<_> = main_stream.try_collect().await?; - let (selection, modseqs): (Vec<_>, Vec<_>) = internal_result.into_iter().unzip(); - - // 9. Aggregate the maximum modseq value - let maybe_modseq = match crit.is_modseq() { - true => modseqs.into_iter().max(), - _ => None, - }; - - // 10. Return the final result - Ok(( - vec![Body::Data(Data::Search(selection, maybe_modseq))], - maybe_modseq.is_some(), - )) - } - - // ---- - /// @FIXME index should be stored for longer than a single request - /// Instead they should be tied to the FrozenMailbox refresh - /// It's not trivial to refactor the code to do that, so we are doing - /// some useless computation for now... - fn index<'a>(&'a self) -> Result<Index<'a>> { - Index::new(&self.internal.snapshot) - } - - /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` - fn uidvalidity_status(&self) -> Result<Body<'static>> { - let uid_validity = Status::ok( - None, - Some(Code::UidValidity(self.uidvalidity())), - "UIDs valid", - ) - .map_err(Error::msg)?; - Ok(Body::Status(uid_validity)) - } - - pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { - self.internal.snapshot.uidvalidity - } - - /// Produce an OK [UIDNEXT _] message corresponding to `known_state` - fn uidnext_status(&self) -> Result<Body<'static>> { - let next_uid = Status::ok( - None, - Some(Code::UidNext(self.uidnext())), - "Predict next UID", - ) - .map_err(Error::msg)?; - Ok(Body::Status(next_uid)) - } - - pub(crate) fn uidnext(&self) -> ImapUid { - self.internal.snapshot.uidnext - } - - pub(crate) fn highestmodseq_status(&self) -> Result<Body<'static>> { - Ok(Body::Status(Status::ok( - None, - Some(Code::Other(CodeOther::unvalidated( - format!("HIGHESTMODSEQ {}", self.highestmodseq()).into_bytes(), - ))), - "Highest", - )?)) - } - - pub(crate) fn highestmodseq(&self) -> ModSeq { - self.internal.snapshot.highestmodseq - } - - /// Produce an EXISTS message corresponding to the number of mails - /// in `known_state` - fn exists_status(&self) -> Result<Body<'static>> { - Ok(Body::Data(Data::Exists(self.exists()?))) - } - - pub(crate) fn exists(&self) -> Result<u32> { - Ok(u32::try_from(self.internal.snapshot.idx_by_uid.len())?) - } - - /// Produce a RECENT message corresponding to the number of - /// recent mails in `known_state` - fn recent_status(&self) -> Result<Body<'static>> { - Ok(Body::Data(Data::Recent(self.recent()?))) - } - - #[allow(dead_code)] - fn unseen_first_status(&self) -> Result<Option<Body<'static>>> { - Ok(self - .unseen_first()? - .map(|unseen_id| { - Status::ok(None, Some(Code::Unseen(unseen_id)), "First unseen.").map(Body::Status) - }) - .transpose()?) - } - - #[allow(dead_code)] - fn unseen_first(&self) -> Result<Option<NonZeroU32>> { - Ok(self - .internal - .snapshot - .table - .values() - .enumerate() - .find(|(_i, (_imap_uid, _modseq, flags))| !flags.contains(&"\\Seen".to_string())) - .map(|(i, _)| NonZeroU32::try_from(i as u32 + 1)) - .transpose()?) - } - - pub(crate) fn recent(&self) -> Result<u32> { - let recent = self - .internal - .snapshot - .idx_by_flag - .get(&"\\Recent".to_string()) - .map(|os| os.len()) - .unwrap_or(0); - Ok(u32::try_from(recent)?) - } - - /// Produce a FLAGS and a PERMANENTFLAGS message that indicates - /// the flags that are in `known_state` + default flags - fn flags_status(&self) -> Result<Vec<Body<'static>>> { - let mut body = vec![]; - - // 1. Collecting all the possible flags in the mailbox - // 1.a Fetch them from our index - let mut known_flags: Vec<Flag> = self - .internal - .snapshot - .idx_by_flag - .flags() - .filter_map(|f| match flags::from_str(f) { - Some(FlagFetch::Flag(fl)) => Some(fl), - _ => None, - }) - .collect(); - // 1.b Merge it with our default flags list - for f in DEFAULT_FLAGS.iter() { - if !known_flags.contains(f) { - known_flags.push(f.clone()); - } - } - // 1.c Create the IMAP message - body.push(Body::Data(Data::Flags(known_flags.clone()))); - - // 2. Returning flags that are persisted - // 2.a Always advertise our default flags - let mut permanent = DEFAULT_FLAGS - .iter() - .map(|f| FlagPerm::Flag(f.clone())) - .collect::<Vec<_>>(); - // 2.b Say that we support any keyword flag - permanent.push(FlagPerm::Asterisk); - // 2.c Create the IMAP message - let permanent_flags = Status::ok( - None, - Some(Code::PermanentFlags(permanent)), - "Flags permitted", - ) - .map_err(Error::msg)?; - body.push(Body::Status(permanent_flags)); - - // Done! - Ok(body) - } - - pub(crate) fn unseen_count(&self) -> usize { - let total = self.internal.snapshot.table.len(); - let seen = self - .internal - .snapshot - .idx_by_flag - .get(&Flag::Seen.to_string()) - .map(|x| x.len()) - .unwrap_or(0); - total - seen - } -} - -#[cfg(test)] -mod tests { - use super::*; - use imap_codec::encode::Encoder; - use imap_codec::imap_types::core::Vec1; - use imap_codec::imap_types::fetch::Section; - use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName}; - use imap_codec::imap_types::response::Response; - use imap_codec::ResponseCodec; - use std::fs; - - use crate::cryptoblob; - use crate::imap::index::MailIndex; - use crate::imap::mail_view::MailView; - use crate::imap::mime_view; - use crate::mail::mailbox::MailMeta; - use crate::mail::query::QueryResult; - use crate::mail::unique_ident; - - #[test] - fn mailview_body_ext() -> Result<()> { - let ap = AttributesProxy::new( - &MacroOrMessageDataItemNames::MessageDataItemNames(vec![ - MessageDataItemName::BodyExt { - section: Some(Section::Header(None)), - partial: None, - peek: false, - }, - ]), - &[], - false, - ); - - let key = cryptoblob::gen_key(); - let meta = MailMeta { - internaldate: 0u64, - headers: vec![], - message_key: key, - rfc822_size: 8usize, - }; - - let index_entry = (NonZeroU32::MIN, NonZeroU64::MIN, vec![]); - let mail_in_idx = MailIndex { - i: NonZeroU32::MIN, - uid: index_entry.0, - modseq: index_entry.1, - uuid: unique_ident::gen_ident(), - flags: &index_entry.2, - }; - let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; - let qr = QueryResult::FullResult { - uuid: mail_in_idx.uuid.clone(), - metadata: meta, - content: rfc822.to_vec(), - }; - - let mv = MailView::new(&qr, &mail_in_idx)?; - let (res_body, _seen) = mv.filter(&ap)?; - - let fattr = match res_body { - Body::Data(Data::Fetch { - seq: _seq, - items: attr, - }) => Ok(attr), - _ => Err(anyhow!("Not a fetch body")), - }?; - - assert_eq!(fattr.as_ref().len(), 1); - - let (sec, _orig, _data) = match &fattr.as_ref()[0] { - MessageDataItem::BodyExt { - section, - origin, - data, - } => Ok((section, origin, data)), - _ => Err(anyhow!("not a body ext message attribute")), - }?; - - assert_eq!(sec.as_ref().unwrap(), &Section::Header(None)); - - Ok(()) - } - - /// Future automated test. We use lossy utf8 conversion + lowercase everything, - /// so this test might allow invalid results. But at least it allows us to quickly test a - /// large variety of emails. - /// Keep in mind that special cases must still be tested manually! - #[test] - fn fetch_body() -> Result<()> { - let prefixes = [ - /* *** MY OWN DATASET *** */ - "tests/emails/dxflrs/0001_simple", - "tests/emails/dxflrs/0002_mime", - "tests/emails/dxflrs/0003_mime-in-mime", - "tests/emails/dxflrs/0004_msg-in-msg", - // eml_codec do not support continuation for the moment - //"tests/emails/dxflrs/0005_mail-parser-readme", - "tests/emails/dxflrs/0006_single-mime", - "tests/emails/dxflrs/0007_raw_msg_in_rfc822", - /* *** (STRANGE) RFC *** */ - //"tests/emails/rfc/000", // must return text/enriched, we return text/plain - //"tests/emails/rfc/001", // does not recognize the multipart/external-body, breaks the - // whole parsing - //"tests/emails/rfc/002", // wrong date in email - - //"tests/emails/rfc/003", // dovecot fixes \r\r: the bytes number is wrong + text/enriched - - /* *** THIRD PARTY *** */ - //"tests/emails/thirdparty/000", // dovecot fixes \r\r: the bytes number is wrong - //"tests/emails/thirdparty/001", // same - "tests/emails/thirdparty/002", // same - - /* *** LEGACY *** */ - //"tests/emails/legacy/000", // same issue with \r\r - ]; - - for pref in prefixes.iter() { - println!("{}", pref); - let txt = fs::read(format!("{}.eml", pref))?; - let oracle = fs::read(format!("{}.dovecot.body", pref))?; - let message = eml_codec::parse_message(&txt).unwrap().1; - - let test_repr = Response::Data(Data::Fetch { - seq: NonZeroU32::new(1).unwrap(), - items: Vec1::from(MessageDataItem::Body(mime_view::bodystructure( - &message.child, - false, - )?)), - }); - let test_bytes = ResponseCodec::new().encode(&test_repr).dump(); - let test_str = String::from_utf8_lossy(&test_bytes).to_lowercase(); - - let oracle_str = - format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).to_lowercase(); - - println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str); - //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); - assert_eq!(test_str, oracle_str); - } - - Ok(()) - } -} diff --git a/src/imap/mime_view.rs b/src/imap/mime_view.rs deleted file mode 100644 index 8bbbd2d..0000000 --- a/src/imap/mime_view.rs +++ /dev/null @@ -1,580 +0,0 @@ -use std::borrow::Cow; -use std::collections::HashSet; -use std::num::NonZeroU32; - -use anyhow::{anyhow, bail, Result}; - -use imap_codec::imap_types::body::{ - BasicFields, Body as FetchBody, BodyStructure, MultiPartExtensionData, SinglePartExtensionData, - SpecificFields, -}; -use imap_codec::imap_types::core::{AString, IString, NString, Vec1}; -use imap_codec::imap_types::fetch::{Part as FetchPart, Section as FetchSection}; - -use eml_codec::{ - header, mime, mime::r#type::Deductible, part::composite, part::discrete, part::AnyPart, -}; - -use crate::imap::imf_view::ImfView; - -pub enum BodySection<'a> { - Full(Cow<'a, [u8]>), - Slice { - body: Cow<'a, [u8]>, - origin_octet: u32, - }, -} - -/// Logic for BODY[<section>]<<partial>> -/// Works in 3 times: -/// 1. Find the section (RootMime::subset) -/// 2. Apply the extraction logic (SelectedMime::extract), like TEXT, HEADERS, etc. -/// 3. Keep only the given subset provided by partial -/// -/// Example of message sections: -/// -/// ``` -/// HEADER ([RFC-2822] header of the message) -/// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 1 TEXT/PLAIN -/// 2 APPLICATION/OCTET-STREAM -/// 3 MESSAGE/RFC822 -/// 3.HEADER ([RFC-2822] header of the message) -/// 3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 3.1 TEXT/PLAIN -/// 3.2 APPLICATION/OCTET-STREAM -/// 4 MULTIPART/MIXED -/// 4.1 IMAGE/GIF -/// 4.1.MIME ([MIME-IMB] header for the IMAGE/GIF) -/// 4.2 MESSAGE/RFC822 -/// 4.2.HEADER ([RFC-2822] header of the message) -/// 4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 4.2.1 TEXT/PLAIN -/// 4.2.2 MULTIPART/ALTERNATIVE -/// 4.2.2.1 TEXT/PLAIN -/// 4.2.2.2 TEXT/RICHTEXT -/// ``` -pub fn body_ext<'a>( - part: &'a AnyPart<'a>, - section: &'a Option<FetchSection<'a>>, - partial: &'a Option<(u32, NonZeroU32)>, -) -> Result<BodySection<'a>> { - let root_mime = NodeMime(part); - let (extractor, path) = SubsettedSection::from(section); - let selected_mime = root_mime.subset(path)?; - let extracted_full = selected_mime.extract(&extractor)?; - Ok(extracted_full.to_body_section(partial)) -} - -/// Logic for BODY and BODYSTRUCTURE -/// -/// ```raw -/// b fetch 29878:29879 (BODY) -/// * 29878 FETCH (BODY (("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 3264 82)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 31834 643) "alternative")) -/// * 29879 FETCH (BODY ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 4107 131)) -/// ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^^^ ^^^ -/// | | | | | | number of lines -/// | | | | | size -/// | | | | content transfer encoding -/// | | | description -/// | | id -/// | parameter list -/// b OK Fetch completed (0.001 + 0.000 secs). -/// ``` -pub fn bodystructure(part: &AnyPart, is_ext: bool) -> Result<BodyStructure<'static>> { - NodeMime(part).structure(is_ext) -} - -/// NodeMime -/// -/// Used for recursive logic on MIME. -/// See SelectedMime for inspection. -struct NodeMime<'a>(&'a AnyPart<'a>); -impl<'a> NodeMime<'a> { - /// A MIME object is a tree of elements. - /// The path indicates which element must be picked. - /// This function returns the picked element as the new view - fn subset(self, path: Option<&'a FetchPart>) -> Result<SelectedMime<'a>> { - match path { - None => Ok(SelectedMime(self.0)), - Some(v) => self.rec_subset(v.0.as_ref()), - } - } - - fn rec_subset(self, path: &'a [NonZeroU32]) -> Result<SelectedMime> { - if path.is_empty() { - Ok(SelectedMime(self.0)) - } else { - match self.0 { - AnyPart::Mult(x) => { - let next = Self(x.children - .get(path[0].get() as usize - 1) - .ok_or(anyhow!("Unable to resolve subpath {:?}, current multipart has only {} elements", path, x.children.len()))?); - next.rec_subset(&path[1..]) - }, - AnyPart::Msg(x) => { - let next = Self(x.child.as_ref()); - next.rec_subset(path) - }, - _ => bail!("You tried to access a subpart on an atomic part (text or binary). Unresolved subpath {:?}", path), - } - } - } - - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - match self.0 { - AnyPart::Txt(x) => NodeTxt(self, x).structure(is_ext), - AnyPart::Bin(x) => NodeBin(self, x).structure(is_ext), - AnyPart::Mult(x) => NodeMult(self, x).structure(is_ext), - AnyPart::Msg(x) => NodeMsg(self, x).structure(is_ext), - } - } -} - -//---------------------------------------------------------- - -/// A FetchSection must be handled in 2 times: -/// - First we must extract the MIME part -/// - Then we must process it as desired -/// The given struct mixes both work, so -/// we separate this work here. -enum SubsettedSection<'a> { - Part, - Header, - HeaderFields(&'a Vec1<AString<'a>>), - HeaderFieldsNot(&'a Vec1<AString<'a>>), - Text, - Mime, -} -impl<'a> SubsettedSection<'a> { - fn from(section: &'a Option<FetchSection>) -> (Self, Option<&'a FetchPart>) { - match section { - Some(FetchSection::Text(maybe_part)) => (Self::Text, maybe_part.as_ref()), - Some(FetchSection::Header(maybe_part)) => (Self::Header, maybe_part.as_ref()), - Some(FetchSection::HeaderFields(maybe_part, fields)) => { - (Self::HeaderFields(fields), maybe_part.as_ref()) - } - Some(FetchSection::HeaderFieldsNot(maybe_part, fields)) => { - (Self::HeaderFieldsNot(fields), maybe_part.as_ref()) - } - Some(FetchSection::Mime(part)) => (Self::Mime, Some(part)), - Some(FetchSection::Part(part)) => (Self::Part, Some(part)), - None => (Self::Part, None), - } - } -} - -/// Used for current MIME inspection -/// -/// See NodeMime for recursive logic -pub struct SelectedMime<'a>(pub &'a AnyPart<'a>); -impl<'a> SelectedMime<'a> { - pub fn header_value(&'a self, to_match_ext: &[u8]) -> Option<&'a [u8]> { - let to_match = to_match_ext.to_ascii_lowercase(); - - self.eml_mime() - .kv - .iter() - .filter_map(|field| match field { - header::Field::Good(header::Kv2(k, v)) => Some((k, v)), - _ => None, - }) - .find(|(k, _)| k.to_ascii_lowercase() == to_match) - .map(|(_, v)| v) - .copied() - } - - /// The subsetted fetch section basically tells us the - /// extraction logic to apply on our selected MIME. - /// This function acts as a router for these logic. - fn extract(&self, extractor: &SubsettedSection<'a>) -> Result<ExtractedFull<'a>> { - match extractor { - SubsettedSection::Text => self.text(), - SubsettedSection::Header => self.header(), - SubsettedSection::HeaderFields(fields) => self.header_fields(fields, false), - SubsettedSection::HeaderFieldsNot(fields) => self.header_fields(fields, true), - SubsettedSection::Part => self.part(), - SubsettedSection::Mime => self.mime(), - } - } - - fn mime(&self) -> Result<ExtractedFull<'a>> { - let bytes = match &self.0 { - AnyPart::Txt(p) => p.mime.fields.raw, - AnyPart::Bin(p) => p.mime.fields.raw, - AnyPart::Msg(p) => p.child.mime().raw, - AnyPart::Mult(p) => p.mime.fields.raw, - }; - Ok(ExtractedFull(bytes.into())) - } - - fn part(&self) -> Result<ExtractedFull<'a>> { - let bytes = match &self.0 { - AnyPart::Txt(p) => p.body, - AnyPart::Bin(p) => p.body, - AnyPart::Msg(p) => p.raw_part, - AnyPart::Mult(_) => bail!("Multipart part has no body"), - }; - Ok(ExtractedFull(bytes.to_vec().into())) - } - - fn eml_mime(&self) -> &eml_codec::mime::NaiveMIME<'_> { - match &self.0 { - AnyPart::Msg(msg) => msg.child.mime(), - other => other.mime(), - } - } - - /// The [...] HEADER.FIELDS, and HEADER.FIELDS.NOT part - /// specifiers refer to the [RFC-2822] header of the message or of - /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. - /// HEADER.FIELDS and HEADER.FIELDS.NOT are followed by a list of - /// field-name (as defined in [RFC-2822]) names, and return a - /// subset of the header. The subset returned by HEADER.FIELDS - /// contains only those header fields with a field-name that - /// matches one of the names in the list; similarly, the subset - /// returned by HEADER.FIELDS.NOT contains only the header fields - /// with a non-matching field-name. The field-matching is - /// case-insensitive but otherwise exact. - fn header_fields( - &self, - fields: &'a Vec1<AString<'a>>, - invert: bool, - ) -> Result<ExtractedFull<'a>> { - // Build a lowercase ascii hashset with the fields to fetch - let index = fields - .as_ref() - .iter() - .map(|x| { - match x { - AString::Atom(a) => a.inner().as_bytes(), - AString::String(IString::Literal(l)) => l.as_ref(), - AString::String(IString::Quoted(q)) => q.inner().as_bytes(), - } - .to_ascii_lowercase() - }) - .collect::<HashSet<_>>(); - - // Extract MIME headers - let mime = self.eml_mime(); - - // Filter our MIME headers based on the field index - // 1. Keep only the correctly formatted headers - // 2. Keep only based on the index presence or absence - // 3. Reduce as a byte vector - let buffer = mime - .kv - .iter() - .filter_map(|field| match field { - header::Field::Good(header::Kv2(k, v)) => Some((k, v)), - _ => None, - }) - .filter(|(k, _)| index.contains(&k.to_ascii_lowercase()) ^ invert) - .fold(vec![], |mut acc, (k, v)| { - acc.extend(*k); - acc.extend(b": "); - acc.extend(*v); - acc.extend(b"\r\n"); - acc - }); - - Ok(ExtractedFull(buffer.into())) - } - - /// The HEADER [...] part specifiers refer to the [RFC-2822] header of the message or of - /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. - /// ```raw - /// HEADER ([RFC-2822] header of the message) - /// ``` - fn header(&self) -> Result<ExtractedFull<'a>> { - let msg = self - .0 - .as_message() - .ok_or(anyhow!("Selected part must be a message/rfc822"))?; - Ok(ExtractedFull(msg.raw_headers.into())) - } - - /// The TEXT part specifier refers to the text body of the message, omitting the [RFC-2822] header. - fn text(&self) -> Result<ExtractedFull<'a>> { - let msg = self - .0 - .as_message() - .ok_or(anyhow!("Selected part must be a message/rfc822"))?; - Ok(ExtractedFull(msg.raw_body.into())) - } - - // ------------ - - /// Basic field of a MIME part that is - /// common to all parts - fn basic_fields(&self) -> Result<BasicFields<'static>> { - let sz = match self.0 { - AnyPart::Txt(x) => x.body.len(), - AnyPart::Bin(x) => x.body.len(), - AnyPart::Msg(x) => x.raw_part.len(), - AnyPart::Mult(_) => 0, - }; - let m = self.0.mime(); - let parameter_list = m - .ctype - .as_ref() - .map(|x| { - x.params - .iter() - .map(|p| { - ( - IString::try_from(String::from_utf8_lossy(p.name).to_string()), - IString::try_from(p.value.to_string()), - ) - }) - .filter(|(k, v)| k.is_ok() && v.is_ok()) - .map(|(k, v)| (k.unwrap(), v.unwrap())) - .collect() - }) - .unwrap_or(vec![]); - - Ok(BasicFields { - parameter_list, - id: NString( - m.id.as_ref() - .and_then(|ci| IString::try_from(ci.to_string()).ok()), - ), - description: NString( - m.description - .as_ref() - .and_then(|cd| IString::try_from(cd.to_string()).ok()), - ), - content_transfer_encoding: match m.transfer_encoding { - mime::mechanism::Mechanism::_8Bit => unchecked_istring("8bit"), - mime::mechanism::Mechanism::Binary => unchecked_istring("binary"), - mime::mechanism::Mechanism::QuotedPrintable => { - unchecked_istring("quoted-printable") - } - mime::mechanism::Mechanism::Base64 => unchecked_istring("base64"), - _ => unchecked_istring("7bit"), - }, - // @FIXME we can't compute the size of the message currently... - size: u32::try_from(sz)?, - }) - } -} - -// --------------------------- -struct NodeMsg<'a>(&'a NodeMime<'a>, &'a composite::Message<'a>); -impl<'a> NodeMsg<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let basic = SelectedMime(self.0 .0).basic_fields()?; - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Message { - envelope: Box::new(ImfView(&self.1.imf).message_envelope()), - body_structure: Box::new(NodeMime(&self.1.child).structure(is_ext)?), - number_of_lines: nol(self.1.raw_part), - }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} -struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>); -impl<'a> NodeMult<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let itype = &self.1.mime.interpreted_type; - let subtype = IString::try_from(itype.subtype.to_string()) - .unwrap_or(unchecked_istring("alternative")); - - let inner_bodies = self - .1 - .children - .iter() - .filter_map(|inner| NodeMime(&inner).structure(is_ext).ok()) - .collect::<Vec<_>>(); - - Vec1::validate(&inner_bodies)?; - let bodies = Vec1::unvalidated(inner_bodies); - - Ok(BodyStructure::Multi { - bodies, - subtype, - extension_data: match is_ext { - true => Some(MultiPartExtensionData { - parameter_list: vec![( - IString::try_from("boundary").unwrap(), - IString::try_from(self.1.mime.interpreted_type.boundary.to_string())?, - )], - tail: None, - }), - _ => None, - }, - }) - } -} -struct NodeTxt<'a>(&'a NodeMime<'a>, &'a discrete::Text<'a>); -impl<'a> NodeTxt<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let mut basic = SelectedMime(self.0 .0).basic_fields()?; - - // Get the interpreted content type, set it - let itype = match &self.1.mime.interpreted_type { - Deductible::Inferred(v) | Deductible::Explicit(v) => v, - }; - let subtype = - IString::try_from(itype.subtype.to_string()).unwrap_or(unchecked_istring("plain")); - - // Add charset to the list of parameters if we know it has been inferred as it will be - // missing from the parsed content. - if let Deductible::Inferred(charset) = &itype.charset { - basic.parameter_list.push(( - unchecked_istring("charset"), - IString::try_from(charset.to_string()).unwrap_or(unchecked_istring("us-ascii")), - )); - } - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Text { - subtype, - number_of_lines: nol(self.1.body), - }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} - -struct NodeBin<'a>(&'a NodeMime<'a>, &'a discrete::Binary<'a>); -impl<'a> NodeBin<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let basic = SelectedMime(self.0 .0).basic_fields()?; - - let default = mime::r#type::NaiveType { - main: &b"application"[..], - sub: &b"octet-stream"[..], - params: vec![], - }; - let ct = self.1.mime.fields.ctype.as_ref().unwrap_or(&default); - - let r#type = IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err( - anyhow!("Unable to build IString from given Content-Type type given"), - ))?; - - let subtype = IString::try_from(String::from_utf8_lossy(ct.sub).to_string()).or(Err( - anyhow!("Unable to build IString from given Content-Type subtype given"), - ))?; - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Basic { r#type, subtype }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} - -// --------------------------- - -struct ExtractedFull<'a>(Cow<'a, [u8]>); -impl<'a> ExtractedFull<'a> { - /// It is possible to fetch a substring of the designated text. - /// This is done by appending an open angle bracket ("<"), the - /// octet position of the first desired octet, a period, the - /// maximum number of octets desired, and a close angle bracket - /// (">") to the part specifier. If the starting octet is beyond - /// the end of the text, an empty string is returned. - /// - /// Any partial fetch that attempts to read beyond the end of the - /// text is truncated as appropriate. A partial fetch that starts - /// at octet 0 is returned as a partial fetch, even if this - /// truncation happened. - /// - /// Note: This means that BODY[]<0.2048> of a 1500-octet message - /// will return BODY[]<0> with a literal of size 1500, not - /// BODY[]. - /// - /// Note: A substring fetch of a HEADER.FIELDS or - /// HEADER.FIELDS.NOT part specifier is calculated after - /// subsetting the header. - fn to_body_section(self, partial: &'_ Option<(u32, NonZeroU32)>) -> BodySection<'a> { - match partial { - Some((begin, len)) => self.partialize(*begin, *len), - None => BodySection::Full(self.0), - } - } - - fn partialize(self, begin: u32, len: NonZeroU32) -> BodySection<'a> { - // Asked range is starting after the end of the content, - // returning an empty buffer - if begin as usize > self.0.len() { - return BodySection::Slice { - body: Cow::Borrowed(&[][..]), - origin_octet: begin, - }; - } - - // Asked range is ending after the end of the content, - // slice only the beginning of the buffer - if (begin + len.get()) as usize >= self.0.len() { - return BodySection::Slice { - body: match self.0 { - Cow::Borrowed(body) => Cow::Borrowed(&body[begin as usize..]), - Cow::Owned(body) => Cow::Owned(body[begin as usize..].to_vec()), - }, - origin_octet: begin, - }; - } - - // Range is included inside the considered content, - // this is the "happy case" - BodySection::Slice { - body: match self.0 { - Cow::Borrowed(body) => { - Cow::Borrowed(&body[begin as usize..(begin + len.get()) as usize]) - } - Cow::Owned(body) => { - Cow::Owned(body[begin as usize..(begin + len.get()) as usize].to_vec()) - } - }, - origin_octet: begin, - } - } -} - -/// ---- LEGACY - -/// s is set to static to ensure that only compile time values -/// checked by developpers are passed. -fn unchecked_istring(s: &'static str) -> IString { - IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") -} - -// Number Of Lines -fn nol(input: &[u8]) -> u32 { - input - .iter() - .filter(|x| **x == b'\n') - .count() - .try_into() - .unwrap_or(0) -} diff --git a/src/imap/mod.rs b/src/imap/mod.rs deleted file mode 100644 index 02ab9ce..0000000 --- a/src/imap/mod.rs +++ /dev/null @@ -1,421 +0,0 @@ -mod attributes; -mod capability; -mod command; -mod flags; -mod flow; -mod imf_view; -mod index; -mod mail_view; -mod mailbox_view; -mod mime_view; -mod request; -mod response; -mod search; -mod session; - -use std::net::SocketAddr; - -use anyhow::{anyhow, bail, Context, Result}; -use futures::stream::{FuturesUnordered, StreamExt}; - -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio::sync::watch; - -use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Response, Status}; -use imap_codec::imap_types::{core::Text, response::Greeting}; -use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; -use imap_flow::stream::AnyStream; -use rustls_pemfile::{certs, private_key}; -use tokio_rustls::TlsAcceptor; - -use crate::config::{ImapConfig, ImapUnsecureConfig}; -use crate::imap::capability::ServerCapability; -use crate::imap::request::Request; -use crate::imap::response::{Body, ResponseOrIdle}; -use crate::imap::session::Instance; -use crate::login::ArcLoginProvider; - -/// Server is a thin wrapper to register our Services in BÃ L -pub struct Server { - bind_addr: SocketAddr, - login_provider: ArcLoginProvider, - capabilities: ServerCapability, - tls: Option<TlsAcceptor>, -} - -#[derive(Clone)] -struct ClientContext { - addr: SocketAddr, - login_provider: ArcLoginProvider, - must_exit: watch::Receiver<bool>, - server_capabilities: ServerCapability, -} - -pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Result<Server> { - let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open( - config.certs, - )?)) - .collect::<Result<Vec<_>, _>>()?; - let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open( - config.key, - )?))? - .unwrap(); - - let tls_config = rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(loaded_certs, loaded_key)?; - let acceptor = TlsAcceptor::from(Arc::new(tls_config)); - - Ok(Server { - bind_addr: config.bind_addr, - login_provider: login, - capabilities: ServerCapability::default(), - tls: Some(acceptor), - }) -} - -pub fn new_unsecure(config: ImapUnsecureConfig, login: ArcLoginProvider) -> Server { - Server { - bind_addr: config.bind_addr, - login_provider: login, - capabilities: ServerCapability::default(), - tls: None, - } -} - -impl Server { - pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - tracing::info!("IMAP server listening on {:#}", self.bind_addr); - - let mut connections = FuturesUnordered::new(); - - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - let (socket, remote_addr) = tokio::select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - tracing::info!("IMAP: accepted connection from {}", remote_addr); - let stream = match self.tls.clone() { - Some(acceptor) => { - let stream = match acceptor.accept(socket).await { - Ok(v) => v, - Err(e) => { - tracing::error!(err=?e, "TLS negociation failed"); - continue; - } - }; - AnyStream::new(stream) - } - None => AnyStream::new(socket), - }; - - let client = ClientContext { - addr: remote_addr.clone(), - login_provider: self.login_provider.clone(), - must_exit: must_exit.clone(), - server_capabilities: self.capabilities.clone(), - }; - let conn = tokio::spawn(NetLoop::handler(client, stream)); - connections.push(conn); - } - drop(tcp); - - tracing::info!("IMAP server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -use std::sync::Arc; -use tokio::sync::mpsc::*; -use tokio::sync::Notify; -use tokio_util::bytes::BytesMut; - -const PIPELINABLE_COMMANDS: usize = 64; - -// @FIXME a full refactor of this part of the code will be needed sooner or later -struct NetLoop { - ctx: ClientContext, - server: ServerFlow, - cmd_tx: Sender<Request>, - resp_rx: UnboundedReceiver<ResponseOrIdle>, -} - -impl NetLoop { - async fn handler(ctx: ClientContext, sock: AnyStream) { - let addr = ctx.addr.clone(); - - let mut nl = match Self::new(ctx, sock).await { - Ok(nl) => { - tracing::debug!(addr=?addr, "netloop successfully initialized"); - nl - } - Err(e) => { - tracing::error!(addr=?addr, err=?e, "netloop can not be initialized, closing session"); - return; - } - }; - - match nl.core().await { - Ok(()) => { - tracing::debug!("closing successful netloop core for {:?}", addr); - } - Err(e) => { - tracing::error!("closing errored netloop core for {:?}: {}", addr, e); - } - } - } - - async fn new(ctx: ClientContext, sock: AnyStream) -> Result<Self> { - let mut opts = ServerFlowOptions::default(); - opts.crlf_relaxed = false; - opts.literal_accept_text = Text::unvalidated("OK"); - opts.literal_reject_text = Text::unvalidated("Literal rejected"); - - // Send greeting - let (server, _) = ServerFlow::send_greeting( - sock, - opts, - Greeting::ok( - Some(Code::Capability(ctx.server_capabilities.to_vec())), - "Aerogramme", - ) - .unwrap(), - ) - .await?; - - // Start a mailbox session in background - let (cmd_tx, cmd_rx) = mpsc::channel::<Request>(PIPELINABLE_COMMANDS); - let (resp_tx, resp_rx) = mpsc::unbounded_channel::<ResponseOrIdle>(); - tokio::spawn(Self::session(ctx.clone(), cmd_rx, resp_tx)); - - // Return the object - Ok(NetLoop { - ctx, - server, - cmd_tx, - resp_rx, - }) - } - - /// Coms with the background session - async fn session( - ctx: ClientContext, - mut cmd_rx: Receiver<Request>, - resp_tx: UnboundedSender<ResponseOrIdle>, - ) -> () { - let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities); - loop { - let cmd = match cmd_rx.recv().await { - None => break, - Some(cmd_recv) => cmd_recv, - }; - - tracing::debug!(cmd=?cmd, sock=%ctx.addr, "command"); - let maybe_response = session.request(cmd).await; - tracing::debug!(cmd=?maybe_response, sock=%ctx.addr, "response"); - - match resp_tx.send(maybe_response) { - Err(_) => break, - Ok(_) => (), - }; - } - tracing::info!("runner is quitting"); - } - - async fn core(&mut self) -> Result<()> { - let mut maybe_idle: Option<Arc<Notify>> = None; - loop { - tokio::select! { - // Managing imap_flow stuff - srv_evt = self.server.progress() => match srv_evt? { - ServerFlowEvent::ResponseSent { handle: _handle, response } => { - match response { - Response::Status(Status::Bye(_)) => return Ok(()), - _ => tracing::trace!("sent to {} content {:?}", self.ctx.addr, response), - } - }, - ServerFlowEvent::CommandReceived { command } => { - match self.cmd_tx.try_send(Request::ImapCommand(command)) { - Ok(_) => (), - Err(mpsc::error::TrySendError::Full(_)) => { - self.server.enqueue_status(Status::bye(None, "Too fast").unwrap()); - tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr); - } - _ => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - } - } - }, - ServerFlowEvent::IdleCommandReceived { tag } => { - match self.cmd_tx.try_send(Request::IdleStart(tag)) { - Ok(_) => (), - Err(mpsc::error::TrySendError::Full(_)) => { - self.server.enqueue_status(Status::bye(None, "Too fast").unwrap()); - tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr); - } - _ => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - } - } - } - ServerFlowEvent::IdleDoneReceived => { - tracing::trace!("client sent DONE and want to stop IDLE"); - maybe_idle.ok_or(anyhow!("Received IDLE done but not idling currently"))?.notify_one(); - maybe_idle = None; - } - flow => { - self.server.enqueue_status(Status::bye(None, "Unsupported server flow event").unwrap()); - tracing::error!("session task exited for {:?} due to unsupported flow {:?}", self.ctx.addr, flow); - } - }, - - // Managing response generated by Aerogramme - maybe_msg = self.resp_rx.recv() => match maybe_msg { - Some(ResponseOrIdle::Response(response)) => { - tracing::trace!("Interactive, server has a response for the client"); - for body_elem in response.body.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.server.enqueue_status(response.completion); - }, - Some(ResponseOrIdle::IdleAccept(stop)) => { - tracing::trace!("Interactive, server agreed to switch in idle mode"); - let cr = CommandContinuationRequest::basic(None, "Idling")?; - self.server.idle_accept(cr).or(Err(anyhow!("refused continuation for idle accept")))?; - self.cmd_tx.try_send(Request::IdlePoll)?; - if maybe_idle.is_some() { - bail!("Can't start IDLE if already idling"); - } - maybe_idle = Some(stop); - }, - Some(ResponseOrIdle::IdleEvent(elems)) => { - tracing::trace!("server imap session has some change to communicate to the client"); - for body_elem in elems.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.cmd_tx.try_send(Request::IdlePoll)?; - }, - Some(ResponseOrIdle::IdleReject(response)) => { - tracing::trace!("inform client that session rejected idle"); - self.server - .idle_reject(response.completion) - .or(Err(anyhow!("wrong reject command")))?; - }, - None => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - }, - Some(_) => unreachable!(), - - }, - - // When receiving a CTRL+C - _ = self.ctx.must_exit.changed() => { - tracing::trace!("Interactive, CTRL+C, exiting"); - self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap()); - }, - }; - } - } - - /* - async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc<Notify>) -> Result<LoopMode> { - // Flush send - loop { - tracing::trace!("flush server send"); - match self.server.progress_send().await? { - Some(..) => continue, - None => break, - } - } - - tokio::select! { - // Receiving IDLE event from background - maybe_msg = self.resp_rx.recv() => match maybe_msg { - // Session decided idle is terminated - Some(ResponseOrIdle::Response(response)) => { - tracing::trace!("server imap session said idle is done, sending response done, switching to interactive"); - for body_elem in response.body.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.server.enqueue_status(response.completion); - return Ok(LoopMode::Interactive) - }, - // Session has some information for user - Some(ResponseOrIdle::IdleEvent(elems)) => { - tracing::trace!("server imap session has some change to communicate to the client"); - for body_elem in elems.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.cmd_tx.try_send(Request::Idle)?; - return Ok(LoopMode::Idle(buff, stop)) - }, - - // Session crashed - None => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - return Ok(LoopMode::Interactive) - }, - - // Session can't start idling while already idling, it's a logic error! - Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"), - }, - - // User is trying to interact with us - read_client_result = self.server.stream.read(&mut buff) => { - let _bytes_read = read_client_result?; - use imap_codec::decode::Decoder; - let codec = imap_codec::IdleDoneCodec::new(); - tracing::trace!("client sent some data for the server IMAP session"); - match codec.decode(&buff) { - Ok(([], imap_codec::imap_types::extensions::idle::IdleDone)) => { - // Session will be informed that it must stop idle - // It will generate the "done" message and change the loop mode - tracing::trace!("client sent DONE and want to stop IDLE"); - stop.notify_one() - }, - Err(_) => { - tracing::trace!("Unable to decode DONE, maybe not enough data were sent?"); - }, - _ => bail!("Client sent data after terminating the continuation without waiting for the server. This is an unsupported behavior and bug in Aerogramme, quitting."), - }; - - return Ok(LoopMode::Idle(buff, stop)) - }, - - // When receiving a CTRL+C - _ = self.ctx.must_exit.changed() => { - tracing::trace!("CTRL+C sent, aborting IDLE for this session"); - self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap()); - return Ok(LoopMode::Interactive) - }, - }; - }*/ -} diff --git a/src/imap/request.rs b/src/imap/request.rs deleted file mode 100644 index cff18a3..0000000 --- a/src/imap/request.rs +++ /dev/null @@ -1,9 +0,0 @@ -use imap_codec::imap_types::command::Command; -use imap_codec::imap_types::core::Tag; - -#[derive(Debug)] -pub enum Request { - ImapCommand(Command<'static>), - IdleStart(Tag<'static>), - IdlePoll, -} diff --git a/src/imap/response.rs b/src/imap/response.rs deleted file mode 100644 index b6a0e98..0000000 --- a/src/imap/response.rs +++ /dev/null @@ -1,124 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::command::Command; -use imap_codec::imap_types::core::Tag; -use imap_codec::imap_types::response::{Code, Data, Status}; -use std::sync::Arc; -use tokio::sync::Notify; - -#[derive(Debug)] -pub enum Body<'a> { - Data(Data<'a>), - Status(Status<'a>), -} - -pub struct ResponseBuilder<'a> { - tag: Option<Tag<'a>>, - code: Option<Code<'a>>, - text: String, - body: Vec<Body<'a>>, -} - -impl<'a> ResponseBuilder<'a> { - pub fn to_req(mut self, cmd: &Command<'a>) -> Self { - self.tag = Some(cmd.tag.clone()); - self - } - pub fn tag(mut self, tag: Tag<'a>) -> Self { - self.tag = Some(tag); - self - } - - pub fn message(mut self, txt: impl Into<String>) -> Self { - self.text = txt.into(); - self - } - - pub fn code(mut self, code: Code<'a>) -> Self { - self.code = Some(code); - self - } - - pub fn data(mut self, data: Data<'a>) -> Self { - self.body.push(Body::Data(data)); - self - } - - pub fn many_data(mut self, data: Vec<Data<'a>>) -> Self { - for d in data.into_iter() { - self = self.data(d); - } - self - } - - #[allow(dead_code)] - pub fn info(mut self, status: Status<'a>) -> Self { - self.body.push(Body::Status(status)); - self - } - - #[allow(dead_code)] - pub fn many_info(mut self, status: Vec<Status<'a>>) -> Self { - for d in status.into_iter() { - self = self.info(d); - } - self - } - - pub fn set_body(mut self, body: Vec<Body<'a>>) -> Self { - self.body = body; - self - } - - pub fn ok(self) -> Result<Response<'a>> { - Ok(Response { - completion: Status::ok(self.tag, self.code, self.text)?, - body: self.body, - }) - } - - pub fn no(self) -> Result<Response<'a>> { - Ok(Response { - completion: Status::no(self.tag, self.code, self.text)?, - body: self.body, - }) - } - - pub fn bad(self) -> Result<Response<'a>> { - Ok(Response { - completion: Status::bad(self.tag, self.code, self.text)?, - body: self.body, - }) - } -} - -#[derive(Debug)] -pub struct Response<'a> { - pub body: Vec<Body<'a>>, - pub completion: Status<'a>, -} - -impl<'a> Response<'a> { - pub fn build() -> ResponseBuilder<'a> { - ResponseBuilder { - tag: None, - code: None, - text: "".to_string(), - body: vec![], - } - } - - pub fn bye() -> Result<Response<'a>> { - Ok(Response { - completion: Status::bye(None, "bye")?, - body: vec![], - }) - } -} - -#[derive(Debug)] -pub enum ResponseOrIdle { - Response(Response<'static>), - IdleAccept(Arc<Notify>), - IdleReject(Response<'static>), - IdleEvent(Vec<Body<'static>>), -} diff --git a/src/imap/search.rs b/src/imap/search.rs deleted file mode 100644 index 37a7e9e..0000000 --- a/src/imap/search.rs +++ /dev/null @@ -1,477 +0,0 @@ -use std::num::{NonZeroU32, NonZeroU64}; - -use imap_codec::imap_types::core::Vec1; -use imap_codec::imap_types::search::{MetadataItemSearch, SearchKey}; -use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; - -use crate::imap::index::MailIndex; -use crate::imap::mail_view::MailView; -use crate::mail::query::QueryScope; - -pub enum SeqType { - Undefined, - NonUid, - Uid, -} -impl SeqType { - pub fn is_uid(&self) -> bool { - matches!(self, Self::Uid) - } -} - -pub struct Criteria<'a>(pub &'a SearchKey<'a>); -impl<'a> Criteria<'a> { - /// Returns a set of email identifiers that is greater or equal - /// to the set of emails to return - pub fn to_sequence_set(&self) -> (SequenceSet, SeqType) { - match self.0 { - SearchKey::All => (sequence_set_all(), SeqType::Undefined), - SearchKey::SequenceSet(seq_set) => (seq_set.clone(), SeqType::NonUid), - SearchKey::Uid(seq_set) => (seq_set.clone(), SeqType::Uid), - SearchKey::Not(_inner) => { - tracing::debug!( - "using NOT in a search request is slow: it selects all identifiers" - ); - (sequence_set_all(), SeqType::Undefined) - } - SearchKey::Or(left, right) => { - tracing::debug!("using OR in a search request is slow: no deduplication is done"); - let (base, base_seqtype) = Self(&left).to_sequence_set(); - let (ext, ext_seqtype) = Self(&right).to_sequence_set(); - - // Check if we have a UID/ID conflict in fetching: now we don't know how to handle them - match (base_seqtype, ext_seqtype) { - (SeqType::Uid, SeqType::NonUid) | (SeqType::NonUid, SeqType::Uid) => { - (sequence_set_all(), SeqType::Undefined) - } - (SeqType::Undefined, x) | (x, _) => { - let mut new_vec = base.0.into_inner(); - new_vec.extend_from_slice(ext.0.as_ref()); - let seq = SequenceSet( - Vec1::try_from(new_vec) - .expect("merging non empty vec lead to non empty vec"), - ); - (seq, x) - } - } - } - SearchKey::And(search_list) => { - tracing::debug!( - "using AND in a search request is slow: no intersection is performed" - ); - // As we perform no intersection, we don't care if we mix uid or id. - // We only keep the smallest range, being it ID or UID, depending of - // which one has the less items. This is an approximation as UID ranges - // can have holes while ID ones can't. - search_list - .as_ref() - .iter() - .map(|crit| Self(&crit).to_sequence_set()) - .min_by(|(x, _), (y, _)| { - let x_size = approx_sequence_set_size(x); - let y_size = approx_sequence_set_size(y); - x_size.cmp(&y_size) - }) - .unwrap_or((sequence_set_all(), SeqType::Undefined)) - } - _ => (sequence_set_all(), SeqType::Undefined), - } - } - - /// Not really clever as we can have cases where we filter out - /// the email before needing to inspect its meta. - /// But for now we are seeking the most basic/stupid algorithm. - pub fn query_scope(&self) -> QueryScope { - use SearchKey::*; - match self.0 { - // Combinators - And(and_list) => and_list - .as_ref() - .iter() - .fold(QueryScope::Index, |prev, sk| { - prev.union(&Criteria(sk).query_scope()) - }), - Not(inner) => Criteria(inner).query_scope(), - Or(left, right) => Criteria(left) - .query_scope() - .union(&Criteria(right).query_scope()), - All => QueryScope::Index, - - // IMF Headers - Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) - | Subject(_) | To(_) => QueryScope::Partial, - // Internal Date is also stored in MailMeta - Before(_) | On(_) | Since(_) => QueryScope::Partial, - // Message size is also stored in MailMeta - Larger(_) | Smaller(_) => QueryScope::Partial, - // Text and Body require that we fetch the full content! - Text(_) | Body(_) => QueryScope::Full, - - _ => QueryScope::Index, - } - } - - pub fn is_modseq(&self) -> bool { - use SearchKey::*; - match self.0 { - And(and_list) => and_list - .as_ref() - .iter() - .any(|child| Criteria(child).is_modseq()), - Or(left, right) => Criteria(left).is_modseq() || Criteria(right).is_modseq(), - Not(child) => Criteria(child).is_modseq(), - ModSeq { .. } => true, - _ => false, - } - } - - /// Returns emails that we now for sure we want to keep - /// but also a second list of emails we need to investigate further by - /// fetching some remote data - pub fn filter_on_idx<'b>( - &self, - midx_list: &[&'b MailIndex<'b>], - ) -> (Vec<&'b MailIndex<'b>>, Vec<&'b MailIndex<'b>>) { - let (p1, p2): (Vec<_>, Vec<_>) = midx_list - .iter() - .map(|x| (x, self.is_keep_on_idx(x))) - .filter(|(_midx, decision)| decision.is_keep()) - .map(|(midx, decision)| (*midx, decision)) - .partition(|(_midx, decision)| matches!(decision, PartialDecision::Keep)); - - let to_keep = p1.into_iter().map(|(v, _)| v).collect(); - let to_fetch = p2.into_iter().map(|(v, _)| v).collect(); - (to_keep, to_fetch) - } - - // ---- - - /// Here we are doing a partial filtering: we do not have access - /// to the headers or to the body, so every time we encounter a rule - /// based on them, we need to keep it. - /// - /// @TODO Could be optimized on a per-email basis by also returning the QueryScope - /// when more information is needed! - fn is_keep_on_idx(&self, midx: &MailIndex) -> PartialDecision { - use SearchKey::*; - match self.0 { - // Combinator logic - And(expr_list) => expr_list - .as_ref() - .iter() - .fold(PartialDecision::Keep, |acc, cur| { - acc.and(&Criteria(cur).is_keep_on_idx(midx)) - }), - Or(left, right) => { - let left_decision = Criteria(left).is_keep_on_idx(midx); - let right_decision = Criteria(right).is_keep_on_idx(midx); - left_decision.or(&right_decision) - } - Not(expr) => Criteria(expr).is_keep_on_idx(midx).not(), - All => PartialDecision::Keep, - - // Sequence logic - maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, midx).into(), - maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, midx).into(), - ModSeq { - metadata_item, - modseq, - } => is_keep_modseq(metadata_item, modseq, midx).into(), - - // All the stuff we can't evaluate yet - Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) - | Subject(_) | To(_) | Before(_) | On(_) | Since(_) | Larger(_) | Smaller(_) - | Text(_) | Body(_) => PartialDecision::Postpone, - - unknown => { - tracing::error!("Unknown filter {:?}", unknown); - PartialDecision::Discard - } - } - } - - /// @TODO we re-eveluate twice the same logic. The correct way would be, on each pass, - /// to simplify the searck query, by removing the elements that were already checked. - /// For example if we have AND(OR(seqid(X), body(Y)), body(X)), we can't keep for sure - /// the email, as body(x) might be false. So we need to check it. But as seqid(x) is true, - /// we could simplify the request to just body(x) and truncate the first OR. Today, we are - /// not doing that, and thus we reevaluate everything. - pub fn is_keep_on_query(&self, mail_view: &MailView) -> bool { - use SearchKey::*; - match self.0 { - // Combinator logic - And(expr_list) => expr_list - .as_ref() - .iter() - .all(|cur| Criteria(cur).is_keep_on_query(mail_view)), - Or(left, right) => { - Criteria(left).is_keep_on_query(mail_view) - || Criteria(right).is_keep_on_query(mail_view) - } - Not(expr) => !Criteria(expr).is_keep_on_query(mail_view), - All => true, - - //@FIXME Reevaluating our previous logic... - maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, &mail_view.in_idx), - maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, &mail_view.in_idx), - ModSeq { - metadata_item, - modseq, - } => is_keep_modseq(metadata_item, modseq, &mail_view.in_idx).into(), - - // Filter on mail meta - Before(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive < search_naive.as_ref(), - _ => false, - }, - On(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive == search_naive.as_ref(), - _ => false, - }, - Since(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive > search_naive.as_ref(), - _ => false, - }, - - // Message size is also stored in MailMeta - Larger(size_ref) => { - mail_view - .query_result - .metadata() - .expect("metadata were fetched") - .rfc822_size - > *size_ref as usize - } - Smaller(size_ref) => { - mail_view - .query_result - .metadata() - .expect("metadata were fetched") - .rfc822_size - < *size_ref as usize - } - - // Filter on well-known headers - Bcc(txt) => mail_view.is_header_contains_pattern(&b"bcc"[..], txt.as_ref()), - Cc(txt) => mail_view.is_header_contains_pattern(&b"cc"[..], txt.as_ref()), - From(txt) => mail_view.is_header_contains_pattern(&b"from"[..], txt.as_ref()), - Subject(txt) => mail_view.is_header_contains_pattern(&b"subject"[..], txt.as_ref()), - To(txt) => mail_view.is_header_contains_pattern(&b"to"[..], txt.as_ref()), - Header(hdr, txt) => mail_view.is_header_contains_pattern(hdr.as_ref(), txt.as_ref()), - - // Filter on Date header - SentBefore(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive < search_naive.as_ref()) - .unwrap_or(false), - SentOn(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive == search_naive.as_ref()) - .unwrap_or(false), - SentSince(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive > search_naive.as_ref()) - .unwrap_or(false), - - // Filter on the full content of the email - Text(txt) => mail_view - .content - .as_msg() - .map(|msg| { - msg.raw_part - .windows(txt.as_ref().len()) - .any(|win| win == txt.as_ref()) - }) - .unwrap_or(false), - Body(txt) => mail_view - .content - .as_msg() - .map(|msg| { - msg.raw_body - .windows(txt.as_ref().len()) - .any(|win| win == txt.as_ref()) - }) - .unwrap_or(false), - - unknown => { - tracing::error!("Unknown filter {:?}", unknown); - false - } - } - } -} - -// ---- Sequence things ---- -fn sequence_set_all() -> SequenceSet { - SequenceSet::from(Sequence::Range( - SeqOrUid::Value(NonZeroU32::MIN), - SeqOrUid::Asterisk, - )) -} - -// This is wrong as sequences can overlap -fn approx_sequence_set_size(seq_set: &SequenceSet) -> u64 { - seq_set.0.as_ref().iter().fold(0u64, |acc, seq| { - acc.saturating_add(approx_sequence_size(seq)) - }) -} - -// This is wrong as sequence UID can have holes, -// as we don't know the number of messages in the mailbox also -// we gave to guess -fn approx_sequence_size(seq: &Sequence) -> u64 { - match seq { - Sequence::Single(_) => 1, - Sequence::Range(SeqOrUid::Asterisk, _) | Sequence::Range(_, SeqOrUid::Asterisk) => u64::MAX, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - let x2 = x2.get() as i64; - let x1 = x1.get() as i64; - (x2 - x1).abs().try_into().unwrap_or(1) - } - } -} - -// --- Partial decision things ---- - -enum PartialDecision { - Keep, - Discard, - Postpone, -} -impl From<bool> for PartialDecision { - fn from(x: bool) -> Self { - match x { - true => PartialDecision::Keep, - _ => PartialDecision::Discard, - } - } -} -impl PartialDecision { - fn not(&self) -> Self { - match self { - Self::Keep => Self::Discard, - Self::Discard => Self::Keep, - Self::Postpone => Self::Postpone, - } - } - - fn or(&self, other: &Self) -> Self { - match (self, other) { - (Self::Keep, _) | (_, Self::Keep) => Self::Keep, - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, - (Self::Discard, Self::Discard) => Self::Discard, - } - } - - fn and(&self, other: &Self) -> Self { - match (self, other) { - (Self::Discard, _) | (_, Self::Discard) => Self::Discard, - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, - (Self::Keep, Self::Keep) => Self::Keep, - } - } - - fn is_keep(&self) -> bool { - !matches!(self, Self::Discard) - } -} - -// ----- Search Key things --- -fn is_sk_flag(sk: &SearchKey) -> bool { - use SearchKey::*; - match sk { - Answered | Deleted | Draft | Flagged | Keyword(..) | New | Old | Recent | Seen - | Unanswered | Undeleted | Undraft | Unflagged | Unkeyword(..) | Unseen => true, - _ => false, - } -} - -fn is_keep_flag(sk: &SearchKey, midx: &MailIndex) -> bool { - use SearchKey::*; - match sk { - Answered => midx.is_flag_set("\\Answered"), - Deleted => midx.is_flag_set("\\Deleted"), - Draft => midx.is_flag_set("\\Draft"), - Flagged => midx.is_flag_set("\\Flagged"), - Keyword(kw) => midx.is_flag_set(kw.inner()), - New => { - let is_recent = midx.is_flag_set("\\Recent"); - let is_seen = midx.is_flag_set("\\Seen"); - is_recent && !is_seen - } - Old => { - let is_recent = midx.is_flag_set("\\Recent"); - !is_recent - } - Recent => midx.is_flag_set("\\Recent"), - Seen => midx.is_flag_set("\\Seen"), - Unanswered => { - let is_answered = midx.is_flag_set("\\Recent"); - !is_answered - } - Undeleted => { - let is_deleted = midx.is_flag_set("\\Deleted"); - !is_deleted - } - Undraft => { - let is_draft = midx.is_flag_set("\\Draft"); - !is_draft - } - Unflagged => { - let is_flagged = midx.is_flag_set("\\Flagged"); - !is_flagged - } - Unkeyword(kw) => { - let is_keyword_set = midx.is_flag_set(kw.inner()); - !is_keyword_set - } - Unseen => { - let is_seen = midx.is_flag_set("\\Seen"); - !is_seen - } - - // Not flag logic - _ => unreachable!(), - } -} - -fn is_sk_seq(sk: &SearchKey) -> bool { - use SearchKey::*; - match sk { - SequenceSet(..) | Uid(..) => true, - _ => false, - } -} -fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool { - use SearchKey::*; - match sk { - SequenceSet(seq_set) => seq_set - .0 - .as_ref() - .iter() - .any(|seq| midx.is_in_sequence_i(seq)), - Uid(seq_set) => seq_set - .0 - .as_ref() - .iter() - .any(|seq| midx.is_in_sequence_uid(seq)), - _ => unreachable!(), - } -} - -fn is_keep_modseq( - filter: &Option<MetadataItemSearch>, - modseq: &NonZeroU64, - midx: &MailIndex, -) -> bool { - if filter.is_some() { - tracing::warn!(filter=?filter, "Ignoring search metadata filter as it's not supported yet"); - } - modseq <= &midx.modseq -} diff --git a/src/imap/session.rs b/src/imap/session.rs deleted file mode 100644 index fa3232a..0000000 --- a/src/imap/session.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anonymous, authenticated, selected}; -use crate::imap::flow; -use crate::imap::request::Request; -use crate::imap::response::{Response, ResponseOrIdle}; -use crate::login::ArcLoginProvider; -use anyhow::{anyhow, bail, Context, Result}; -use imap_codec::imap_types::{command::Command, core::Tag}; - -//----- -pub struct Instance { - pub login_provider: ArcLoginProvider, - pub server_capabilities: ServerCapability, - pub client_capabilities: ClientCapability, - pub state: flow::State, -} -impl Instance { - pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { - let client_cap = ClientCapability::new(&cap); - Self { - login_provider, - state: flow::State::NotAuthenticated, - server_capabilities: cap, - client_capabilities: client_cap, - } - } - - pub async fn request(&mut self, req: Request) -> ResponseOrIdle { - match req { - Request::IdleStart(tag) => self.idle_init(tag), - Request::IdlePoll => self.idle_poll().await, - Request::ImapCommand(cmd) => self.command(cmd).await, - } - } - - pub fn idle_init(&mut self, tag: Tag<'static>) -> ResponseOrIdle { - // Build transition - //@FIXME the notifier should be hidden inside the state and thus not part of the transition! - let transition = flow::Transition::Idle(tag.clone(), tokio::sync::Notify::new()); - - // Try to apply the transition and get the stop notifier - let maybe_stop = self - .state - .apply(transition) - .context("IDLE transition failed") - .and_then(|_| { - self.state - .notify() - .ok_or(anyhow!("IDLE state has no Notify object")) - }); - - // Build an appropriate response - match maybe_stop { - Ok(stop) => ResponseOrIdle::IdleAccept(stop), - Err(e) => { - tracing::error!(err=?e, "unable to init idle due to a transition error"); - //ResponseOrIdle::IdleReject(tag) - let no = Response::build() - .tag(tag) - .message( - "Internal error, processing command triggered an illegal IMAP state transition", - ) - .no() - .unwrap(); - ResponseOrIdle::IdleReject(no) - } - } - } - - pub async fn idle_poll(&mut self) -> ResponseOrIdle { - match self.idle_poll_happy().await { - Ok(r) => r, - Err(e) => { - tracing::error!(err=?e, "something bad happened in idle"); - ResponseOrIdle::Response(Response::bye().unwrap()) - } - } - } - - pub async fn idle_poll_happy(&mut self) -> Result<ResponseOrIdle> { - let (mbx, tag, stop) = match &mut self.state { - flow::State::Idle(_, ref mut mbx, _, tag, stop) => (mbx, tag.clone(), stop.clone()), - _ => bail!("Invalid session state, can't idle"), - }; - - tokio::select! { - _ = stop.notified() => { - self.state.apply(flow::Transition::UnIdle)?; - return Ok(ResponseOrIdle::Response(Response::build() - .tag(tag.clone()) - .message("IDLE completed") - .ok()?)) - }, - change = mbx.idle_sync() => { - tracing::debug!("idle event"); - return Ok(ResponseOrIdle::IdleEvent(change?)); - } - } - } - - pub async fn command(&mut self, cmd: Command<'static>) -> ResponseOrIdle { - // Command behavior is modulated by the state. - // To prevent state error, we handle the same command in separate code paths. - let (resp, tr) = match &mut self.state { - flow::State::NotAuthenticated => { - let ctx = anonymous::AnonymousContext { - req: &cmd, - login_provider: &self.login_provider, - server_capabilities: &self.server_capabilities, - }; - anonymous::dispatch(ctx).await - } - flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &cmd, - server_capabilities: &self.server_capabilities, - client_capabilities: &mut self.client_capabilities, - user, - }; - authenticated::dispatch(ctx).await - } - flow::State::Selected(ref user, ref mut mailbox, ref perm) => { - let ctx = selected::SelectedContext { - req: &cmd, - server_capabilities: &self.server_capabilities, - client_capabilities: &mut self.client_capabilities, - user, - mailbox, - perm, - }; - selected::dispatch(ctx).await - } - flow::State::Idle(..) => Err(anyhow!("can not receive command while idling")), - flow::State::Logout => Response::build() - .tag(cmd.tag.clone()) - .message("No commands are allowed in the LOGOUT state.") - .bad() - .map(|r| (r, flow::Transition::None)), - } - .unwrap_or_else(|err| { - tracing::error!("Command error {:?} occured while processing {:?}", err, cmd); - ( - Response::build() - .to_req(&cmd) - .message("Internal error while processing command") - .bad() - .unwrap(), - flow::Transition::None, - ) - }); - - if let Err(e) = self.state.apply(tr) { - tracing::error!( - "Transition error {:?} occured while processing on command {:?}", - e, - cmd - ); - return ResponseOrIdle::Response(Response::build() - .to_req(&cmd) - .message( - "Internal error, processing command triggered an illegal IMAP state transition", - ) - .bad() - .unwrap()); - } - ResponseOrIdle::Response(resp) - - /*match &self.state { - flow::State::Idle(_, _, _, _, n) => ResponseOrIdle::StartIdle(n.clone()), - _ => ResponseOrIdle::Response(resp), - }*/ - } -} |