diff --git a/Cargo.lock b/Cargo.lock
index b433689..a430b9e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -689,6 +689,15 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "env_logger"
version = "0.7.1"
@@ -1259,6 +1268,16 @@ dependencies = [
"value-bag",
]
+[[package]]
+name = "mail-parser"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c46a841ae5276aba5218ade7bb76896358f9f95a925c7b3deea6a0ec0fb8e2a7"
+dependencies = [
+ "encoding_rs",
+ "serde",
+]
+
[[package]]
name = "mailrage"
version = "0.0.1"
@@ -1279,6 +1298,7 @@ dependencies = [
"lazy_static",
"ldap3",
"log",
+ "mail-parser",
"pretty_env_logger",
"rand",
"rmp-serde",
diff --git a/Cargo.toml b/Cargo.toml
index 4393d1c..be05124 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ itertools = "0.10"
lazy_static = "1.4"
ldap3 = { version = "0.10", default-features = false, features = ["tls"] }
log = "0.4"
+mail-parser = "0.4.8"
pretty_env_logger = "0.4"
rusoto_core = "0.48.0"
rusoto_credential = "0.48.0"
diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs
index 47df5be..443edda 100644
--- a/src/imap/command/authenticated.rs
+++ b/src/imap/command/authenticated.rs
@@ -8,18 +8,11 @@ use imap_codec::types::response::{Code, Data, Status};
use crate::imap::command::anonymous;
use crate::imap::flow;
+use crate::imap::mailbox_view::MailboxView;
use crate::mail::mailbox::Mailbox;
use crate::mail::user::User;
-const DEFAULT_FLAGS: [Flag; 5] = [
- Flag::Seen,
- Flag::Answered,
- Flag::Flagged,
- Flag::Deleted,
- Flag::Draft,
-];
-
pub struct AuthenticatedContext<'a> {
pub req: &'a Request,
pub user: &'a User,
@@ -96,59 +89,24 @@ impl<'a> AuthenticatedContext<'a> {
async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
let name = String::try_from(mailbox.clone())?;
- let mut mb = self.user.open_mailbox(name)?;
+ let mb_opt = self.user.open_mailbox(&name).await?;
+ let mb = match mb_opt {
+ Some(mb) => mb,
+ None => {
+ return Ok((
+ Response::no("Mailbox does not exist")?,
+ flow::Transition::None,
+ ))
+ }
+ };
tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected");
- let sum = mb.summary().await?;
- tracing::trace!(summary=%sum, "mailbox.summary");
-
- let mut res = Vec::
::new();
-
- res.push(Body::Data(Data::Exists(sum.exists)));
-
- res.push(Body::Data(Data::Recent(sum.recent)));
-
- let mut flags: Vec = sum.flags.map(|f| match f.chars().next() {
- Some('\\') => None,
- Some('$') if f == "$unseen" => None,
- Some(_) => match Atom::try_from(f.clone()) {
- Err(_) => {
- tracing::error!(username=%self.user.username, mailbox=%name, flag=%f, "Unable to encode flag as IMAP atom");
- None
- },
- Ok(a) => Some(Flag::Keyword(a)),
- },
- None => None,
- }).flatten().collect();
- flags.extend_from_slice(&DEFAULT_FLAGS);
-
- res.push(Body::Data(Data::Flags(flags.clone())));
-
- let uid_validity = Status::ok(None, Some(Code::UidValidity(sum.validity)), "UIDs valid")
- .map_err(Error::msg)?;
- res.push(Body::Status(uid_validity));
-
- let next_uid = Status::ok(None, Some(Code::UidNext(sum.next)), "Predict next UID")
- .map_err(Error::msg)?;
- res.push(Body::Status(next_uid));
-
- if let Some(unseen) = sum.unseen {
- let status_unseen =
- Status::ok(None, Some(Code::Unseen(unseen.clone())), "First unseen UID")
- .map_err(Error::msg)?;
- res.push(Body::Status(status_unseen));
- }
-
- flags.push(Flag::Permanent);
- let permanent_flags =
- Status::ok(None, Some(Code::PermanentFlags(flags)), "Flags permitted")
- .map_err(Error::msg)?;
- res.push(Body::Status(permanent_flags));
+ let (mb, data) = MailboxView::new(mb).await?;
Ok((
Response::ok("Select completed")?
.with_extra_code(Code::ReadWrite)
- .with_body(res),
+ .with_body(data),
flow::Transition::Select(mb),
))
}
diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs
index b1bba23..4e3ff2f 100644
--- a/src/imap/command/selected.rs
+++ b/src/imap/command/selected.rs
@@ -9,6 +9,7 @@ use imap_codec::types::sequence::SequenceSet;
use crate::imap::command::authenticated;
use crate::imap::flow;
+use crate::imap::mailbox_view::MailboxView;
use crate::mail::mailbox::Mailbox;
use crate::mail::user::User;
@@ -16,7 +17,7 @@ use crate::mail::user::User;
pub struct SelectedContext<'a> {
pub req: &'a Request,
pub user: &'a User,
- pub mailbox: &'a mut Mailbox,
+ pub mailbox: &'a mut MailboxView,
}
pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response, flow::Transition)> {
diff --git a/src/imap/flow.rs b/src/imap/flow.rs
index 0fe6f92..c9d7e40 100644
--- a/src/imap/flow.rs
+++ b/src/imap/flow.rs
@@ -1,6 +1,7 @@
use std::error::Error as StdError;
use std::fmt;
+use crate::imap::mailbox_view::MailboxView;
use crate::mail::mailbox::Mailbox;
use crate::mail::user::User;
@@ -18,14 +19,14 @@ impl StdError for Error {}
pub enum State {
NotAuthenticated,
Authenticated(User),
- Selected(User, Mailbox),
+ Selected(User, MailboxView),
Logout,
}
pub enum Transition {
None,
Authenticate(User),
- Select(Mailbox),
+ Select(MailboxView),
Unselect,
Logout,
}
diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs
new file mode 100644
index 0000000..ec5580d
--- /dev/null
+++ b/src/imap/mailbox_view.rs
@@ -0,0 +1,154 @@
+use std::sync::Arc;
+
+use anyhow::{Error, Result};
+use boitalettres::proto::{res::body::Data as Body, Request, Response};
+use imap_codec::types::command::CommandBody;
+use imap_codec::types::core::Atom;
+use imap_codec::types::flag::Flag;
+use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
+use imap_codec::types::response::{Code, Data, Status};
+
+use crate::mail::mailbox::{Mailbox, Summary};
+use crate::mail::uidindex::UidIndex;
+
+const DEFAULT_FLAGS: [Flag; 5] = [
+ Flag::Seen,
+ Flag::Answered,
+ Flag::Flagged,
+ Flag::Deleted,
+ Flag::Draft,
+];
+
+/// 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 {
+ mailbox: Arc,
+ known_state: UidIndex,
+}
+
+impl MailboxView {
+ /// Creates a new IMAP view into a mailbox.
+ /// Generates the necessary IMAP messages so that the client
+ /// has a satisfactory summary of the current mailbox's state.
+ pub async fn new(mailbox: Arc) -> Result<(Self, Vec)> {
+ let state = mailbox.current_uid_index().await;
+
+ let new_view = Self {
+ mailbox,
+ known_state: state,
+ };
+
+ let mut data = Vec::::new();
+ data.push(new_view.exists()?);
+ data.push(new_view.recent()?);
+ data.extend(new_view.flags()?.into_iter());
+ data.push(new_view.uidvalidity()?);
+ data.push(new_view.uidnext()?);
+ if let Some(unseen) = new_view.unseen()? {
+ data.push(unseen);
+ }
+
+ Ok((new_view, data))
+ }
+
+ // ----
+
+ /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state`
+ fn uidvalidity(&self) -> Result {
+ let uid_validity = Status::ok(
+ None,
+ Some(Code::UidValidity(self.known_state.uidvalidity)),
+ "UIDs valid",
+ )
+ .map_err(Error::msg)?;
+ Ok(Body::Status(uid_validity))
+ }
+
+ /// Produce an OK [UIDNEXT _] message corresponding to `known_state`
+ fn uidnext(&self) -> Result {
+ let next_uid = Status::ok(
+ None,
+ Some(Code::UidNext(self.known_state.uidnext)),
+ "Predict next UID",
+ )
+ .map_err(Error::msg)?;
+ Ok(Body::Status(next_uid))
+ }
+
+ /// Produces an UNSEEN message (if relevant) corresponding to the
+ /// first unseen message id in `known_state`
+ fn unseen(&self) -> Result