diff options
author | Quentin <quentin@dufour.io> | 2024-01-02 22:44:29 +0000 |
---|---|---|
committer | Quentin <quentin@dufour.io> | 2024-01-02 22:44:29 +0000 |
commit | b9a0c1e6eced036eb71e8221a4f236f72832fec2 (patch) | |
tree | c498a7a2a5833f2c6f27d4ba97894747f9d454c0 /src | |
parent | 6ff3c6f71efd802da422a371e6168ae528fb2ddc (diff) | |
parent | c9a33c080d39d4a2b269e3c8f166a708b6606da5 (diff) | |
download | aerogramme-b9a0c1e6eced036eb71e8221a4f236f72832fec2.tar.gz aerogramme-b9a0c1e6eced036eb71e8221a4f236f72832fec2.zip |
Merge pull request 'Implement imap-flow' (#34) from refactor/imap-flow into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/pulls/34
Diffstat (limited to 'src')
-rw-r--r-- | src/bayou.rs | 5 | ||||
-rw-r--r-- | src/config.rs | 1 | ||||
-rw-r--r-- | src/future_rest_admin_api.txt | 174 | ||||
-rw-r--r-- | src/imap/command/anonymous.rs | 91 | ||||
-rw-r--r-- | src/imap/command/anystate.rs | 52 | ||||
-rw-r--r-- | src/imap/command/authenticated.rs | 400 | ||||
-rw-r--r-- | src/imap/command/examined.rs | 142 | ||||
-rw-r--r-- | src/imap/command/mod.rs | 17 | ||||
-rw-r--r-- | src/imap/command/selected.rs | 161 | ||||
-rw-r--r-- | src/imap/flow.rs | 24 | ||||
-rw-r--r-- | src/imap/mailbox_view.rs | 493 | ||||
-rw-r--r-- | src/imap/mod.rs | 223 | ||||
-rw-r--r-- | src/imap/response.rs | 112 | ||||
-rw-r--r-- | src/imap/session.rs | 224 | ||||
-rw-r--r-- | src/login/demo_provider.rs | 51 | ||||
-rw-r--r-- | src/login/mod.rs | 1 | ||||
-rw-r--r-- | src/main.rs | 26 | ||||
-rw-r--r-- | src/server.rs | 7 |
18 files changed, 1278 insertions, 926 deletions
diff --git a/src/bayou.rs b/src/bayou.rs index 7253a30..c6a7ac0 100644 --- a/src/bayou.rs +++ b/src/bayou.rs @@ -450,10 +450,7 @@ impl K2vWatch { ) { let mut row = match Weak::upgrade(&self_weak) { Some(this) => this.target.clone(), - None => { - error!("can't start loop"); - return; - } + None => return, }; while let Some(this) = Weak::upgrade(&self_weak) { diff --git a/src/config.rs b/src/config.rs index 1438910..b9c1f09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,6 +26,7 @@ pub struct ProviderConfig { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "user_driver")] pub enum UserManagement { + Demo, Static(LoginStaticConfig), Ldap(LoginLdapConfig), } diff --git a/src/future_rest_admin_api.txt b/src/future_rest_admin_api.txt deleted file mode 100644 index 19ece27..0000000 --- a/src/future_rest_admin_api.txt +++ /dev/null @@ -1,174 +0,0 @@ - Command::FirstLogin { - creds, - user_secrets, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - println!("Please enter your password for key decryption."); - println!("If you are using LDAP login, this must be your LDAP password."); - println!("If you are using the static login provider, enter any password, and this will also become your password for local IMAP access."); - let password = rpassword::prompt_password("Enter password: ")?; - let password_confirm = rpassword::prompt_password("Confirm password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - - CryptoKeys::init(&creds, &user_secrets, &password).await?; - - println!(""); - println!("Cryptographic key setup is complete."); - println!(""); - println!("If you are using the static login provider, add the following section to your .toml configuration file:"); - println!(""); - dump_config(&password, &creds); - } - Command::InitializeLocalKeys { creds } => { - let creds = make_storage_creds(creds); - - println!("Please enter a password for local IMAP access."); - println!("This password is not used for key decryption, your keys will be printed below (do not lose them!)"); - println!( - "If you plan on using LDAP login, stop right here and use `first-login` instead" - ); - let password = rpassword::prompt_password("Enter password: ")?; - let password_confirm = rpassword::prompt_password("Confirm password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - - let master = gen_key(); - let (_, secret) = gen_keypair(); - let keys = CryptoKeys::init_without_password(&creds, &master, &secret).await?; - - println!(""); - println!("Cryptographic key setup is complete."); - println!(""); - println!("Add the following section to your .toml configuration file:"); - println!(""); - dump_config(&password, &creds); - dump_keys(&keys); - } - Command::AddPassword { - creds, - user_secrets, - gen, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - let existing_password = - rpassword::prompt_password("Enter existing password to decrypt keys: ")?; - let new_password = if gen { - let password = base64::encode_config( - &u128::to_be_bytes(thread_rng().gen())[..10], - base64::URL_SAFE_NO_PAD, - ); - println!("Your new password: {}", password); - println!("Keep it safe!"); - password - } else { - let password = rpassword::prompt_password("Enter new password: ")?; - let password_confirm = rpassword::prompt_password("Confirm new password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - password - }; - - let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; - keys.add_password(&creds, &user_secrets, &new_password) - .await?; - println!(""); - println!("New password added successfully."); - } - Command::DeletePassword { - creds, - user_secrets, - allow_delete_all, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - let existing_password = rpassword::prompt_password("Enter password to delete: ")?; - - let keys = match allow_delete_all { - true => Some(CryptoKeys::open(&creds, &user_secrets, &existing_password).await?), - false => None, - }; - - CryptoKeys::delete_password(&creds, &existing_password, allow_delete_all).await?; - - println!(""); - println!("Password was deleted successfully."); - - if let Some(keys) = keys { - println!("As a reminder, here are your cryptographic keys:"); - dump_keys(&keys); - } - } - Command::ShowKeys { - creds, - user_secrets, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - let existing_password = rpassword::prompt_password("Enter key decryption password: ")?; - - let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; - dump_keys(&keys); - } - } - - Ok(()) -} - -fn make_storage_creds(c: StorageCredsArgs) -> StorageCredentials { - let s3_region = Region { - name: c.region.clone(), - endpoint: c.s3_endpoint, - }; - let k2v_region = Region { - name: c.region, - endpoint: c.k2v_endpoint, - }; - StorageCredentials { - k2v_region, - s3_region, - aws_access_key_id: c.aws_access_key_id, - aws_secret_access_key: c.aws_secret_access_key, - bucket: c.bucket, - } -} - -fn make_user_secrets(c: UserSecretsArgs) -> UserSecrets { - UserSecrets { - user_secret: c.user_secret, - alternate_user_secrets: c - .alternate_user_secrets - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|x| x.to_string()) - .collect(), - } -} - -fn dump_config(password: &str, creds: &StorageCredentials) { - println!("[login_static.users.<username>]"); - println!( - "password = \"{}\"", - hash_password(password).expect("unable to hash password") - ); - println!("aws_access_key_id = \"{}\"", creds.aws_access_key_id); - println!( - "aws_secret_access_key = \"{}\"", - creds.aws_secret_access_key - ); -} - -fn dump_keys(keys: &CryptoKeys) { - println!("master_key = \"{}\"", base64::encode(&keys.master)); - println!("secret_key = \"{}\"", base64::encode(&keys.secret)); -} diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index d258bd3..fbd10e9 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,92 +1,77 @@ -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::AString; -use imap_codec::types::response::{Capability, Data, Status}; +use anyhow::Result; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::AString; +use imap_codec::imap_types::secret::Secret; +use crate::imap::command::anystate; use crate::imap::flow; +use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; //--- dispatching pub struct AnonymousContext<'a> { - pub req: &'a Request, - pub login_provider: Option<&'a ArcLoginProvider>, + pub req: &'a Command<'static>, + pub login_provider: &'a ArcLoginProvider, } -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { - CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)), - CommandBody::Capability => ctx.capability().await, - CommandBody::Logout => ctx.logout().await, +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()), + CommandBody::Logout => anystate::logout(), + + // Specific to anonymous context (3 commands) CommandBody::Login { username, password } => ctx.login(username, password).await, - _ => Ok((Response::no("Command unavailable")?, flow::Transition::None)), + 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 capability(self) -> Result<(Response, flow::Transition)> { - let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; - let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities)); - Ok((res, flow::Transition::None)) - } - async fn login( self, - username: &AString, - password: &AString, - ) -> Result<(Response, flow::Transition)> { + username: &AString<'a>, + password: &Secret<AString<'a>>, + ) -> Result<(Response<'static>, flow::Transition)> { let (u, p) = ( - String::try_from(username.clone())?, - String::try_from(password.clone())?, + std::str::from_utf8(username.as_ref())?, + std::str::from_utf8(password.declassify().as_ref())?, ); tracing::info!(user = %u, "command.login"); - let login_provider = match &self.login_provider { - Some(lp) => lp, - None => { - return Ok(( - Response::no("Login command not available (already logged in)")?, - flow::Transition::None, - )) - } - }; - - let creds = match login_provider.login(&u, &p).await { + let creds = match self.login_provider.login(&u, &p).await { Err(e) => { tracing::debug!(error=%e, "authentication failed"); return Ok(( - Response::no("Authentication failed")?, + Response::build() + .to_req(self.req) + .message("Authentication failed") + .no()?, flow::Transition::None, )); } Ok(c) => c, }; - let user = User::new(u.clone(), creds).await?; + let user = User::new(u.to_string(), creds).await?; tracing::info!(username=%u, "connected"); Ok(( - Response::ok("Completed")?, + Response::build() + .to_req(self.req) + .message("Completed") + .ok()?, flow::Transition::Authenticate(user), )) } - - // C: 10 logout - // S: * BYE Logging out - // S: 10 OK Logout completed. - async fn logout(self) -> Result<(Response, flow::Transition)> { - // @FIXME we should implement From<Vec<Status>> and From<Vec<ImapStatus>> in - // boitalettres/src/proto/res/body.rs - Ok(( - Response::ok("Logout completed")?.with_body(vec![Body::Status( - Status::bye(None, "Logging out") - .map_err(|e| Error::msg(e).context("Unable to generate IMAP status"))?, - )]), - flow::Transition::Logout, - )) - } } diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs new file mode 100644 index 0000000..42fe645 --- /dev/null +++ b/src/imap/command/anystate.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use imap_codec::imap_types::core::{NonEmptyVec, Tag}; +use imap_codec::imap_types::response::{Capability, Data}; + +use crate::imap::flow; +use crate::imap::response::Response; + +pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { + let capabilities: NonEmptyVec<Capability> = + (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; + let res = Response::build() + .tag(tag) + .message("Server capabilities") + .data(Data::Capability(capabilities)) + .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 index 2deb723..1bb4c6d 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -2,37 +2,42 @@ use std::collections::BTreeMap; use std::sync::Arc; 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::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}; - -use crate::imap::command::anonymous; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::{Atom, Literal, QuotedChar}; +use imap_codec::imap_types::datetime::DateTime; +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::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; -use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER}; +use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; use crate::mail::IMF; pub struct AuthenticatedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'static>, pub user: &'a Arc<User>, } -pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { +pub async fn dispatch<'a>( + 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()), + 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 { - mailbox, - new_mailbox, - } => ctx.rename(mailbox, new_mailbox).await, + CommandBody::Rename { from, to } => ctx.rename(from, to).await, CommandBody::Lsub { reference, mailbox_wildcard, @@ -43,8 +48,8 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: } => ctx.list(reference, mailbox_wildcard, false).await, CommandBody::Status { mailbox, - attributes, - } => ctx.status(mailbox, attributes).await, + 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 } => ctx.select(mailbox).await, @@ -55,90 +60,148 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: date, message, } => ctx.append(mailbox, flags, date, message).await, - _ => { - let ctx = anonymous::AnonymousContext { - req: ctx.req, - login_provider: None, - }; - anonymous::dispatch(ctx).await - } + + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), } } // --- PRIVATE --- - impl<'a> AuthenticatedContext<'a> { - async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - - if name == INBOX { - return Ok(( - Response::bad("Cannot create INBOX")?, - flow::Transition::None, - )); - } + 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::ok("CREATE complete")?, flow::Transition::None)), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + 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) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + 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::ok("DELETE complete")?, flow::Transition::None)), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + 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, - mailbox: &MailboxCodec, - new_mailbox: &MailboxCodec, - ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - let new_name = String::try_from(new_mailbox.clone())?; + 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::ok("RENAME complete")?, flow::Transition::None)), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + 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( self, - reference: &MailboxCodec, - mailbox_wildcard: &ListMailbox, + reference: &MailboxCodec<'a>, + mailbox_wildcard: &ListMailbox<'a>, is_lsub: bool, - ) -> Result<(Response, flow::Transition)> { - let reference = String::try_from(reference.clone())?; + ) -> 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::bad("References not supported")?, + Response::build() + .to_req(self.req) + .message("References not supported") + .bad()?, flow::Transition::None, )); } - let wildcard = String::try_from(mailbox_wildcard.clone())?; + // @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::ok("LSUB complete")?.with_body(vec![Data::Lsub { - items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), - mailbox: "".try_into().unwrap(), - }]), + 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::ok("LIST complete")?.with_body(vec![Data::List { - items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), - mailbox: "".try_into().unwrap(), - }]), + 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, )); } @@ -147,7 +210,7 @@ impl<'a> AuthenticatedContext<'a> { let mailboxes = self.user.list_mailboxes().await?; let mut vmailboxes = BTreeMap::new(); for mb in mailboxes.iter() { - for (i, _) in mb.match_indices(MAILBOX_HIERARCHY_DELIMITER) { + for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) { if i > 0 { let smb = &mb[..i]; vmailboxes.entry(smb).or_insert(false); @@ -163,22 +226,22 @@ impl<'a> AuthenticatedContext<'a> { .to_string() .try_into() .map_err(|_| anyhow!("invalid mailbox name"))?; - let mut items = vec![FlagNameAttribute::Extension( - "Subscribed".try_into().unwrap(), - )]; + let mut items = vec![FlagNameAttribute::try_from(Atom::unvalidated( + "Subscribed", + ))?]; if !*is_real { items.push(FlagNameAttribute::Noselect); } if is_lsub { ret.push(Data::Lsub { items, - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox, }); } else { ret.push(Data::List { items, - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox, }); } @@ -190,79 +253,120 @@ impl<'a> AuthenticatedContext<'a> { } else { "LIST completed" }; - Ok((Response::ok(msg)?.with_body(ret), flow::Transition::None)) + Ok(( + Response::build() + .to_req(self.req) + .message(msg) + .many_data(ret) + .ok()?, + flow::Transition::None, + )) } async fn status( self, - mailbox: &MailboxCodec, - attributes: &[StatusAttribute], - ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - let mb_opt = self.user.open_mailbox(&name).await?; + mailbox: &MailboxCodec<'static>, + attributes: &[StatusDataItemName], + ) -> Result<(Response<'static>, flow::Transition)> { + 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::no("Mailbox does not exist")?, + Response::build() + .to_req(self.req) + .message("Mailbox does not exist") + .no()?, flow::Transition::None, )) } }; - let (view, _data) = MailboxView::new(mb).await?; + let view = MailboxView::new(mb).await; let mut ret_attrs = vec![]; for attr in attributes.iter() { ret_attrs.push(match attr { - StatusAttribute::Messages => StatusAttributeValue::Messages(view.exists()?), - StatusAttribute::Unseen => StatusAttributeValue::Unseen(view.unseen_count() as u32), - StatusAttribute::Recent => StatusAttributeValue::Recent(view.recent()?), - StatusAttribute::UidNext => StatusAttributeValue::UidNext(view.uidnext()), - StatusAttribute::UidValidity => { - StatusAttributeValue::UidValidity(view.uidvalidity()) + 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"); + }, }); } - let data = vec![Body::Data(Data::Status { + let data = Data::Status { mailbox: mailbox.clone(), - attributes: ret_attrs, - })]; + items: ret_attrs.into(), + }; Ok(( - Response::ok("STATUS completed")?.with_body(data), + Response::build() + .to_req(self.req) + .message("STATUS completed") + .data(data) + .ok()?, flow::Transition::None, )) } - async fn subscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + 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::ok("SUBSCRIBE complete")?, flow::Transition::None)) + Ok(( + Response::build() + .to_req(self.req) + .message("SUBSCRIBE complete") + .ok()?, + flow::Transition::None, + )) } else { Ok(( - Response::bad(&format!("Mailbox {} does not exist", name))?, + Response::build() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .bad()?, flow::Transition::None, )) } } - async fn unsubscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + 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::bad(&format!( - "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", - name - ))?, + Response::build() + .to_req(self.req) + .message(format!( + "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", + name + )) + .bad()?, flow::Transition::None, )) } else { Ok(( - Response::bad(&format!("Mailbox {} does not exist", name))?, + Response::build() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .no()?, flow::Transition::None, )) } @@ -301,83 +405,113 @@ impl<'a> AuthenticatedContext<'a> { * TRACE END --- */ - async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn select( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + 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::no("Mailbox does not exist")?, + 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, data) = MailboxView::new(mb).await?; + let mb = MailboxView::new(mb).await; + let data = mb.summary()?; Ok(( - Response::ok("Select completed")? - .with_extra_code(Code::ReadWrite) - .with_body(data), + Response::build() + .message("Select completed") + .to_req(self.req) + .code(Code::ReadWrite) + .set_body(data) + .ok()?, flow::Transition::Select(mb), )) } - async fn examine(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn examine( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + 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::no("Mailbox does not exist")?, + 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, data) = MailboxView::new(mb).await?; + let mb = MailboxView::new(mb).await; + let data = mb.summary()?; Ok(( - Response::ok("Examine completed")? - .with_extra_code(Code::ReadOnly) - .with_body(data), + Response::build() + .to_req(self.req) + .message("Examine completed") + .code(Code::ReadOnly) + .set_body(data) + .ok()?, flow::Transition::Examine(mb), )) } async fn append( self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option<MyDateTime>, - message: &NonZeroBytes, - ) -> Result<(Response, flow::Transition)> { + 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, uidvalidity, uid)) => Ok(( - Response::ok("APPEND completed")?.with_extra_code(Code::Other( - "APPENDUID".try_into().unwrap(), - Some(format!("{} {}", uidvalidity, uid)), - )), + 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, )), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), } } pub(crate) async fn append_internal( self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option<MyDateTime>, - message: &NonZeroBytes, + mailbox: &MailboxCodec<'a>, + flags: &[Flag<'a>], + date: &Option<DateTime>, + message: &Literal<'a>, ) -> Result<(Arc<Mailbox>, ImapUidvalidity, ImapUidvalidity)> { - let name = String::try_from(mailbox.clone())?; + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; let mb = match mb_opt { @@ -389,8 +523,8 @@ impl<'a> AuthenticatedContext<'a> { 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 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 @@ -422,7 +556,7 @@ fn matches_wildcard(wildcard: &str, name: &str) -> bool { && j > 0 && matches[i - 1][j] && (wildcard[j - 1] == '*' - || (wildcard[j - 1] == '%' && name[i - 1] != MAILBOX_HIERARCHY_DELIMITER))); + || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW))); } } diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 1740b39..7de94f4 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -1,56 +1,60 @@ use std::sync::Arc; use anyhow::Result; -use boitalettres::proto::Request; -use boitalettres::proto::Response; -use imap_codec::types::command::{CommandBody, SearchKey}; -use imap_codec::types::core::{Charset, NonZeroBytes}; -use imap_codec::types::datetime::MyDateTime; -use imap_codec::types::fetch_attributes::MacroOrFetchAttributes; -use imap_codec::types::flag::Flag; -use imap_codec::types::mailbox::Mailbox as MailboxCodec; -use imap_codec::types::response::Code; -use imap_codec::types::sequence::SequenceSet; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::Charset; +use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; +use imap_codec::imap_types::search::SearchKey; +use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::command::authenticated; +use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::user::User; pub struct ExaminedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'static>, pub user: &'a Arc<User>, pub mailbox: &'a mut MailboxView, } -pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { - // CLOSE in examined state is not the same as in selected state - // (in selected state it also does an EXPUNGE, here it doesn't) +pub async fn dispatch(ctx: ExaminedContext<'_>) -> 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()), + CommandBody::Logout => anystate::logout(), + + // Specific to the EXAMINE state (specialization of the SELECTED state) + // ~3 commands -> close, fetch, search + NOOP CommandBody::Close => ctx.close().await, CommandBody::Fetch { sequence_set, - attributes, + macro_or_item_names, uid, - } => ctx.fetch(sequence_set, attributes, uid).await, + } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, CommandBody::Search { charset, criteria, 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, + CommandBody::Noop | CommandBody::Check => ctx.noop().await, + CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok(( + Response::build() + .to_req(ctx.req) + .message("Forbidden command: can't write in read-only mode (EXAMINE)") + .bad()?, + flow::Transition::None, + )), + + // In examined mode, we fallback to authenticated when needed _ => { - let ctx = authenticated::AuthenticatedContext { + authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, user: ctx.user, - }; - authenticated::dispatch(ctx).await + }) + .await } } } @@ -58,71 +62,69 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Trans // --- PRIVATE --- impl<'a> ExaminedContext<'a> { - async fn close(self) -> Result<(Response, flow::Transition)> { - Ok((Response::ok("CLOSE completed")?, 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 close(self) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .to_req(self.req) + .message("CLOSE completed") + .ok()?, + flow::Transition::Unselect, + )) } pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &MacroOrFetchAttributes, + attributes: &'a MacroOrMessageDataItemNames<'static>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( - Response::ok("FETCH completed")?.with_body(resp), + 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, )), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), } } pub async fn search( self, - _charset: &Option<Charset>, - _criteria: &SearchKey, + _charset: &Option<Charset<'a>>, + _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + ) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .to_req(self.req) + .message("Not implemented") + .bad()?, + flow::Transition::None, + )) } - pub async fn noop(self) -> Result<(Response, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; Ok(( - Response::ok("NOOP completed.")?.with_body(updates), + Response::build() + .to_req(self.req) + .message("NOOP completed.") + .set_body(updates) + .ok()?, flow::Transition::None, )) } - - async fn append( - self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option<MyDateTime>, - 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/command/mod.rs b/src/imap/command/mod.rs index 0b7e576..dc95746 100644 --- a/src/imap/command/mod.rs +++ b/src/imap/command/mod.rs @@ -1,4 +1,21 @@ pub mod anonymous; +pub mod anystate; pub mod authenticated; pub mod examined; pub mod selected; + +use crate::mail::user::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 index 90a00ee..220a952 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,31 +1,50 @@ use std::sync::Arc; use anyhow::Result; -use boitalettres::proto::Request; -use boitalettres::proto::Response; -use imap_codec::types::command::CommandBody; -use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::types::mailbox::Mailbox as MailboxCodec; -use imap_codec::types::response::Code; -use imap_codec::types::sequence::SequenceSet; - -use crate::imap::command::examined; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::Charset; +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::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::user::User; pub struct SelectedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'static>, pub user: &'a Arc<User>, pub mailbox: &'a mut MailboxView, } -pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { - // Only write commands here, read commands are handled in - // `examined.rs` +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()), + CommandBody::Logout => anystate::logout(), + + // Specific to this state (7 commands + NOOP) CommandBody::Close => ctx.close().await, + CommandBody::Noop | CommandBody::Check => ctx.noop().await, + CommandBody::Fetch { + sequence_set, + macro_or_item_names, + uid, + } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, + CommandBody::Search { + charset, + criteria, + uid, + } => ctx.search(charset, criteria, uid).await, CommandBody::Expunge => ctx.expunge().await, CommandBody::Store { sequence_set, @@ -39,13 +58,14 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans mailbox, uid, } => ctx.copy(sequence_set, mailbox, uid).await, + + // In selected mode, we fallback to authenticated when needed _ => { - let ctx = examined::ExaminedContext { + authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, user: ctx.user, - mailbox: ctx.mailbox, - }; - examined::dispatch(ctx).await + }) + .await } } } @@ -53,18 +73,81 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans // --- PRIVATE --- impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response, flow::Transition)> { + 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().await?; - Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) + Ok(( + Response::build().tag(tag).message("CLOSE completed").ok()?, + flow::Transition::Unselect, + )) + } + + pub async fn fetch( + self, + sequence_set: &SequenceSet, + attributes: &'a MacroOrMessageDataItemNames<'static>, + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + match self.mailbox.fetch(sequence_set, attributes, uid).await { + Ok(resp) => 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)> { + Ok(( + Response::build() + .to_req(self.req) + .message("Not implemented") + .bad()?, + flow::Transition::None, + )) } - async fn expunge(self) -> Result<(Response, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { + self.mailbox.mailbox.force_sync().await?; + + let updates = self.mailbox.update().await?; + Ok(( + Response::build() + .to_req(self.req) + .message("NOOP completed.") + .set_body(updates) + .ok()?, + flow::Transition::None, + )) + } + + async fn expunge(self) -> Result<(Response<'static>, flow::Transition)> { + let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; Ok(( - Response::ok("EXPUNGE completed")?.with_body(data), + Response::build() + .tag(tag) + .message("EXPUNGE completed") + .set_body(data) + .ok()?, flow::Transition::None, )) } @@ -74,16 +157,20 @@ impl<'a> SelectedContext<'a> { sequence_set: &SequenceSet, kind: &StoreType, response: &StoreResponse, - flags: &[Flag], + flags: &[Flag<'a>], uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let data = self .mailbox .store(sequence_set, kind, response, flags, uid) .await?; Ok(( - Response::ok("STORE completed")?.with_body(data), + Response::build() + .to_req(self.req) + .message("STORE completed") + .set_body(data) + .ok()?, flow::Transition::None, )) } @@ -91,18 +178,21 @@ impl<'a> SelectedContext<'a> { async fn copy( self, sequence_set: &SequenceSet, - mailbox: &MailboxCodec, + mailbox: &MailboxCodec<'a>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + ) -> Result<(Response<'static>, flow::Transition)> { + 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::no("Destination mailbox does not exist")? - .with_extra_code(Code::TryCreate), + Response::build() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .no()?, flow::Transition::None, )) } @@ -126,10 +216,13 @@ impl<'a> SelectedContext<'a> { ); Ok(( - Response::ok("COPY completed")?.with_extra_code(Code::Other( - "COPYUID".try_into().unwrap(), - Some(copyuid_str), - )), + Response::build() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .ok()?, flow::Transition::None, )) } diff --git a/src/imap/flow.rs b/src/imap/flow.rs index eb94bb5..95810c1 100644 --- a/src/imap/flow.rs +++ b/src/imap/flow.rs @@ -37,23 +37,27 @@ pub enum Transition { // See RFC3501 section 3. // https://datatracker.ietf.org/doc/html/rfc3501#page-13 impl State { - pub fn apply(self, tr: Transition) -> Result<Self, Error> { - match (self, tr) { - (s, Transition::None) => Ok(s), - (State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)), + pub fn apply(&mut self, tr: Transition) -> Result<(), Error> { + let new_state = match (&self, tr) { + (_s, Transition::None) => return Ok(()), + (State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u), ( State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _), Transition::Select(m), - ) => Ok(State::Selected(u, m)), + ) => State::Selected(u.clone(), m), ( State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _), Transition::Examine(m), - ) => Ok(State::Examined(u, m)), + ) => State::Examined(u.clone(), m), (State::Selected(u, _) | State::Examined(u, _), Transition::Unselect) => { - Ok(State::Authenticated(u)) + State::Authenticated(u.clone()) } - (_, Transition::Logout) => Ok(State::Logout), - _ => Err(Error::ForbiddenTransition), - } + (_, Transition::Logout) => State::Logout, + _ => return Err(Error::ForbiddenTransition), + }; + + *self = new_state; + + Ok(()) } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 99069e2..7434512 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -4,22 +4,20 @@ use std::num::NonZeroU32; use std::sync::Arc; use anyhow::{anyhow, bail, Error, Result}; -use boitalettres::proto::res::body::Data as Body; use chrono::{Offset, TimeZone, Utc}; use futures::stream::{FuturesOrdered, StreamExt}; -use imap_codec::types::address::Address; -use imap_codec::types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields}; -use imap_codec::types::core::{AString, Atom, IString, NString}; -use imap_codec::types::datetime::MyDateTime; -use imap_codec::types::envelope::Envelope; -use imap_codec::types::fetch_attributes::{ - FetchAttribute, MacroOrFetchAttributes, Section as FetchSection, +use imap_codec::imap_types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields}; +use imap_codec::imap_types::core::{AString, Atom, IString, NString, NonEmptyVec}; +use imap_codec::imap_types::datetime::DateTime; +use imap_codec::imap_types::envelope::{Address, Envelope}; +use imap_codec::imap_types::fetch::{ + MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName, Section as FetchSection, }; -use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::types::response::{Code, Data, MessageAttribute, Status}; -use imap_codec::types::sequence::{self, SequenceSet}; +use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; +use imap_codec::imap_types::response::{Code, Data, Status}; +use imap_codec::imap_types::sequence::{self, SequenceSet}; use eml_codec::{ header, imf, mime, @@ -28,6 +26,7 @@ use eml_codec::{ }; use crate::cryptoblob::Key; +use crate::imap::response::Body; use crate::mail::mailbox::{MailMeta, Mailbox}; use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex}; use crate::mail::unique_ident::UniqueIdent; @@ -77,19 +76,31 @@ impl<'a> FetchedMail<'a> { } pub struct AttributesProxy { - attrs: Vec<FetchAttribute>, + attrs: Vec<MessageDataItemName<'static>>, } impl AttributesProxy { - fn new(attrs: &MacroOrFetchAttributes, is_uid_fetch: bool) -> Self { + fn new(attrs: &MacroOrMessageDataItemNames<'static>, is_uid_fetch: bool) -> Self { // Expand macros let mut fetch_attrs = match attrs { - MacroOrFetchAttributes::Macro(m) => m.expand(), - MacroOrFetchAttributes::FetchAttributes(a) => a.clone(), + 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(&FetchAttribute::Uid) { - fetch_attrs.push(FetchAttribute::Uid); + if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) { + fetch_attrs.push(MessageDataItemName::Uid); } Self { attrs: fetch_attrs } @@ -99,11 +110,11 @@ impl AttributesProxy { self.attrs.iter().any(|x| { matches!( x, - FetchAttribute::Body - | FetchAttribute::BodyExt { .. } - | FetchAttribute::Rfc822 - | FetchAttribute::Rfc822Text - | FetchAttribute::BodyStructure + MessageDataItemName::Body + | MessageDataItemName::BodyExt { .. } + | MessageDataItemName::Rfc822 + | MessageDataItemName::Rfc822Text + | MessageDataItemName::BodyStructure ) }) } @@ -127,16 +138,20 @@ pub struct MailView<'a> { meta: &'a MailMeta, flags: &'a Vec<String>, content: FetchedMail<'a>, - add_seen: bool, +} + +enum SeenFlag { + DoNothing, + MustAdd, } impl<'a> MailView<'a> { - fn uid(&self) -> MessageAttribute { - MessageAttribute::Uid(self.ids.uid) + fn uid(&self) -> MessageDataItem<'static> { + MessageDataItem::Uid(self.ids.uid.clone()) } - fn flags(&self) -> MessageAttribute { - MessageAttribute::Flags( + fn flags(&self) -> MessageDataItem<'static> { + MessageDataItem::Flags( self.flags .iter() .filter_map(|f| string_to_flag(f)) @@ -144,12 +159,12 @@ impl<'a> MailView<'a> { ) } - fn rfc_822_size(&self) -> MessageAttribute { - MessageAttribute::Rfc822Size(self.meta.rfc822_size as u32) + fn rfc_822_size(&self) -> MessageDataItem<'static> { + MessageDataItem::Rfc822Size(self.meta.rfc822_size as u32) } - fn rfc_822_header(&self) -> MessageAttribute { - MessageAttribute::Rfc822Header(NString( + fn rfc_822_header(&self) -> MessageDataItem<'static> { + MessageDataItem::Rfc822Header(NString( self.meta .headers .to_vec() @@ -159,41 +174,42 @@ impl<'a> MailView<'a> { )) } - fn rfc_822_text(&self) -> Result<MessageAttribute> { - Ok(MessageAttribute::Rfc822Text(NString( + fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> { + Ok(MessageDataItem::Rfc822Text(NString( self.content .as_full()? .raw_body + .to_vec() .try_into() .ok() .map(IString::Literal), ))) } - fn rfc822(&self) -> Result<MessageAttribute> { - Ok(MessageAttribute::Rfc822(NString( + fn rfc822(&self) -> Result<MessageDataItem<'static>> { + Ok(MessageDataItem::Rfc822(NString( self.content .as_full()? - .raw_body - .clone() + .raw_part + .to_vec() .try_into() .ok() .map(IString::Literal), ))) } - fn envelope(&self) -> MessageAttribute { - MessageAttribute::Envelope(message_envelope(self.content.imf())) + fn envelope(&self) -> MessageDataItem<'static> { + MessageDataItem::Envelope(message_envelope(self.content.imf().clone())) } - fn body(&self) -> Result<MessageAttribute> { - Ok(MessageAttribute::Body(build_imap_email_struct( + fn body(&self) -> Result<MessageDataItem<'static>> { + Ok(MessageDataItem::Body(build_imap_email_struct( self.content.as_full()?.child.as_ref(), )?)) } - fn body_structure(&self) -> Result<MessageAttribute> { - Ok(MessageAttribute::Body(build_imap_email_struct( + fn body_structure(&self) -> Result<MessageDataItem<'static>> { + Ok(MessageDataItem::Body(build_imap_email_struct( self.content.as_full()?.child.as_ref(), )?)) } @@ -202,12 +218,14 @@ impl<'a> MailView<'a> { /// peek does not implicitly set the \Seen flag /// eg. BODY[HEADER.FIELDS (DATE FROM)] /// eg. BODY[]<0.2048> - fn body_ext( - &mut self, - section: &Option<FetchSection>, + fn body_ext<'b>( + &self, + section: &Option<FetchSection<'b>>, partial: &Option<(u32, NonZeroU32)>, peek: &bool, - ) -> Result<MessageAttribute> { + ) -> Result<(MessageDataItem<'b>, SeenFlag)> { + let mut seen = SeenFlag::DoNothing; + // Extract message section let text = get_message_section(self.content.as_anypart()?, section)?; @@ -215,7 +233,7 @@ impl<'a> MailView<'a> { if !peek && !self.flags.iter().any(|x| *x == seen_flag) { // Add \Seen flag //self.mailbox.add_flags(uuid, &[seen_flag]).await?; - self.add_seen = true; + seen = SeenFlag::MustAdd; } // Handle <<partial>> which cut the message bytes @@ -223,49 +241,60 @@ impl<'a> MailView<'a> { let data = NString(text.to_vec().try_into().ok().map(IString::Literal)); - return Ok(MessageAttribute::BodyExt { - section: section.clone(), - origin, - data, - }); + return Ok(( + MessageDataItem::BodyExt { + section: section.as_ref().map(|fs| fs.clone()), + origin, + data, + }, + seen, + )); } - fn internal_date(&self) -> Result<MessageAttribute> { + fn internal_date(&self) -> Result<MessageDataItem<'static>> { let dt = Utc .fix() .timestamp_opt(i64::try_from(self.meta.internaldate / 1000)?, 0) .earliest() .ok_or(anyhow!("Unable to parse internal date"))?; - Ok(MessageAttribute::InternalDate(MyDateTime(dt))) + Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) } - fn filter(&mut self, ap: &AttributesProxy) -> Result<Body> { + fn filter<'b>(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { + let mut seen = SeenFlag::DoNothing; let res_attrs = ap .attrs .iter() .map(|attr| match attr { - FetchAttribute::Uid => Ok(self.uid()), - FetchAttribute::Flags => Ok(self.flags()), - FetchAttribute::Rfc822Size => Ok(self.rfc_822_size()), - FetchAttribute::Rfc822Header => Ok(self.rfc_822_header()), - FetchAttribute::Rfc822Text => self.rfc_822_text(), - FetchAttribute::Rfc822 => self.rfc822(), - FetchAttribute::Envelope => Ok(self.envelope()), - FetchAttribute::Body => self.body(), - FetchAttribute::BodyStructure => self.body_structure(), - FetchAttribute::BodyExt { + MessageDataItemName::Uid => Ok(self.uid()), + MessageDataItemName::Flags => Ok(self.flags()), + MessageDataItemName::Rfc822Size => Ok(self.rfc_822_size()), + MessageDataItemName::Rfc822Header => Ok(self.rfc_822_header()), + MessageDataItemName::Rfc822Text => self.rfc_822_text(), + MessageDataItemName::Rfc822 => self.rfc822(), + MessageDataItemName::Envelope => Ok(self.envelope()), + MessageDataItemName::Body => self.body(), + MessageDataItemName::BodyStructure => self.body_structure(), + MessageDataItemName::BodyExt { section, partial, peek, - } => self.body_ext(section, partial, peek), - FetchAttribute::InternalDate => self.internal_date(), + } => { + let (body, has_seen) = self.body_ext(section, partial, peek)?; + seen = has_seen; + Ok(body) + } + MessageDataItemName::InternalDate => self.internal_date(), }) .collect::<Result<Vec<_>, _>>()?; - Ok(Body::Data(Data::Fetch { - seq_or_uid: self.ids.i, - attributes: res_attrs, - })) + Ok(( + Body::Data(Data::Fetch { + seq: self.ids.i, + items: res_attrs.try_into()?, + }), + seen, + )) } } @@ -376,7 +405,6 @@ impl<'a> MailSelectionBuilder<'a> { meta, flags, content, - add_seen: false, }) .collect()) } @@ -396,35 +424,26 @@ pub struct MailboxView { 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. - /// These are the messages that are sent in response to a SELECT command. - pub async fn new(mailbox: Arc<Mailbox>) -> Result<(Self, Vec<Body>)> { + pub async fn new(mailbox: Arc<Mailbox>) -> Self { let state = mailbox.current_uid_index().await; - let new_view = Self { + Self { mailbox, known_state: state, - }; - - let mut data = Vec::<Body>::new(); - data.push(new_view.exists_status()?); - data.push(new_view.recent_status()?); - data.extend(new_view.flags_status()?.into_iter()); - data.push(new_view.uidvalidity_status()?); - data.push(new_view.uidnext_status()?); - - Ok((new_view, data)) + } } + /// 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) -> Result<Vec<Body>> { - let new_view = MailboxView { - mailbox: self.mailbox.clone(), - known_state: self.mailbox.current_uid_index().await, + pub async fn update(&mut self) -> Result<Vec<Body<'static>>> { + let old_view: &mut Self = self; + let new_view = Self { + mailbox: old_view.mailbox.clone(), + known_state: old_view.mailbox.current_uid_index().await, }; let mut data = Vec::<Body>::new(); @@ -446,7 +465,7 @@ impl MailboxView { // - notify client of expunged mails let mut n_expunge = 0; - for (i, (_uid, uuid)) in self.known_state.idx_by_uid.iter().enumerate() { + for (i, (_uid, uuid)) in old_view.known_state.idx_by_uid.iter().enumerate() { if !new_view.known_state.table.contains_key(uuid) { data.push(Body::Data(Data::Expunge( NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), @@ -456,49 +475,63 @@ impl MailboxView { } // - if new mails arrived, notify client of number of existing mails - if new_view.known_state.table.len() != self.known_state.table.len() - n_expunge - || new_view.known_state.uidvalidity != self.known_state.uidvalidity + if new_view.known_state.table.len() != old_view.known_state.table.len() - n_expunge + || new_view.known_state.uidvalidity != old_view.known_state.uidvalidity { data.push(new_view.exists_status()?); } - if new_view.known_state.uidvalidity != self.known_state.uidvalidity { + if new_view.known_state.uidvalidity != old_view.known_state.uidvalidity { // TODO: do we want to push less/more info than this? data.push(new_view.uidvalidity_status()?); data.push(new_view.uidnext_status()?); } else { // - if flags changed for existing mails, tell client for (i, (_uid, uuid)) in new_view.known_state.idx_by_uid.iter().enumerate() { - let old_mail = self.known_state.table.get(uuid); + let old_mail = old_view.known_state.table.get(uuid); let new_mail = new_view.known_state.table.get(uuid); if old_mail.is_some() && old_mail != new_mail { if let Some((uid, flags)) = new_mail { data.push(Body::Data(Data::Fetch { - seq_or_uid: NonZeroU32::try_from((i + 1) as u32).unwrap(), - attributes: vec![ - MessageAttribute::Uid(*uid), - MessageAttribute::Flags( + seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), + items: vec![ + MessageDataItem::Uid(*uid), + MessageDataItem::Flags( flags.iter().filter_map(|f| string_to_flag(f)).collect(), ), - ], + ] + .try_into()?, })); } } } } + *old_view = new_view; + 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()?); - *self = new_view; Ok(data) } - pub async fn store( + pub async fn store<'a>( &mut self, sequence_set: &SequenceSet, kind: &StoreType, _response: &StoreResponse, - flags: &[Flag], + flags: &[Flag<'a>], is_uid_store: &bool, - ) -> Result<Vec<Body>> { + ) -> Result<Vec<Body<'static>>> { self.mailbox.opportunistic_sync().await?; let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); @@ -522,7 +555,7 @@ impl MailboxView { self.update().await } - pub async fn expunge(&mut self) -> Result<Vec<Body>> { + pub async fn expunge(&mut self) -> Result<Vec<Body<'static>>> { self.mailbox.opportunistic_sync().await?; let deleted_flag = Flag::Deleted.to_string(); @@ -569,12 +602,12 @@ impl MailboxView { /// Looks up state changes in the mailbox and produces a set of IMAP /// responses describing the new state. - pub async fn fetch( + pub async fn fetch<'b>( &self, sequence_set: &SequenceSet, - attributes: &MacroOrFetchAttributes, + attributes: &'b MacroOrMessageDataItemNames<'static>, is_uid_fetch: &bool, - ) -> Result<Vec<Body>> { + ) -> Result<Vec<Body<'static>>> { let ap = AttributesProxy::new(attributes, *is_uid_fetch); // Prepare data @@ -619,31 +652,37 @@ impl MailboxView { selection.with_bodies(bodies.as_slice()); // Build mail selection views - let mut views = selection.build()?; + let views = selection.build()?; // Filter views to build the result - let ret = views - .iter_mut() - .filter_map(|mv| mv.filter(&ap).ok()) + // Also identify what must be put as seen + let filtered_view = views + .iter() + .filter_map(|mv| mv.filter(&ap).ok().map(|(body, seen)| (mv, body, seen))) .collect::<Vec<_>>(); - // Register seen flags - let future_flags = views + let future_flags = filtered_view .iter() - .filter(|mv| mv.add_seen) - .map(|mv| async move { + .filter(|(_mv, _body, seen)| matches!(seen, SeenFlag::MustAdd)) + .map(|(mv, _body, _seen)| async move { let seen_flag = Flag::Seen.to_string(); self.mailbox.add_flags(mv.ids.uuid, &[seen_flag]).await?; Ok::<_, anyhow::Error>(()) }) .collect::<FuturesOrdered<_>>(); + future_flags .collect::<Vec<_>>() .await .into_iter() .collect::<Result<_, _>>()?; - Ok(ret) + let command_body = filtered_view + .into_iter() + .map(|(_mv, body, _seen)| body) + .collect::<Vec<_>>(); + + Ok(command_body) } // ---- @@ -717,7 +756,7 @@ impl MailboxView { // ---- /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` - fn uidvalidity_status(&self) -> Result<Body> { + fn uidvalidity_status(&self) -> Result<Body<'static>> { let uid_validity = Status::ok( None, Some(Code::UidValidity(self.uidvalidity())), @@ -732,7 +771,7 @@ impl MailboxView { } /// Produce an OK [UIDNEXT _] message corresponding to `known_state` - fn uidnext_status(&self) -> Result<Body> { + fn uidnext_status(&self) -> Result<Body<'static>> { let next_uid = Status::ok( None, Some(Code::UidNext(self.uidnext())), @@ -748,7 +787,7 @@ impl MailboxView { /// Produce an EXISTS message corresponding to the number of mails /// in `known_state` - fn exists_status(&self) -> Result<Body> { + fn exists_status(&self) -> Result<Body<'static>> { Ok(Body::Data(Data::Exists(self.exists()?))) } @@ -758,7 +797,7 @@ impl MailboxView { /// Produce a RECENT message corresponding to the number of /// recent mails in `known_state` - fn recent_status(&self) -> Result<Body> { + fn recent_status(&self) -> Result<Body<'static>> { Ok(Body::Data(Data::Recent(self.recent()?))) } @@ -774,27 +813,48 @@ impl MailboxView { /// 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>> { - let mut flags: Vec<Flag> = self + 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 .known_state .idx_by_flag .flags() - .filter_map(|f| string_to_flag(f)) + .filter_map(|f| match string_to_flag(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 !flags.contains(f) { - flags.push(f.clone()); + if !known_flags.contains(f) { + known_flags.push(f.clone()); } } - let mut ret = vec![Body::Data(Data::Flags(flags.clone()))]; + // 1.c Create the IMAP message + body.push(Body::Data(Data::Flags(known_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)); + // 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)); - Ok(ret) + // Done! + Ok(body) } pub(crate) fn unseen_count(&self) -> usize { @@ -809,21 +869,21 @@ impl MailboxView { } } -fn string_to_flag(f: &str) -> Option<Flag> { +fn string_to_flag(f: &str) -> Option<FlagFetch<'static>> { match f.chars().next() { Some('\\') => match f { - "\\Seen" => Some(Flag::Seen), - "\\Answered" => Some(Flag::Answered), - "\\Flagged" => Some(Flag::Flagged), - "\\Deleted" => Some(Flag::Deleted), - "\\Draft" => Some(Flag::Draft), - "\\Recent" => Some(Flag::Recent), + "\\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(Flag::Extension(a)), + Ok(a) => Some(FlagFetch::Flag(Flag::system(a))), }, }, Some(_) => match Atom::try_from(f.to_string()) { @@ -831,7 +891,7 @@ fn string_to_flag(f: &str) -> Option<Flag> { tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); None } - Ok(a) => Some(Flag::Keyword(a)), + Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))), }, None => None, } @@ -858,7 +918,7 @@ fn string_to_flag(f: &str) -> Option<Flag> { //@FIXME return an error if the envelope is invalid instead of panicking //@FIXME some fields must be defaulted if there are not set. -fn message_envelope(msg: &imf::Imf) -> Envelope { +fn message_envelope(msg: &imf::Imf) -> Envelope<'static> { let from = msg.from.iter().map(convert_mbx).collect::<Vec<_>>(); Envelope { @@ -900,7 +960,7 @@ fn message_envelope(msg: &imf::Imf) -> Envelope { } } -fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address> { +fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address<'static>> { let mut acc = vec![]; for item in addrlist { match item { @@ -911,23 +971,23 @@ fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address> { return acc; } -fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address { - Address::new( - NString( +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 - NString(None), - NString(Some( + adl: NString(None), + mailbox: NString(Some( IString::try_from(addr.addrspec.local_part.to_string()).unwrap(), )), - NString(Some( + host: NString(Some( IString::try_from(addr.addrspec.domain.to_string()).unwrap(), )), - ) + } } /* @@ -945,19 +1005,23 @@ b fetch 29878:29879 (BODY) b OK Fetch completed (0.001 + 0.000 secs). */ -fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> { +fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure<'static>> { match part { AnyPart::Mult(x) => { let itype = &x.mime.interpreted_type; let subtype = IString::try_from(itype.subtype.to_string()) .unwrap_or(unchecked_istring("alternative")); + let inner_bodies = x + .children + .iter() + .filter_map(|inner| build_imap_email_struct(&inner).ok()) + .collect::<Vec<_>>(); + NonEmptyVec::validate(&inner_bodies)?; + let bodies = NonEmptyVec::unvalidated(inner_bodies); + Ok(BodyStructure::Multi { - bodies: x - .children - .iter() - .filter_map(|inner| build_imap_email_struct(&inner).ok()) - .collect(), + bodies, subtype, extension_data: None, /*Some(MultipartExtensionData { @@ -996,7 +1060,7 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> { number_of_lines: nol(x.body), }, }, - extension: None, + extension_data: None, }) } AnyPart::Bin(x) => { @@ -1009,9 +1073,10 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> { }; let ct = x.mime.fields.ctype.as_ref().unwrap_or(&default); - let 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 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!( @@ -1021,9 +1086,9 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> { Ok(BodyStructure::Single { body: FetchBody { basic, - specific: SpecificFields::Basic { type_, subtype }, + specific: SpecificFields::Basic { r#type, subtype }, }, - extension: None, + extension_data: None, }) } AnyPart::Msg(x) => { @@ -1033,12 +1098,12 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> { body: FetchBody { basic, specific: SpecificFields::Message { - envelope: message_envelope(&x.imf), + envelope: Box::new(message_envelope(&x.imf)), body_structure: Box::new(build_imap_email_struct(x.child.as_ref())?), number_of_lines: nol(x.raw_part), }, }, - extension: None, + extension_data: None, }) } } @@ -1059,7 +1124,7 @@ fn unchecked_istring(s: &'static str) -> IString { IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") } -fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result<BasicFields> { +fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result<BasicFields<'static>> { let parameter_list = m .ctype .as_ref() @@ -1136,20 +1201,18 @@ fn get_message_section<'a>( .ok_or(anyhow!("Part must be a message"))?; match section { Some(FetchSection::Text(None)) => Ok(msg.raw_body.into()), - Some(FetchSection::Text(Some(part))) => { - map_subpart(parsed, part.0.as_slice(), |part_msg| { - Ok(part_msg - .as_message() - .ok_or(Error::msg( - "Not a message/rfc822 part while expected by request (TEXT)", - ))? - .raw_body - .into()) - }) - } + Some(FetchSection::Text(Some(part))) => map_subpart(parsed, part.0.as_ref(), |part_msg| { + Ok(part_msg + .as_message() + .ok_or(Error::msg( + "Not a message/rfc822 part while expected by request (TEXT)", + ))? + .raw_body + .into()) + }), Some(FetchSection::Header(part)) => map_subpart( parsed, - part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), + part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]), |part_msg| { Ok(part_msg .as_message() @@ -1165,17 +1228,18 @@ fn get_message_section<'a>( ) => { let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _))); let fields = fields + .as_ref() .iter() .map(|x| match x { - AString::Atom(a) => a.as_bytes(), - AString::String(IString::Literal(l)) => l.as_slice(), - AString::String(IString::Quoted(q)) => q.as_bytes(), + AString::Atom(a) => a.inner().as_bytes(), + AString::String(IString::Literal(l)) => l.as_ref(), + AString::String(IString::Quoted(q)) => q.inner().as_bytes(), }) .collect::<Vec<_>>(); map_subpart( parsed, - part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), + part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]), |part_msg| { let mut ret = vec![]; for f in &part_msg.mime().kv { @@ -1195,7 +1259,7 @@ fn get_message_section<'a>( }, ) } - Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_slice(), |part| { + Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_ref(), |part| { let bytes = match &part { AnyPart::Txt(p) => p.body, AnyPart::Bin(p) => p.body, @@ -1204,7 +1268,7 @@ fn get_message_section<'a>( }; Ok(bytes.to_vec().into()) }), - Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_slice(), |part| { + Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_ref(), |part| { let bytes = match &part { AnyPart::Txt(p) => p.mime.fields.raw, AnyPart::Bin(p) => p.mime.fields.raw, @@ -1245,18 +1309,22 @@ mod tests { use super::*; use crate::cryptoblob; use crate::mail::unique_ident; - use imap_codec::codec::Encode; - use imap_codec::types::fetch_attributes::Section; + use imap_codec::encode::Encoder; + use imap_codec::imap_types::fetch::Section; + use imap_codec::imap_types::response::Response; + use imap_codec::ResponseCodec; use std::fs; #[test] fn mailview_body_ext() -> Result<()> { let ap = AttributesProxy::new( - &MacroOrFetchAttributes::FetchAttributes(vec![FetchAttribute::BodyExt { - section: Some(Section::Header(None)), - partial: None, - peek: false, - }]), + &MacroOrMessageDataItemNames::MessageDataItemNames(vec![ + MessageDataItemName::BodyExt { + section: Some(Section::Header(None)), + partial: None, + peek: false, + }, + ]), false, ); @@ -1276,27 +1344,26 @@ mod tests { 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 content = FetchedMail::new_from_message(eml_codec::parse_message(rfc822)?.1); - let mut mv = MailView { + let mv = MailView { ids: &ids, content, meta: &meta, flags: &flags, - add_seen: false, }; - let res_body = mv.filter(&ap)?; + let (res_body, _seen) = mv.filter(&ap)?; let fattr = match res_body { Body::Data(Data::Fetch { - seq_or_uid: _seq, - attributes: attr, + seq: _seq, + items: attr, }) => Ok(attr), _ => Err(anyhow!("Not a fetch body")), }?; - assert_eq!(fattr.len(), 1); + assert_eq!(fattr.as_ref().len(), 1); - let (sec, _orig, _data) = match &fattr[0] { - MessageAttribute::BodyExt { + let (sec, _orig, _data) = match &fattr.as_ref()[0] { + MessageDataItem::BodyExt { section, origin, data, @@ -1345,22 +1412,24 @@ mod tests { for pref in prefixes.iter() { println!("{}", pref); let txt = fs::read(format!("{}.eml", pref))?; - let exp = fs::read(format!("{}.dovecot.body", pref))?; + let oracle = fs::read(format!("{}.dovecot.body", pref))?; let message = eml_codec::parse_message(&txt).unwrap().1; - let mut resp = Vec::new(); - MessageAttribute::Body(build_imap_email_struct(&message.child)?) - .encode(&mut resp) - .unwrap(); - - let resp_str = String::from_utf8_lossy(&resp).to_lowercase(); + let test_repr = Response::Data(Data::Fetch { + seq: NonZeroU32::new(1).unwrap(), + items: NonEmptyVec::from(MessageDataItem::Body(build_imap_email_struct( + &message.child, + )?)), + }); + let test_bytes = ResponseCodec::new().encode(&test_repr).dump(); + let test_str = String::from_utf8_lossy(&test_bytes).to_lowercase(); - let exp_no_parenthesis = &exp[1..exp.len() - 1]; - let exp_str = String::from_utf8_lossy(exp_no_parenthesis).to_lowercase(); + let oracle_str = + format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).to_lowercase(); - println!("aerogramme: {}\n\ndovecot: {}\n\n", resp_str, exp_str); + println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str); //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); - assert_eq!(resp_str, exp_str); + assert_eq!(test_str, oracle_str); } Ok(()) diff --git a/src/imap/mod.rs b/src/imap/mod.rs index f85bcc6..31eeaa8 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -1,105 +1,186 @@ mod command; mod flow; mod mailbox_view; +mod response; mod session; -use std::task::{Context, Poll}; +use std::net::SocketAddr; use anyhow::Result; -use boitalettres::errors::Error as BalError; -use boitalettres::proto::{Request, Response}; -use boitalettres::server::accept::addr::AddrIncoming; -use boitalettres::server::accept::addr::AddrStream; -use boitalettres::server::Server as ImapServer; -use futures::future::BoxFuture; -use futures::future::FutureExt; +use futures::stream::{FuturesUnordered, StreamExt}; + +use tokio::net::TcpListener; use tokio::sync::watch; -use tower::Service; + +use imap_codec::imap_types::response::Greeting; +use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; +use imap_flow::stream::AnyStream; use crate::config::ImapConfig; use crate::login::ArcLoginProvider; /// Server is a thin wrapper to register our Services in BàL -pub struct Server(ImapServer<AddrIncoming, Instance>); - -pub async fn new(config: ImapConfig, login: ArcLoginProvider) -> Result<Server> { - //@FIXME add a configuration parameter - let incoming = AddrIncoming::new(config.bind_addr).await?; - tracing::info!("IMAP activated, will listen on {:#}", incoming.local_addr); - - let imap = ImapServer::new(incoming).serve(Instance::new(login.clone())); - Ok(Server(imap)) -} - -impl Server { - pub async fn run(self, mut must_exit: watch::Receiver<bool>) -> Result<()> { - tracing::info!("IMAP started!"); - tokio::select! { - s = self.0 => s?, - _ = must_exit.changed() => tracing::info!("Stopped IMAP server"), - } - - Ok(()) - } +pub struct Server { + bind_addr: SocketAddr, + login_provider: ArcLoginProvider, } -//--- - -/// Instance is the main Tokio Tower service that we register in BàL. -/// It receives new connection demands and spawn a dedicated service. -struct Instance { +struct ClientContext { + stream: AnyStream, + addr: SocketAddr, login_provider: ArcLoginProvider, + must_exit: watch::Receiver<bool>, } -impl Instance { - pub fn new(login_provider: ArcLoginProvider) -> Self { - Self { login_provider } +pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server { + Server { + bind_addr: config.bind_addr, + login_provider: login, } } -impl<'a> Service<&'a AddrStream> for Instance { - type Response = Connection; - type Error = anyhow::Error; - type Future = BoxFuture<'static, Result<Self::Response>>; +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 client = ClientContext { + stream: AnyStream::new(socket), + addr: remote_addr.clone(), + login_provider: self.login_provider.clone(), + must_exit: must_exit.clone(), + }; + let conn = tokio::spawn(client_wrapper(client)); + connections.push(conn); + } + drop(tcp); - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { - Poll::Ready(Ok(())) - } + tracing::info!("IMAP server shutting down, draining remaining connections..."); + while connections.next().await.is_some() {} - fn call(&mut self, addr: &'a AddrStream) -> Self::Future { - tracing::info!(remote_addr = %addr.remote_addr, local_addr = %addr.local_addr, "accept"); - let lp = self.login_provider.clone(); - async { Ok(Connection::new(lp)) }.boxed() + Ok(()) } } -//--- - -/// Connection is the per-connection Tokio Tower service we register in BàL. -/// It handles a single TCP connection, and thus has a business logic. -struct Connection { - session: session::Manager, -} - -impl Connection { - pub fn new(login_provider: ArcLoginProvider) -> Self { - Self { - session: session::Manager::new(login_provider), +async fn client_wrapper(ctx: ClientContext) { + let addr = ctx.addr.clone(); + match client(ctx).await { + Ok(()) => { + tracing::info!("closing successful session for {:?}", addr); + } + Err(e) => { + tracing::error!("closing errored session for {:?}: {}", addr, e); } } } -impl Service<Request> for Connection { - type Response = Response; - type Error = BalError; - type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>; +async fn client(mut ctx: ClientContext) -> Result<()> { + // Send greeting + let (mut server, _) = ServerFlow::send_greeting( + ctx.stream, + ServerFlowOptions::default(), + Greeting::ok(None, "Aerogramme").unwrap(), + ) + .await?; + + use crate::imap::response::{Body, Response as MyResponse}; + use crate::imap::session::Instance; + use imap_codec::imap_types::command::Command; + use imap_codec::imap_types::response::{Response, Status}; + + use tokio::sync::mpsc; + let (cmd_tx, mut cmd_rx) = mpsc::channel::<Command<'static>>(10); + let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::<MyResponse<'static>>(); + + let bckgrnd = tokio::spawn(async move { + let mut session = Instance::new(ctx.login_provider); + loop { + let cmd = match cmd_rx.recv().await { + None => break, + Some(cmd_recv) => cmd_recv, + }; + + let maybe_response = session.command(cmd).await; + + match resp_tx.send(maybe_response) { + Err(_) => break, + Ok(_) => (), + }; + } + tracing::info!("runner is quitting"); + }); - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { - Poll::Ready(Ok(())) + // Main loop + loop { + tokio::select! { + // Managing imap_flow stuff + srv_evt = server.progress() => match srv_evt? { + ServerFlowEvent::ResponseSent { handle: _handle, response } => { + match response { + Response::Status(Status::Bye(_)) => break, + _ => tracing::trace!("sent to {} content {:?}", ctx.addr, response), + } + }, + ServerFlowEvent::CommandReceived { command } => { + match cmd_tx.try_send(command) { + Ok(_) => (), + Err(mpsc::error::TrySendError::Full(_)) => { + server.enqueue_status(Status::bye(None, "Too fast").unwrap()); + tracing::error!("client {:?} is sending commands too fast, closing.", ctx.addr); + } + _ => { + server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); + tracing::error!("session task exited for {:?}, quitting", ctx.addr); + } + } + }, + }, + + // Managing response generated by Aerogramme + maybe_msg = resp_rx.recv() => { + let response = match maybe_msg { + None => { + server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); + tracing::error!("session task exited for {:?}, quitting", ctx.addr); + continue + }, + Some(r) => r, + }; + + for body_elem in response.body.into_iter() { + let _handle = match body_elem { + Body::Data(d) => server.enqueue_data(d), + Body::Status(s) => server.enqueue_status(s), + }; + } + server.enqueue_status(response.completion); + }, + + // When receiving a CTRL+C + _ = ctx.must_exit.changed() => { + server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap()); + }, + }; } - fn call(&mut self, req: Request) -> Self::Future { - tracing::debug!("Got request: {:#?}", req.command); - self.session.process(req) - } + drop(cmd_tx); + bckgrnd.await?; + Ok(()) } diff --git a/src/imap/response.rs b/src/imap/response.rs new file mode 100644 index 0000000..d20e58e --- /dev/null +++ b/src/imap/response.rs @@ -0,0 +1,112 @@ +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}; + +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, + }) + } +} + +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![], + }) + } +} diff --git a/src/imap/session.rs b/src/imap/session.rs index 15141d3..5c67f8e 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,180 +1,86 @@ -use anyhow::Error; -use boitalettres::errors::Error as BalError; -use boitalettres::proto::{Request, Response}; -use futures::future::BoxFuture; -use futures::future::FutureExt; - -use tokio::sync::mpsc::error::TrySendError; -use tokio::sync::{mpsc, oneshot}; - use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; +use crate::imap::response::Response; use crate::login::ArcLoginProvider; - -/* This constant configures backpressure in the system, - * or more specifically, how many pipelined messages are allowed - * before refusing them - */ -const MAX_PIPELINED_COMMANDS: usize = 10; - -struct Message { - req: Request, - tx: oneshot::Sender<Result<Response, BalError>>, -} - -//----- - -pub struct Manager { - tx: mpsc::Sender<Message>, -} - -impl Manager { - pub fn new(login_provider: ArcLoginProvider) -> Self { - let (tx, rx) = mpsc::channel(MAX_PIPELINED_COMMANDS); - tokio::spawn(async move { - let instance = Instance::new(login_provider, rx); - instance.start().await; - }); - Self { tx } - } - - pub fn process(&self, req: Request) -> BoxFuture<'static, Result<Response, BalError>> { - let (tx, rx) = oneshot::channel(); - let msg = Message { req, tx }; - - // We use try_send on a bounded channel to protect the daemons from DoS. - // Pipelining requests in IMAP are a special case: they should not occure often - // and in a limited number (like 3 requests). Someone filling the channel - // will probably be malicious so we "rate limit" them. - match self.tx.try_send(msg) { - Ok(()) => (), - Err(TrySendError::Full(_)) => { - return async { Response::bad("Too fast! Send less pipelined requests.") }.boxed() - } - Err(TrySendError::Closed(_)) => { - return async { Err(BalError::Text("Terminated session".to_string())) }.boxed() - } - }; - - // @FIXME add a timeout, handle a session that fails. - async { - match rx.await { - Ok(r) => r, - Err(e) => { - tracing::warn!("Got error {:#?}", e); - Response::bad("No response from the session handler") - } - } - } - .boxed() - } -} +use imap_codec::imap_types::command::Command; //----- - pub struct Instance { - rx: mpsc::Receiver<Message>, - pub login_provider: ArcLoginProvider, pub state: flow::State, } impl Instance { - fn new(login_provider: ArcLoginProvider, rx: mpsc::Receiver<Message>) -> Self { + pub fn new(login_provider: ArcLoginProvider) -> Self { Self { login_provider, - rx, state: flow::State::NotAuthenticated, } } - //@FIXME add a function that compute the runner's name from its local info - // to ease debug - // fn name(&self) -> String { } - - async fn start(mut self) { - //@FIXME add more info about the runner - tracing::debug!("starting runner"); - - while let Some(msg) = self.rx.recv().await { - // Command behavior is modulated by the state. - // To prevent state error, we handle the same command in separate code paths. - let ctrl = match &mut self.state { - flow::State::NotAuthenticated => { - let ctx = anonymous::AnonymousContext { - req: &msg.req, - login_provider: Some(&self.login_provider), - }; - anonymous::dispatch(ctx).await - } - flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &msg.req, - user, - }; - authenticated::dispatch(ctx).await - } - flow::State::Selected(ref user, ref mut mailbox) => { - let ctx = selected::SelectedContext { - req: &msg.req, - user, - mailbox, - }; - selected::dispatch(ctx).await - } - flow::State::Examined(ref user, ref mut mailbox) => { - let ctx = examined::ExaminedContext { - req: &msg.req, - user, - mailbox, - }; - examined::dispatch(ctx).await - } - flow::State::Logout => { - Response::bad("No commands are allowed in the LOGOUT state.") - .map(|r| (r, flow::Transition::None)) - .map_err(Error::msg) - } - }; - - // Process result - let res = match ctrl { - Ok((res, tr)) => { - //@FIXME remove unwrap - self.state = match self.state.apply(tr) { - Ok(new_state) => new_state, - Err(e) => { - tracing::error!("Invalid transition: {}, exiting", e); - break; - } - }; - - //@FIXME enrich here the command with some global status - - Ok(res) - } - // Cast from anyhow::Error to Bal::Error - // @FIXME proper error handling would be great - Err(e) => match e.downcast::<BalError>() { - Ok(be) => Err(be), - Err(e) => { - tracing::warn!(error=%e, "internal.error"); - Response::bad("Internal error") - } - }, - }; - - //@FIXME I think we should quit this thread on error and having our manager watch it, - // and then abort the session as it is corrupted. - msg.tx.send(res).unwrap_or_else(|e| { - tracing::warn!("failed to send imap response to manager: {:#?}", e) - }); - - if let flow::State::Logout = &self.state { - break; + pub async fn command(&mut self, cmd: Command<'static>) -> Response<'static> { + // 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, + }; + anonymous::dispatch(ctx).await + } + flow::State::Authenticated(ref user) => { + let ctx = authenticated::AuthenticatedContext { req: &cmd, user }; + authenticated::dispatch(ctx).await + } + flow::State::Selected(ref user, ref mut mailbox) => { + let ctx = selected::SelectedContext { + req: &cmd, + user, + mailbox, + }; + selected::dispatch(ctx).await } + flow::State::Examined(ref user, ref mut mailbox) => { + let ctx = examined::ExaminedContext { + req: &cmd, + user, + mailbox, + }; + examined::dispatch(ctx).await + } + 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 Response::build() + .to_req(&cmd) + .message( + "Internal error, processing command triggered an illegal IMAP state transition", + ) + .bad() + .unwrap(); } - //@FIXME add more info about the runner - tracing::debug!("exiting runner"); + resp } } diff --git a/src/login/demo_provider.rs b/src/login/demo_provider.rs new file mode 100644 index 0000000..11c7d54 --- /dev/null +++ b/src/login/demo_provider.rs @@ -0,0 +1,51 @@ +use crate::login::*; +use crate::storage::*; + +pub struct DemoLoginProvider { + keys: CryptoKeys, + in_memory_store: in_memory::MemDb, +} + +impl DemoLoginProvider { + pub fn new() -> Self { + Self { + keys: CryptoKeys::init(), + in_memory_store: in_memory::MemDb::new(), + } + } +} + +#[async_trait] +impl LoginProvider for DemoLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result<Credentials> { + tracing::debug!(user=%username, "login"); + + if username != "alice" { + bail!("user does not exist"); + } + + if password != "hunter2" { + bail!("wrong password"); + } + + let storage = self.in_memory_store.builder("alice").await; + let keys = self.keys.clone(); + + Ok(Credentials { storage, keys }) + } + + async fn public_login(&self, email: &str) -> Result<PublicCredentials> { + tracing::debug!(user=%email, "public_login"); + if email != "alice@example.tld" { + bail!("invalid email address"); + } + + let storage = self.in_memory_store.builder("alice").await; + let public_key = self.keys.public.clone(); + + Ok(PublicCredentials { + storage, + public_key, + }) + } +} diff --git a/src/login/mod.rs b/src/login/mod.rs index 2926738..4a1dee1 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,3 +1,4 @@ +pub mod demo_provider; pub mod ldap_provider; pub mod static_provider; diff --git a/src/main.rs b/src/main.rs index 3221c2e..3baa8e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,12 @@ struct Args { #[clap(subcommand)] command: Command, + /// A special mode dedicated to developers, NOT INTENDED FOR PRODUCTION + #[clap(long)] + dev: bool, + #[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")] + /// Path to the main Aerogramme configuration file config_file: PathBuf, } @@ -158,7 +163,22 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let any_config = read_config(args.config_file)?; + let any_config = if args.dev { + use std::net::*; + AnyConfig::Provider(ProviderConfig { + pid: None, + imap: ImapConfig { + bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143), + }, + lmtp: LmtpConfig { + bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025), + hostname: "example.tld".to_string(), + }, + users: UserManagement::Demo, + }) + } else { + read_config(args.config_file)? + }; match (&args.command, any_config) { (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand { @@ -184,8 +204,8 @@ async fn main() -> Result<()> { ProviderCommand::Account(cmd) => { let user_file = match config.users { UserManagement::Static(conf) => conf.user_list, - UserManagement::Ldap(_) => { - panic!("LDAP account management is not supported from Aerogramme.") + _ => { + panic!("Only static account management is supported from Aerogramme.") } }; account_management(&args.command, cmd, user_file)?; diff --git a/src/server.rs b/src/server.rs index 28e0b27..bd2fd5d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -11,7 +11,7 @@ use crate::config::*; use crate::imap; use crate::lmtp::*; use crate::login::ArcLoginProvider; -use crate::login::{ldap_provider::*, static_provider::*}; +use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*}; pub struct Server { lmtp_server: Option<Arc<LmtpServer>>, @@ -25,7 +25,7 @@ impl Server { let login = Arc::new(StaticLoginProvider::new(config.users).await?); let lmtp_server = None; - let imap_server = Some(imap::new(config.imap, login.clone()).await?); + let imap_server = Some(imap::new(config.imap, login.clone())); Ok(Self { lmtp_server, imap_server, @@ -36,12 +36,13 @@ impl Server { pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> { tracing::info!("Init as provider"); let login: ArcLoginProvider = match config.users { + UserManagement::Demo => Arc::new(DemoLoginProvider::new()), UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?), UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?), }; let lmtp_server = Some(LmtpServer::new(config.lmtp, login.clone())); - let imap_server = Some(imap::new(config.imap, login.clone()).await?); + let imap_server = Some(imap::new(config.imap, login.clone())); Ok(Self { lmtp_server, |