From 46d952598474e851ee528515d7a9ffab88d3ad49 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 12 Jul 2022 16:35:11 +0200 Subject: [PATCH] Implement APPEND --- src/imap/command/authenticated.rs | 63 ++++++++++++++++++++++++++++++- src/imap/command/examined.rs | 43 ++++++++++++++++++++- src/imap/mailbox_view.rs | 2 +- src/mail/mailbox.rs | 31 +++++++++++---- 4 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 6208290..32a8e1e 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -1,11 +1,13 @@ use std::collections::BTreeMap; use std::sync::Arc; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use boitalettres::proto::res::body::Data as Body; use boitalettres::proto::{Request, Response}; use imap_codec::types::command::{CommandBody, StatusAttribute}; -use imap_codec::types::flag::FlagNameAttribute; +use imap_codec::types::core::NonZeroBytes; +use imap_codec::types::datetime::MyDateTime; +use imap_codec::types::flag::{Flag, FlagNameAttribute}; use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::types::response::{Code, Data, StatusAttributeValue}; @@ -13,7 +15,10 @@ use crate::imap::command::anonymous; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::mail::mailbox::Mailbox; +use crate::mail::uidindex::*; use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER}; +use crate::mail::IMF; pub struct AuthenticatedContext<'a> { pub req: &'a Request, @@ -44,6 +49,12 @@ pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, fl CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await, CommandBody::Examine { mailbox } => ctx.examine(mailbox).await, + CommandBody::Append { + mailbox, + flags, + date, + message, + } => ctx.append(mailbox, flags, date, message).await, _ => { let ctx = anonymous::AnonymousContext { req: ctx.req, @@ -316,4 +327,52 @@ impl<'a> AuthenticatedContext<'a> { flow::Transition::Examine(mb), )) } + + async fn append( + self, + mailbox: &MailboxCodec, + flags: &[Flag], + date: &Option, + message: &NonZeroBytes, + ) -> Result<(Response, flow::Transition)> { + match self.append_internal(mailbox, flags, date, message).await { + Ok((_mb, uidvalidity, uid)) => Ok(( + Response::ok("APPEND completed")?.with_extra_code(Code::Other( + "APPENDUID".try_into().unwrap(), + Some(format!("{} {}", uidvalidity, uid)), + )), + flow::Transition::None, + )), + Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + } + } + + pub(crate) async fn append_internal( + self, + mailbox: &MailboxCodec, + flags: &[Flag], + date: &Option, + message: &NonZeroBytes, + ) -> Result<(Arc, ImapUidvalidity, ImapUidvalidity)> { + let name = String::try_from(mailbox.clone())?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => bail!("Mailbox does not exist"), + }; + + if date.is_some() { + bail!("Cannot set date when appending message"); + } + + let msg = IMF::try_from(message.as_slice()) + .map_err(|_| anyhow!("Could not parse e-mail message"))?; + let flags = flags.iter().map(|x| x.to_string()).collect::>(); + // TODO: filter allowed flags? ping @Quentin + + let (uidvalidity, uid) = mb.append(msg, None, &flags[..]).await?; + + Ok((mb, uidvalidity, uid)) + } } diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 9dba680..ea773de 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -5,14 +5,19 @@ use boitalettres::proto::Request; use boitalettres::proto::Response; use imap_codec::types::command::{CommandBody, SearchKey}; use imap_codec::types::core::Charset; +use imap_codec::types::core::NonZeroBytes; +use imap_codec::types::datetime::MyDateTime; use imap_codec::types::fetch_attributes::MacroOrFetchAttributes; - +use imap_codec::types::flag::{Flag, FlagNameAttribute}; +use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; +use imap_codec::types::response::{Code, Data, StatusAttributeValue}; use imap_codec::types::sequence::SequenceSet; use crate::imap::command::authenticated; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::mail::uidindex::*; use crate::mail::user::User; pub struct ExaminedContext<'a> { @@ -37,6 +42,12 @@ pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> Result<(Response, flow::T uid, } => ctx.search(charset, criteria, uid).await, CommandBody::Noop => ctx.noop().await, + CommandBody::Append { + mailbox, + flags, + date, + message, + } => ctx.append(mailbox, flags, date, message).await, _ => { let ctx = authenticated::AuthenticatedContext { req: ctx.req, @@ -85,4 +96,34 @@ impl<'a> ExaminedContext<'a> { flow::Transition::None, )) } + + async fn append( + self, + mailbox: &MailboxCodec, + flags: &[Flag], + date: &Option, + message: &NonZeroBytes, + ) -> Result<(Response, flow::Transition)> { + let ctx2 = authenticated::AuthenticatedContext { + req: self.req, + user: self.user, + }; + + match ctx2.append_internal(mailbox, flags, date, message).await { + Ok((mb, uidvalidity, uid)) => { + let resp = Response::ok("APPEND completed")?.with_extra_code(Code::Other( + "APPENDUID".try_into().unwrap(), + Some(format!("{} {}", uidvalidity, uid)), + )); + + if Arc::ptr_eq(&mb, &self.mailbox.mailbox) { + let data = self.mailbox.update().await?; + Ok((resp.with_body(data), flow::Transition::None)) + } else { + Ok((resp, flow::Transition::None)) + } + } + Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + } + } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index db6f490..e4df3d0 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -37,7 +37,7 @@ const DEFAULT_FLAGS: [Flag; 5] = [ /// what the client knows, and produces IMAP messages to be sent to the /// client that go along updates to `known_state`. pub struct MailboxView { - mailbox: Arc, + pub(crate) mailbox: Arc, known_state: UidIndex, } diff --git a/src/mail/mailbox.rs b/src/mail/mailbox.rs index e80fa34..0e8af70 100644 --- a/src/mail/mailbox.rs +++ b/src/mail/mailbox.rs @@ -96,8 +96,13 @@ impl Mailbox { } /// Insert an email into the mailbox - pub async fn append<'a>(&self, msg: IMF<'a>, ident: Option) -> Result<()> { - self.mbox.write().await.append(msg, ident).await + pub async fn append<'a>( + &self, + msg: IMF<'a>, + ident: Option, + flags: &[Flag], + ) -> Result<(ImapUidvalidity, ImapUid)> { + self.mbox.write().await.append(msg, ident, flags).await } /// Insert an email into the mailbox, copying it from an existing S3 object @@ -260,7 +265,12 @@ impl MailboxInternal { self.uid_index.push(del_flag_op).await } - async fn append(&mut self, mail: IMF<'_>, ident: Option) -> Result<()> { + async fn append( + &mut self, + mail: IMF<'_>, + ident: Option, + flags: &[Flag], + ) -> Result<(ImapUidvalidity, ImapUid)> { let ident = ident.unwrap_or_else(|| gen_ident()); let message_key = gen_key(); @@ -292,13 +302,18 @@ impl MailboxInternal { )?; // Add mail to Bayou mail index - let add_mail_op = self - .uid_index - .state() - .op_mail_add(ident, vec!["\\Unseen".into()]); + let uid_state = self.uid_index.state(); + let add_mail_op = uid_state.op_mail_add(ident, flags.to_vec()); + + let uidvalidity = uid_state.uidvalidity; + let uid = match add_mail_op { + UidIndexOp::MailAdd(_, uid, _) => uid, + _ => unreachable!(), + }; + self.uid_index.push(add_mail_op).await?; - Ok(()) + Ok((uidvalidity, uid)) } async fn append_from_s3<'a>(