Some refactoring on mailbox structures and views
This commit is contained in:
parent
8b7eb1ca91
commit
b95028f89e
15 changed files with 330 additions and 155 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -689,6 +689,15 @@ version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
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]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -1259,6 +1268,16 @@ dependencies = [
|
||||||
"value-bag",
|
"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]]
|
[[package]]
|
||||||
name = "mailrage"
|
name = "mailrage"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
@ -1279,6 +1298,7 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"ldap3",
|
"ldap3",
|
||||||
"log",
|
"log",
|
||||||
|
"mail-parser",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"rand",
|
"rand",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
|
|
|
@ -20,6 +20,7 @@ itertools = "0.10"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
ldap3 = { version = "0.10", default-features = false, features = ["tls"] }
|
ldap3 = { version = "0.10", default-features = false, features = ["tls"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
mail-parser = "0.4.8"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
rusoto_core = "0.48.0"
|
rusoto_core = "0.48.0"
|
||||||
rusoto_credential = "0.48.0"
|
rusoto_credential = "0.48.0"
|
||||||
|
|
|
@ -8,18 +8,11 @@ use imap_codec::types::response::{Code, Data, Status};
|
||||||
|
|
||||||
use crate::imap::command::anonymous;
|
use crate::imap::command::anonymous;
|
||||||
use crate::imap::flow;
|
use crate::imap::flow;
|
||||||
|
use crate::imap::mailbox_view::MailboxView;
|
||||||
|
|
||||||
use crate::mail::mailbox::Mailbox;
|
use crate::mail::mailbox::Mailbox;
|
||||||
use crate::mail::user::User;
|
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 struct AuthenticatedContext<'a> {
|
||||||
pub req: &'a Request,
|
pub req: &'a Request,
|
||||||
pub user: &'a User,
|
pub user: &'a User,
|
||||||
|
@ -96,59 +89,24 @@ impl<'a> AuthenticatedContext<'a> {
|
||||||
async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
|
async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
|
||||||
let name = String::try_from(mailbox.clone())?;
|
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");
|
tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected");
|
||||||
|
|
||||||
let sum = mb.summary().await?;
|
let (mb, data) = MailboxView::new(mb).await?;
|
||||||
tracing::trace!(summary=%sum, "mailbox.summary");
|
|
||||||
|
|
||||||
let mut res = Vec::<Body>::new();
|
|
||||||
|
|
||||||
res.push(Body::Data(Data::Exists(sum.exists)));
|
|
||||||
|
|
||||||
res.push(Body::Data(Data::Recent(sum.recent)));
|
|
||||||
|
|
||||||
let mut flags: Vec<Flag> = 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));
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Response::ok("Select completed")?
|
Response::ok("Select completed")?
|
||||||
.with_extra_code(Code::ReadWrite)
|
.with_extra_code(Code::ReadWrite)
|
||||||
.with_body(res),
|
.with_body(data),
|
||||||
flow::Transition::Select(mb),
|
flow::Transition::Select(mb),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use imap_codec::types::sequence::SequenceSet;
|
||||||
|
|
||||||
use crate::imap::command::authenticated;
|
use crate::imap::command::authenticated;
|
||||||
use crate::imap::flow;
|
use crate::imap::flow;
|
||||||
|
use crate::imap::mailbox_view::MailboxView;
|
||||||
|
|
||||||
use crate::mail::mailbox::Mailbox;
|
use crate::mail::mailbox::Mailbox;
|
||||||
use crate::mail::user::User;
|
use crate::mail::user::User;
|
||||||
|
@ -16,7 +17,7 @@ use crate::mail::user::User;
|
||||||
pub struct SelectedContext<'a> {
|
pub struct SelectedContext<'a> {
|
||||||
pub req: &'a Request,
|
pub req: &'a Request,
|
||||||
pub user: &'a User,
|
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)> {
|
pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response, flow::Transition)> {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::imap::mailbox_view::MailboxView;
|
||||||
use crate::mail::mailbox::Mailbox;
|
use crate::mail::mailbox::Mailbox;
|
||||||
use crate::mail::user::User;
|
use crate::mail::user::User;
|
||||||
|
|
||||||
|
@ -18,14 +19,14 @@ impl StdError for Error {}
|
||||||
pub enum State {
|
pub enum State {
|
||||||
NotAuthenticated,
|
NotAuthenticated,
|
||||||
Authenticated(User),
|
Authenticated(User),
|
||||||
Selected(User, Mailbox),
|
Selected(User, MailboxView),
|
||||||
Logout,
|
Logout,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Transition {
|
pub enum Transition {
|
||||||
None,
|
None,
|
||||||
Authenticate(User),
|
Authenticate(User),
|
||||||
Select(Mailbox),
|
Select(MailboxView),
|
||||||
Unselect,
|
Unselect,
|
||||||
Logout,
|
Logout,
|
||||||
}
|
}
|
||||||
|
|
154
src/imap/mailbox_view.rs
Normal file
154
src/imap/mailbox_view.rs
Normal file
|
@ -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<Mailbox>,
|
||||||
|
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<Mailbox>) -> Result<(Self, Vec<Body>)> {
|
||||||
|
let state = mailbox.current_uid_index().await;
|
||||||
|
|
||||||
|
let new_view = Self {
|
||||||
|
mailbox,
|
||||||
|
known_state: state,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut data = Vec::<Body>::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<Body> {
|
||||||
|
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<Body> {
|
||||||
|
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<Option<Body>> {
|
||||||
|
let unseen = self
|
||||||
|
.known_state
|
||||||
|
.idx_by_flag
|
||||||
|
.get(&"$unseen".to_string())
|
||||||
|
.and_then(|os| os.get_min())
|
||||||
|
.cloned();
|
||||||
|
if let Some(unseen) = unseen {
|
||||||
|
let status_unseen =
|
||||||
|
Status::ok(None, Some(Code::Unseen(unseen.clone())), "First unseen UID")
|
||||||
|
.map_err(Error::msg)?;
|
||||||
|
Ok(Some(Body::Status(status_unseen)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce an EXISTS message corresponding to the number of mails
|
||||||
|
/// in `known_state`
|
||||||
|
fn exists(&self) -> Result<Body> {
|
||||||
|
let exists = u32::try_from(self.known_state.idx_by_uid.len())?;
|
||||||
|
Ok(Body::Data(Data::Exists(exists)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a RECENT message corresponding to the number of
|
||||||
|
/// recent mails in `known_state`
|
||||||
|
fn recent(&self) -> Result<Body> {
|
||||||
|
let recent = self
|
||||||
|
.known_state
|
||||||
|
.idx_by_flag
|
||||||
|
.get(&"\\Recent".to_string())
|
||||||
|
.map(|os| os.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let recent = u32::try_from(recent)?;
|
||||||
|
Ok(Body::Data(Data::Recent(recent)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a FLAGS and a PERMANENTFLAGS message that indicates
|
||||||
|
/// the flags that are in `known_state` + default flags
|
||||||
|
fn flags(&self) -> Result<Vec<Body>> {
|
||||||
|
let mut flags: Vec<Flag> = self
|
||||||
|
.known_state
|
||||||
|
.idx_by_flag
|
||||||
|
.flags()
|
||||||
|
.map(|f| match f.chars().next() {
|
||||||
|
Some('\\') => None,
|
||||||
|
Some('$') if f == "$unseen" => None,
|
||||||
|
Some(_) => match Atom::try_from(f.clone()) {
|
||||||
|
Err(_) => {
|
||||||
|
tracing::error!(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);
|
||||||
|
let mut ret = vec![Body::Data(Data::Flags(flags.clone()))];
|
||||||
|
|
||||||
|
flags.push(Flag::Permanent);
|
||||||
|
let permanent_flags =
|
||||||
|
Status::ok(None, Some(Code::PermanentFlags(flags)), "Flags permitted")
|
||||||
|
.map_err(Error::msg)?;
|
||||||
|
ret.push(Body::Status(permanent_flags));
|
||||||
|
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod command;
|
mod command;
|
||||||
mod flow;
|
mod flow;
|
||||||
|
mod mailbox_view;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
|
|
@ -2,7 +2,6 @@ use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use ldap3::{LdapConnAsync, Scope, SearchEntry};
|
use ldap3::{LdapConnAsync, Scope, SearchEntry};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use rusoto_signature::Region;
|
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::login::*;
|
use crate::login::*;
|
||||||
|
|
|
@ -13,7 +13,6 @@ use rand::prelude::*;
|
||||||
use rusoto_core::HttpClient;
|
use rusoto_core::HttpClient;
|
||||||
use rusoto_credential::{AwsCredentials, StaticProvider};
|
use rusoto_credential::{AwsCredentials, StaticProvider};
|
||||||
use rusoto_s3::S3Client;
|
use rusoto_s3::S3Client;
|
||||||
use rusoto_signature::Region;
|
|
||||||
|
|
||||||
use crate::cryptoblob::*;
|
use crate::cryptoblob::*;
|
||||||
|
|
||||||
|
@ -52,7 +51,7 @@ pub struct PublicCredentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The struct StorageCredentials contains access key to an S3 and K2V bucket
|
/// The struct StorageCredentials contains access key to an S3 and K2V bucket
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||||
pub struct StorageCredentials {
|
pub struct StorageCredentials {
|
||||||
pub s3_region: Region,
|
pub s3_region: Region,
|
||||||
pub k2v_region: Region,
|
pub k2v_region: Region,
|
||||||
|
@ -87,6 +86,24 @@ pub struct CryptoKeys {
|
||||||
pub public: PublicKey,
|
pub public: PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A custom S3 region, composed of a region name and endpoint.
|
||||||
|
/// We use this instead of rusoto_signature::Region so that we can
|
||||||
|
/// derive Hash and Eq
|
||||||
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub struct Region {
|
||||||
|
pub name: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Region {
|
||||||
|
pub fn as_rusoto_region(&self) -> rusoto_signature::Region {
|
||||||
|
rusoto_signature::Region::Custom {
|
||||||
|
name: self.name.clone(),
|
||||||
|
endpoint: self.endpoint.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----
|
// ----
|
||||||
|
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
|
@ -111,7 +128,7 @@ impl StorageCredentials {
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(K2vClient::new(
|
Ok(K2vClient::new(
|
||||||
self.k2v_region.clone(),
|
self.k2v_region.as_rusoto_region(),
|
||||||
self.bucket.clone(),
|
self.bucket.clone(),
|
||||||
aws_creds,
|
aws_creds,
|
||||||
None,
|
None,
|
||||||
|
@ -127,7 +144,7 @@ impl StorageCredentials {
|
||||||
Ok(S3Client::new_with(
|
Ok(S3Client::new_with(
|
||||||
HttpClient::new()?,
|
HttpClient::new()?,
|
||||||
aws_creds_provider,
|
aws_creds_provider,
|
||||||
self.s3_region.clone(),
|
self.s3_region.as_rusoto_region(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rusoto_signature::Region;
|
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::cryptoblob::{Key, SecretKey};
|
use crate::cryptoblob::{Key, SecretKey};
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::convert::TryFrom;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use k2v_client::K2vClient;
|
use k2v_client::K2vClient;
|
||||||
use rusoto_s3::S3Client;
|
use rusoto_s3::S3Client;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::bayou::Bayou;
|
use crate::bayou::Bayou;
|
||||||
use crate::cryptoblob::Key;
|
use crate::cryptoblob::Key;
|
||||||
|
@ -11,16 +12,16 @@ use crate::mail::mail_ident::*;
|
||||||
use crate::mail::uidindex::*;
|
use crate::mail::uidindex::*;
|
||||||
use crate::mail::IMF;
|
use crate::mail::IMF;
|
||||||
|
|
||||||
pub struct Summary<'a> {
|
pub struct Summary {
|
||||||
pub validity: ImapUidvalidity,
|
pub validity: ImapUidvalidity,
|
||||||
pub next: ImapUid,
|
pub next: ImapUid,
|
||||||
pub exists: u32,
|
pub exists: u32,
|
||||||
pub recent: u32,
|
pub recent: u32,
|
||||||
pub flags: FlagIter<'a>,
|
pub flags: Vec<String>,
|
||||||
pub unseen: Option<&'a ImapUid>,
|
pub unseen: Option<ImapUid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Summary<'_> {
|
impl std::fmt::Display for Summary {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
|
@ -30,11 +31,67 @@ impl std::fmt::Display for Summary<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Mailbox(RwLock<MailboxInternal>);
|
||||||
|
|
||||||
|
impl Mailbox {
|
||||||
|
pub(super) async fn open(creds: &Credentials, name: &str) -> Result<Self> {
|
||||||
|
let index_path = format!("index/{}", name);
|
||||||
|
let mail_path = format!("mail/{}", name);
|
||||||
|
|
||||||
|
let mut uid_index = Bayou::<UidIndex>::new(creds, index_path)?;
|
||||||
|
uid_index.sync().await?;
|
||||||
|
|
||||||
|
Ok(Self(RwLock::new(MailboxInternal {
|
||||||
|
bucket: creds.bucket().to_string(),
|
||||||
|
key: creds.keys.master.clone(),
|
||||||
|
k2v: creds.k2v_client()?,
|
||||||
|
s3: creds.s3_client()?,
|
||||||
|
uid_index,
|
||||||
|
mail_path,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a clone of the current UID Index of this mailbox
|
||||||
|
/// (cloning is cheap so don't hesitate to use this)
|
||||||
|
pub async fn current_uid_index(&self) -> UidIndex {
|
||||||
|
self.0.read().await.uid_index.state().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert an email in the mailbox
|
||||||
|
pub async fn append<'a>(&self, _msg: IMF<'a>) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy an email from an other Mailbox to this mailbox
|
||||||
|
/// (use this when possible, as it allows for a certain number of storage optimizations)
|
||||||
|
pub async fn copy(&self, _from: &Mailbox, _uid: ImapUid) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all emails with the \Delete flag in the mailbox
|
||||||
|
/// Can be called by CLOSE and EXPUNGE
|
||||||
|
/// @FIXME do we want to implement this feature or a simpler "delete" command
|
||||||
|
/// The controller could then "fetch \Delete" and call delete on each email?
|
||||||
|
pub async fn expunge(&self) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update flags of a range of emails
|
||||||
|
pub async fn store(&self) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch(&self) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
// Non standard but common flags:
|
// Non standard but common flags:
|
||||||
// https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml
|
// https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml
|
||||||
pub struct Mailbox {
|
struct MailboxInternal {
|
||||||
bucket: String,
|
bucket: String,
|
||||||
pub name: String,
|
|
||||||
key: Key,
|
key: Key,
|
||||||
|
|
||||||
k2v: K2vClient,
|
k2v: K2vClient,
|
||||||
|
@ -44,78 +101,7 @@ pub struct Mailbox {
|
||||||
mail_path: String,
|
mail_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mailbox {
|
impl MailboxInternal {
|
||||||
pub(super) fn new(creds: &Credentials, name: &str) -> Result<Self> {
|
|
||||||
let index_path = format!("index/{}", name);
|
|
||||||
let mail_path = format!("mail/{}", name);
|
|
||||||
let uid_index = Bayou::<UidIndex>::new(creds, index_path)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
bucket: creds.bucket().to_string(),
|
|
||||||
name: name.to_string(), // TODO: don't use name field if possible, use mail_path instead
|
|
||||||
key: creds.keys.master.clone(),
|
|
||||||
k2v: creds.k2v_client()?,
|
|
||||||
s3: creds.s3_client()?,
|
|
||||||
uid_index,
|
|
||||||
mail_path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a summary of the mailbox, useful for the SELECT command for example
|
|
||||||
pub async fn summary(&mut self) -> Result<Summary> {
|
|
||||||
self.uid_index.sync().await?;
|
|
||||||
let state = self.uid_index.state();
|
|
||||||
|
|
||||||
let unseen = state
|
|
||||||
.idx_by_flag
|
|
||||||
.get(&"$unseen".to_string())
|
|
||||||
.and_then(|os| os.get_min());
|
|
||||||
let recent = state
|
|
||||||
.idx_by_flag
|
|
||||||
.get(&"\\Recent".to_string())
|
|
||||||
.map(|os| os.len())
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
return Ok(Summary {
|
|
||||||
validity: state.uidvalidity,
|
|
||||||
next: state.uidnext,
|
|
||||||
exists: u32::try_from(state.idx_by_uid.len())?,
|
|
||||||
recent: u32::try_from(recent)?,
|
|
||||||
flags: state.idx_by_flag.flags(),
|
|
||||||
unseen,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert an email in the mailbox
|
|
||||||
pub async fn append(&mut self, _msg: IMF) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy an email from an other Mailbox to this mailbox
|
|
||||||
/// (use this when possible, as it allows for a certain number of storage optimizations)
|
|
||||||
pub async fn copy(&mut self, _from: &Mailbox, _uid: ImapUid) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete all emails with the \Delete flag in the mailbox
|
|
||||||
/// Can be called by CLOSE and EXPUNGE
|
|
||||||
/// @FIXME do we want to implement this feature or a simpler "delete" command
|
|
||||||
/// The controller could then "fetch \Delete" and call delete on each email?
|
|
||||||
pub async fn expunge(&mut self) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update flags of a range of emails
|
|
||||||
pub async fn store(&mut self) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch(&mut self) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
pub async fn test(&mut self) -> Result<()> {
|
pub async fn test(&mut self) -> Result<()> {
|
||||||
self.uid_index.sync().await?;
|
self.uid_index.sync().await?;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod mail_ident;
|
pub mod mail_ident;
|
||||||
pub mod mailbox;
|
pub mod mailbox;
|
||||||
mod uidindex;
|
pub mod uidindex;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
@ -17,4 +17,7 @@ use crate::mail::uidindex::*;
|
||||||
|
|
||||||
// Internet Message Format
|
// Internet Message Format
|
||||||
// aka RFC 822 - RFC 2822 - RFC 5322
|
// aka RFC 822 - RFC 2822 - RFC 5322
|
||||||
pub struct IMF(Vec<u8>);
|
pub struct IMF<'a> {
|
||||||
|
raw: &'a [u8],
|
||||||
|
parsed: mail_parser::Message<'a>,
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
use k2v_client::K2vClient;
|
use k2v_client::K2vClient;
|
||||||
use rusoto_s3::S3Client;
|
use rusoto_s3::S3Client;
|
||||||
|
|
||||||
use crate::login::Credentials;
|
use crate::login::{Credentials, StorageCredentials};
|
||||||
use crate::mail::mailbox::Mailbox;
|
use crate::mail::mailbox::Mailbox;
|
||||||
|
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
@ -31,8 +35,24 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens an existing mailbox given its IMAP name.
|
/// Opens an existing mailbox given its IMAP name.
|
||||||
pub fn open_mailbox(&self, name: &str) -> Result<Option<Mailbox>> {
|
pub async fn open_mailbox(&self, name: &str) -> Result<Option<Arc<Mailbox>>> {
|
||||||
Mailbox::new(&self.creds, name).map(Some)
|
{
|
||||||
|
let cache = MAILBOX_CACHE.cache.lock().unwrap();
|
||||||
|
if let Some(mb) = cache.get(&self.creds.storage).and_then(Weak::upgrade) {
|
||||||
|
return Ok(Some(mb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mb = Arc::new(Mailbox::open(&self.creds, name).await?);
|
||||||
|
|
||||||
|
let mut cache = MAILBOX_CACHE.cache.lock().unwrap();
|
||||||
|
if let Some(concurrent_mb) = cache.get(&self.creds.storage).and_then(Weak::upgrade) {
|
||||||
|
drop(mb); // we worked for nothing but at least we didn't starve someone else
|
||||||
|
Ok(Some(concurrent_mb))
|
||||||
|
} else {
|
||||||
|
cache.insert(self.creds.storage.clone(), Arc::downgrade(&mb));
|
||||||
|
Ok(Some(mb))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new mailbox in the user's IMAP namespace.
|
/// Creates a new mailbox in the user's IMAP namespace.
|
||||||
|
@ -50,3 +70,21 @@ impl User {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Mailbox cache ----
|
||||||
|
|
||||||
|
struct MailboxCache {
|
||||||
|
cache: std::sync::Mutex<HashMap<StorageCredentials, Weak<Mailbox>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailboxCache {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
cache: std::sync::Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref MAILBOX_CACHE: MailboxCache = MailboxCache::new();
|
||||||
|
}
|
||||||
|
|
|
@ -14,8 +14,6 @@ use anyhow::{bail, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
|
||||||
use rusoto_signature::Region;
|
|
||||||
|
|
||||||
use config::*;
|
use config::*;
|
||||||
use cryptoblob::*;
|
use cryptoblob::*;
|
||||||
use login::{static_provider::*, *};
|
use login::{static_provider::*, *};
|
||||||
|
@ -264,11 +262,11 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_storage_creds(c: StorageCredsArgs) -> StorageCredentials {
|
fn make_storage_creds(c: StorageCredsArgs) -> StorageCredentials {
|
||||||
let s3_region = Region::Custom {
|
let s3_region = Region {
|
||||||
name: c.region.clone(),
|
name: c.region.clone(),
|
||||||
endpoint: c.s3_endpoint,
|
endpoint: c.s3_endpoint,
|
||||||
};
|
};
|
||||||
let k2v_region = Region::Custom {
|
let k2v_region = Region {
|
||||||
name: c.region,
|
name: c.region,
|
||||||
endpoint: c.k2v_endpoint,
|
endpoint: c.k2v_endpoint,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,14 +3,13 @@ use std::sync::Arc;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use futures::{try_join, StreamExt};
|
use futures::{try_join, StreamExt};
|
||||||
use log::*;
|
use log::*;
|
||||||
use rusoto_signature::Region;
|
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::imap;
|
use crate::imap;
|
||||||
use crate::lmtp::*;
|
use crate::lmtp::*;
|
||||||
use crate::login::ArcLoginProvider;
|
use crate::login::ArcLoginProvider;
|
||||||
use crate::login::{ldap_provider::*, static_provider::*};
|
use crate::login::{ldap_provider::*, static_provider::*, Region};
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
lmtp_server: Option<Arc<LmtpServer>>,
|
lmtp_server: Option<Arc<LmtpServer>>,
|
||||||
|
@ -62,11 +61,11 @@ impl Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(config: Config) -> Result<(ArcLoginProvider, Option<LmtpConfig>, Option<ImapConfig>)> {
|
fn build(config: Config) -> Result<(ArcLoginProvider, Option<LmtpConfig>, Option<ImapConfig>)> {
|
||||||
let s3_region = Region::Custom {
|
let s3_region = Region {
|
||||||
name: config.aws_region.clone(),
|
name: config.aws_region.clone(),
|
||||||
endpoint: config.s3_endpoint,
|
endpoint: config.s3_endpoint,
|
||||||
};
|
};
|
||||||
let k2v_region = Region::Custom {
|
let k2v_region = Region {
|
||||||
name: config.aws_region,
|
name: config.aws_region,
|
||||||
endpoint: config.k2v_endpoint,
|
endpoint: config.k2v_endpoint,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue