diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 7e72458..5f982ba 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -5,6 +5,7 @@ use imap_codec::types::core::{AString}; use imap_codec::types::response::{Capability, Data, Status}; use crate::imap::flow; +use crate::mail::user::User; use crate::login::ArcLoginProvider; //--- dispatching @@ -68,10 +69,9 @@ impl<'a> AnonymousContext<'a> { Ok(c) => c, }; - let user = flow::User { - creds, - name: u.clone(), - }; + let s3_client = creds.s3_client(); + let k2v_client = creds.k2v_client(); + let user = User::new(u.clone(), creds)?; tracing::info!(username=%u, "connected"); Ok(( diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 392069f..b79865f 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -9,7 +9,8 @@ use imap_codec::types::response::{Code, Data, Status}; use crate::imap::command::anonymous; use crate::imap::flow; -use crate::mail::Mailbox; +use crate::mail::mailbox::Mailbox; +use crate::mail::user::User; const DEFAULT_FLAGS: [Flag; 5] = [ Flag::Seen, @@ -21,7 +22,7 @@ const DEFAULT_FLAGS: [Flag; 5] = [ pub struct AuthenticatedContext<'a> { pub req: &'a Request, - pub user: &'a flow::User, + pub user: &'a User, } pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, flow::Transition)> { @@ -95,8 +96,8 @@ impl<'a> AuthenticatedContext<'a> { async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { let name = String::try_from(mailbox.clone())?; - let mut mb = Mailbox::new(&self.user.creds, name.clone())?; - tracing::info!(username=%self.user.name, mailbox=%name, "mailbox.selected"); + let mut mb = self.user.open_mailbox(name.clone())?; + tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); let sum = mb.summary().await?; tracing::trace!(summary=%sum, "mailbox.summary"); @@ -112,7 +113,7 @@ impl<'a> AuthenticatedContext<'a> { Some('$') if f == "$unseen" => None, Some(_) => match Atom::try_from(f.clone()) { Err(_) => { - tracing::error!(username=%self.user.name, mailbox=%name, flag=%f, "Unable to encode flag as IMAP atom"); + tracing::error!(username=%self.user.username, mailbox=%name, flag=%f, "Unable to encode flag as IMAP atom"); None }, Ok(a) => Some(Flag::Keyword(a)), diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index d3dddd4..bd46bd5 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -10,11 +10,12 @@ use imap_codec::types::sequence::SequenceSet; use crate::imap::command::authenticated; use crate::imap::flow; -use crate::mail::Mailbox; +use crate::mail::mailbox::Mailbox; +use crate::mail::user::User; pub struct SelectedContext<'a> { pub req: &'a Request, - pub user: &'a flow::User, + pub user: &'a User, pub mailbox: &'a mut Mailbox, } diff --git a/src/imap/flow.rs b/src/imap/flow.rs index 7370bd1..369bee6 100644 --- a/src/imap/flow.rs +++ b/src/imap/flow.rs @@ -1,13 +1,9 @@ use std::error::Error as StdError; use std::fmt; -use crate::login::Credentials; -use crate::mail::Mailbox; +use crate::mail::mailbox::Mailbox; +use crate::mail::user::User; -pub struct User { - pub name: String, - pub creds: Credentials, -} #[derive(Debug)] pub enum Error { diff --git a/src/mail/mailbox.rs b/src/mail/mailbox.rs new file mode 100644 index 0000000..e19dfd8 --- /dev/null +++ b/src/mail/mailbox.rs @@ -0,0 +1,165 @@ +use std::convert::TryFrom; + +use anyhow::Result; +use k2v_client::K2vClient; +use rusoto_s3::S3Client; + +use crate::bayou::Bayou; +use crate::cryptoblob::Key; +use crate::login::Credentials; +use crate::mail::mail_ident::*; +use crate::mail::uidindex::*; +use crate::mail::IMF; + +pub struct Summary<'a> { + pub validity: ImapUidvalidity, + pub next: ImapUid, + pub exists: u32, + pub recent: u32, + pub flags: FlagIter<'a>, + pub unseen: Option<&'a ImapUid>, +} +impl std::fmt::Display for Summary<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "uidvalidity: {}, uidnext: {}, exists: {}", + self.validity, self.next, self.exists + ) + } +} + +// Non standard but common flags: +// https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml +pub struct Mailbox { + bucket: String, + pub name: String, + key: Key, + + k2v: K2vClient, + s3: S3Client, + + uid_index: Bayou, + mail_path: String, +} + +impl Mailbox { + pub(super) fn new(creds: &Credentials, name: String) -> Result { + let index_path = format!("index/{}", name); + let mail_path = format!("mail/{}", name); + let uid_index = Bayou::::new(creds, index_path)?; + + Ok(Self { + bucket: creds.bucket().to_string(), + name, + 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 { + 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<()> { + Ok(()) + } + + // Copy an email from an external to this mailbox + // @FIXME is it needed or could we implement it with append? + pub async fn copy(&mut self, _mailbox: String, _uid: ImapUid) -> Result<()> { + Ok(()) + } + + // 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<()> { + Ok(()) + } + + // Update flags of a range of emails + pub async fn store(&mut self) -> Result<()> { + Ok(()) + } + + pub async fn fetch(&mut self) -> Result<()> { + Ok(()) + } + + pub async fn test(&mut self) -> Result<()> { + self.uid_index.sync().await?; + + dump(&self.uid_index); + + let add_mail_op = self + .uid_index + .state() + .op_mail_add(gen_ident(), vec!["\\Unseen".into()]); + self.uid_index.push(add_mail_op).await?; + + dump(&self.uid_index); + + if self.uid_index.state().idx_by_uid.len() > 6 { + for i in 0..2 { + let (_, ident) = self + .uid_index + .state() + .idx_by_uid + .iter() + .skip(3 + i) + .next() + .unwrap(); + let del_mail_op = self.uid_index.state().op_mail_del(*ident); + self.uid_index.push(del_mail_op).await?; + + dump(&self.uid_index); + } + } + + Ok(()) + } +} + +fn dump(uid_index: &Bayou) { + let s = uid_index.state(); + println!("---- MAILBOX STATE ----"); + println!("UIDVALIDITY {}", s.uidvalidity); + println!("UIDNEXT {}", s.uidnext); + println!("INTERNALSEQ {}", s.internalseq); + for (uid, ident) in s.idx_by_uid.iter() { + println!( + "{} {} {}", + uid, + hex::encode(ident.0), + s.table.get(ident).cloned().unwrap().1.join(", ") + ); + } + println!(""); +} diff --git a/src/mail/mod.rs b/src/mail/mod.rs index a130073..f696f6d 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -1,4 +1,6 @@ pub mod mail_ident; +pub mod user; +pub mod mailbox; mod uidindex; use std::convert::TryFrom; @@ -16,152 +18,3 @@ use crate::mail::uidindex::*; // Internet Message Format // aka RFC 822 - RFC 2822 - RFC 5322 pub struct IMF(Vec); - -pub struct Summary<'a> { - pub validity: ImapUidvalidity, - pub next: ImapUid, - pub exists: u32, - pub recent: u32, - pub flags: FlagIter<'a>, - pub unseen: Option<&'a ImapUid>, -} -impl std::fmt::Display for Summary<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "uidvalidity: {}, uidnext: {}, exists: {}", - self.validity, self.next, self.exists - ) - } -} - -// Non standard but common flags: -// https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml -pub struct Mailbox { - bucket: String, - pub name: String, - key: Key, - - k2v: K2vClient, - s3: S3Client, - - uid_index: Bayou, -} - -impl Mailbox { - pub fn new(creds: &Credentials, name: String) -> Result { - let uid_index = Bayou::::new(creds, name.clone())?; - - Ok(Self { - bucket: creds.bucket().to_string(), - name, - key: creds.keys.master.clone(), - k2v: creds.k2v_client()?, - s3: creds.s3_client()?, - uid_index, - }) - } - - // Get a summary of the mailbox, useful for the SELECT command for example - pub async fn summary(&mut self) -> Result { - 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<()> { - Ok(()) - } - - // Copy an email from an external to this mailbox - // @FIXME is it needed or could we implement it with append? - pub async fn copy(&mut self, _mailbox: String, _uid: ImapUid) -> Result<()> { - Ok(()) - } - - // 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<()> { - Ok(()) - } - - // Update flags of a range of emails - pub async fn store(&mut self) -> Result<()> { - Ok(()) - } - - pub async fn fetch(&mut self) -> Result<()> { - Ok(()) - } - - pub async fn test(&mut self) -> Result<()> { - self.uid_index.sync().await?; - - dump(&self.uid_index); - - let add_mail_op = self - .uid_index - .state() - .op_mail_add(gen_ident(), vec!["\\Unseen".into()]); - self.uid_index.push(add_mail_op).await?; - - dump(&self.uid_index); - - if self.uid_index.state().idx_by_uid.len() > 6 { - for i in 0..2 { - let (_, ident) = self - .uid_index - .state() - .idx_by_uid - .iter() - .skip(3 + i) - .next() - .unwrap(); - let del_mail_op = self.uid_index.state().op_mail_del(*ident); - self.uid_index.push(del_mail_op).await?; - - dump(&self.uid_index); - } - } - - Ok(()) - } -} - -fn dump(uid_index: &Bayou) { - let s = uid_index.state(); - println!("---- MAILBOX STATE ----"); - println!("UIDVALIDITY {}", s.uidvalidity); - println!("UIDNEXT {}", s.uidnext); - println!("INTERNALSEQ {}", s.internalseq); - for (uid, ident) in s.idx_by_uid.iter() { - println!( - "{} {} {}", - uid, - hex::encode(ident.0), - s.table.get(ident).cloned().unwrap().1.join(", ") - ); - } - println!(""); -} diff --git a/src/mail/user.rs b/src/mail/user.rs new file mode 100644 index 0000000..7465ab0 --- /dev/null +++ b/src/mail/user.rs @@ -0,0 +1,31 @@ +use anyhow::Result; + +use k2v_client::K2vClient; +use rusoto_s3::S3Client; + +use crate::login::Credentials; +use crate::mail::mailbox::Mailbox; + +pub struct User { + pub username: String, + pub creds: Credentials, + pub s3_client: S3Client, + pub k2v_client: K2vClient, +} + +impl User { + pub fn new(username: String, creds: Credentials) -> Result { + let s3_client = creds.s3_client()?; + let k2v_client = creds.k2v_client()?; + Ok(Self { + username, + creds, + s3_client, + k2v_client, + }) + } + + pub fn open_mailbox(&self, name: String) -> Result { + Mailbox::new(&self.creds, name) + } +}