diff options
-rw-r--r-- | src/imap/command/anonymous.rs | 52 | ||||
-rw-r--r-- | src/imap/command/authenticated.rs | 113 | ||||
-rw-r--r-- | src/imap/command/selected.rs | 38 | ||||
-rw-r--r-- | src/imap/flow.rs | 24 | ||||
-rw-r--r-- | src/imap/mod.rs | 12 | ||||
-rw-r--r-- | src/imap/session.rs | 39 | ||||
-rw-r--r-- | src/server.rs | 9 |
7 files changed, 174 insertions, 113 deletions
diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 7a5e514..c1b800e 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,5 +1,4 @@ - -use anyhow::{Result, Error}; +use anyhow::{Error, Result}; use boitalettres::proto::Response; use imap_codec::types::command::CommandBody; use imap_codec::types::core::AString; @@ -14,9 +13,13 @@ pub async fn dispatch<'a>(ctx: InnerContext<'a>) -> Result<(Response, flow::Tran match &ctx.req.body { CommandBody::Capability => capability(ctx).await, CommandBody::Login { username, password } => login(ctx, username, password).await, - _ => Status::no(Some(ctx.req.tag.clone()), None, "This command is not available in the ANONYMOUS state.") - .map(|s| (vec![ImapRes::Status(s)], flow::Transition::No)) - .map_err(Error::msg), + _ => Status::no( + Some(ctx.req.tag.clone()), + None, + "This command is not available in the ANONYMOUS state.", + ) + .map(|s| (vec![ImapRes::Status(s)], flow::Transition::No)) + .map_err(Error::msg), } } @@ -28,23 +31,33 @@ async fn capability<'a>(ctx: InnerContext<'a>) -> Result<(Response, flow::Transi ImapRes::Data(Data::Capability(capabilities)), ImapRes::Status( Status::ok(Some(ctx.req.tag.clone()), None, "Server capabilities") - .map_err(Error::msg)?, - ), + .map_err(Error::msg)?, + ), ]; Ok((res, flow::Transition::No)) } -async fn login<'a>(ctx: InnerContext<'a>, username: &AString, password: &AString) -> Result<(Response, flow::Transition)> { - let (u, p) = (String::try_from(username.clone())?, String::try_from(password.clone())?); +async fn login<'a>( + ctx: InnerContext<'a>, + username: &AString, + password: &AString, +) -> Result<(Response, flow::Transition)> { + let (u, p) = ( + String::try_from(username.clone())?, + String::try_from(password.clone())?, + ); tracing::info!(user = %u, "command.login"); let creds = match ctx.login.login(&u, &p).await { Err(e) => { tracing::debug!(error=%e, "authentication failed"); - return Ok((vec![ImapRes::Status( + return Ok(( + vec![ImapRes::Status( Status::no(Some(ctx.req.tag.clone()), None, "Authentication failed") - .map_err(Error::msg)?, - )], flow::Transition::No)); + .map_err(Error::msg)?, + )], + flow::Transition::No, + )); } Ok(c) => c, }; @@ -56,10 +69,13 @@ async fn login<'a>(ctx: InnerContext<'a>, username: &AString, password: &AString let tr = flow::Transition::Authenticate(user); tracing::info!(username=%u, "connected"); - Ok((vec![ - //@FIXME we could send a capability status here too - ImapRes::Status( - Status::ok(Some(ctx.req.tag.clone()), None, "completed").map_err(Error::msg)?, - ), - ], tr)) + Ok(( + vec![ + //@FIXME we could send a capability status here too + ImapRes::Status( + Status::ok(Some(ctx.req.tag.clone()), None, "completed").map_err(Error::msg)?, + ), + ], + tr, + )) } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index b4c4432..fca15fc 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -1,5 +1,4 @@ - -use anyhow::{Result, Error, anyhow}; +use anyhow::{anyhow, Error, Result}; use boitalettres::proto::Response; use imap_codec::types::command::CommandBody; use imap_codec::types::core::Tag; @@ -7,16 +6,29 @@ use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::types::response::{Code, Data, Response as ImapRes, Status}; use crate::imap::command::anonymous; -use crate::imap::session::InnerContext; use crate::imap::flow; +use crate::imap::session::InnerContext; use crate::mailbox::Mailbox; -pub async fn dispatch<'a>(inner: InnerContext<'a>, user: &'a flow::User) -> Result<(Response, flow::Transition)> { - let ctx = StateContext { user, tag: &inner.req.tag, inner }; +pub async fn dispatch<'a>( + inner: InnerContext<'a>, + user: &'a flow::User, +) -> Result<(Response, flow::Transition)> { + let ctx = StateContext { + user, + tag: &inner.req.tag, + inner, + }; match &ctx.inner.req.body { - CommandBody::Lsub { reference, mailbox_wildcard, } => ctx.lsub(reference, mailbox_wildcard).await, - CommandBody::List { reference, mailbox_wildcard, } => ctx.list(reference, mailbox_wildcard).await, + CommandBody::Lsub { + reference, + mailbox_wildcard, + } => ctx.lsub(reference, mailbox_wildcard).await, + CommandBody::List { + reference, + mailbox_wildcard, + } => ctx.list(reference, mailbox_wildcard).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await, _ => anonymous::dispatch(ctx.inner).await, } @@ -24,7 +36,6 @@ pub async fn dispatch<'a>(inner: InnerContext<'a>, user: &'a flow::User) -> Resu // --- PRIVATE --- - struct StateContext<'a> { inner: InnerContext<'a>, user: &'a flow::User, @@ -36,45 +47,50 @@ impl<'a> StateContext<'a> { &self, reference: &MailboxCodec, mailbox_wildcard: &ListMailbox, - ) -> Result<(Response, flow::Transition)> { - Ok((vec![ImapRes::Status( + ) -> Result<(Response, flow::Transition)> { + Ok(( + vec![ImapRes::Status( Status::bad(Some(self.tag.clone()), None, "Not implemented").map_err(Error::msg)?, - )], flow::Transition::No)) + )], + flow::Transition::No, + )) } async fn list( &self, reference: &MailboxCodec, mailbox_wildcard: &ListMailbox, - ) -> Result<(Response, flow::Transition)> { - Ok((vec![ - ImapRes::Status(Status::bad(Some(self.tag.clone()), None, "Not implemented").map_err(Error::msg)?), - ], flow::Transition::No)) + ) -> Result<(Response, flow::Transition)> { + Ok(( + vec![ImapRes::Status( + Status::bad(Some(self.tag.clone()), None, "Not implemented").map_err(Error::msg)?, + )], + flow::Transition::No, + )) } /* - * TRACE BEGIN --- + * TRACE BEGIN --- - Example: C: A142 SELECT INBOX - S: * 172 EXISTS - S: * 1 RECENT - S: * OK [UNSEEN 12] Message 12 is first unseen - S: * OK [UIDVALIDITY 3857529045] UIDs valid - S: * OK [UIDNEXT 4392] Predicted next UID - S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) - S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited - S: A142 OK [READ-WRITE] SELECT completed + Example: C: A142 SELECT INBOX + S: * 172 EXISTS + S: * 1 RECENT + S: * OK [UNSEEN 12] Message 12 is first unseen + S: * OK [UIDVALIDITY 3857529045] UIDs valid + S: * OK [UIDNEXT 4392] Predicted next UID + S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) + S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited + S: A142 OK [READ-WRITE] SELECT completed - * TRACE END --- - */ + * TRACE END --- + */ async fn select(&self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { let name = String::try_from(mailbox.clone())?; let mut mb = Mailbox::new(&self.user.creds, name.clone())?; tracing::info!(username=%self.user.name, mailbox=%name, "mailbox.selected"); - let sum = mb.summary().await?; tracing::trace!(summary=%sum, "mailbox.summary"); @@ -82,21 +98,34 @@ impl<'a> StateContext<'a> { let tr = flow::Transition::Select(mb); - let r_unseen = Status::ok(None, Some(Code::Unseen(std::num::NonZeroU32::new(1).ok_or(anyhow!("Invalid message identifier"))?)), "First unseen UID").map_err(Error::msg)?; + let r_unseen = Status::ok( + None, + Some(Code::Unseen( + std::num::NonZeroU32::new(1).ok_or(anyhow!("Invalid message identifier"))?, + )), + "First unseen UID", + ) + .map_err(Error::msg)?; //let r_permanentflags = Status::ok(None, Some(Code:: - Ok((vec![ - ImapRes::Data(Data::Exists(0)), - ImapRes::Data(Data::Recent(0)), - ImapRes::Data(Data::Flags(vec![])), - /*ImapRes::Status(), - ImapRes::Status(), - ImapRes::Status(),*/ - ImapRes::Status(Status::ok( - Some(self.tag.clone()), - Some(Code::ReadWrite), - "Select completed", - ).map_err(Error::msg)?), - ], tr)) + Ok(( + vec![ + ImapRes::Data(Data::Exists(0)), + ImapRes::Data(Data::Recent(0)), + ImapRes::Data(Data::Flags(vec![])), + /*ImapRes::Status(), + ImapRes::Status(), + ImapRes::Status(),*/ + ImapRes::Status( + Status::ok( + Some(self.tag.clone()), + Some(Code::ReadWrite), + "Select completed", + ) + .map_err(Error::msg)?, + ), + ], + tr, + )) } } diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index a068fab..fb6a75d 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,5 +1,4 @@ - -use anyhow::{Result, Error}; +use anyhow::{Error, Result}; use boitalettres::proto::Response; use imap_codec::types::command::CommandBody; use imap_codec::types::core::Tag; @@ -8,21 +7,32 @@ use imap_codec::types::response::{Response as ImapRes, Status}; use imap_codec::types::sequence::SequenceSet; use crate::imap::command::authenticated; -use crate::imap::session::InnerContext; use crate::imap::flow; +use crate::imap::session::InnerContext; use crate::mailbox::Mailbox; - -pub async fn dispatch<'a>(inner: InnerContext<'a>, user: &'a flow::User, mailbox: &'a Mailbox) -> Result<(Response, flow::Transition)> { - let ctx = StateContext { tag: &inner.req.tag, inner, user, mailbox }; +pub async fn dispatch<'a>( + inner: InnerContext<'a>, + user: &'a flow::User, + mailbox: &'a Mailbox, +) -> Result<(Response, flow::Transition)> { + let ctx = StateContext { + tag: &inner.req.tag, + inner, + user, + mailbox, + }; match &ctx.inner.req.body { - CommandBody::Fetch { sequence_set, attributes, uid, } => ctx.fetch(sequence_set, attributes, uid).await, + CommandBody::Fetch { + sequence_set, + attributes, + uid, + } => ctx.fetch(sequence_set, attributes, uid).await, _ => authenticated::dispatch(ctx.inner, user).await, } } - // --- PRIVATE --- struct StateContext<'a> { @@ -32,16 +42,18 @@ struct StateContext<'a> { tag: &'a Tag, } - impl<'a> StateContext<'a> { pub async fn fetch( &self, sequence_set: &SequenceSet, attributes: &MacroOrFetchAttributes, uid: &bool, - ) -> Result<(Response, flow::Transition)> { - Ok((vec![ - ImapRes::Status(Status::bad(Some(self.tag.clone()), None, "Not implemented").map_err(Error::msg)?), - ], flow::Transition::No)) + ) -> Result<(Response, flow::Transition)> { + Ok(( + vec![ImapRes::Status( + Status::bad(Some(self.tag.clone()), None, "Not implemented").map_err(Error::msg)?, + )], + flow::Transition::No, + )) } } diff --git a/src/imap/flow.rs b/src/imap/flow.rs index 9a2a2ba..f0ec7d1 100644 --- a/src/imap/flow.rs +++ b/src/imap/flow.rs @@ -1,5 +1,5 @@ -use std::fmt; use std::error::Error as StdError; +use std::fmt; use crate::login::Credentials; use crate::mailbox::Mailbox; @@ -14,26 +14,25 @@ pub enum Error { ForbiddenTransition, } impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Forbidden Transition") - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Forbidden Transition") + } } -impl StdError for Error { } - +impl StdError for Error {} pub enum State { NotAuthenticated, Authenticated(User), Selected(User, Mailbox), - Logout + Logout, } pub enum Transition { - No, - Authenticate(User), - Select(Mailbox), - Unselect, - Logout, + No, + Authenticate(User), + Select(Mailbox), + Unselect, + Logout, } // See RFC3501 section 3. @@ -50,4 +49,3 @@ impl State { } } } - diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 7cc4f68..4b44140 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -1,6 +1,6 @@ -mod session; -mod flow; mod command; +mod flow; +mod session; use std::task::{Context, Poll}; @@ -15,16 +15,12 @@ use futures::future::FutureExt; use tokio::sync::watch; use tower::Service; -use crate::login::ArcLoginProvider; 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> { - +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); diff --git a/src/imap/session.rs b/src/imap/session.rs index a500e2b..2ff4117 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -7,7 +7,7 @@ use imap_codec::types::response::{Response as ImapRes, Status}; use tokio::sync::mpsc::error::TrySendError; use tokio::sync::{mpsc, oneshot}; -use crate::imap::command::{anonymous,authenticated,selected}; +use crate::imap::command::{anonymous, authenticated, selected}; use crate::imap::flow; use crate::login::ArcLoginProvider; @@ -98,10 +98,7 @@ pub struct Instance { pub state: flow::State, } impl Instance { - fn new( - login_provider: ArcLoginProvider, - rx: mpsc::Receiver<Message>, - ) -> Self { + fn new(login_provider: ArcLoginProvider, rx: mpsc::Receiver<Message>) -> Self { Self { login_provider, rx, @@ -118,7 +115,11 @@ impl Instance { tracing::debug!("starting runner"); while let Some(msg) = self.rx.recv().await { - let ctx = InnerContext { req: &msg.req, state: &self.state, login: &self.login_provider }; + let ctx = InnerContext { + req: &msg.req, + state: &self.state, + login: &self.login_provider, + }; // Command behavior is modulated by the state. // To prevent state error, we handle the same command in separate code path depending @@ -126,10 +127,16 @@ impl Instance { let ctrl = match &self.state { flow::State::NotAuthenticated => anonymous::dispatch(ctx).await, flow::State::Authenticated(user) => authenticated::dispatch(ctx, user).await, - flow::State::Selected(user, mailbox) => selected::dispatch(ctx, user, mailbox).await, - _ => Status::bad(Some(ctx.req.tag.clone()), None, "No commands are allowed in the LOGOUT state.") - .map(|s| (vec![ImapRes::Status(s)], flow::Transition::No)) - .map_err(Error::msg), + flow::State::Selected(user, mailbox) => { + selected::dispatch(ctx, user, mailbox).await + } + _ => Status::bad( + Some(ctx.req.tag.clone()), + None, + "No commands are allowed in the LOGOUT state.", + ) + .map(|s| (vec![ImapRes::Status(s)], flow::Transition::No)) + .map_err(Error::msg), }; // Process result @@ -138,7 +145,7 @@ impl Instance { //@FIXME unwrap self.state = self.state.apply(tr).unwrap(); Ok(res) - }, + } // Cast from anyhow::Error to Bal::Error // @FIXME proper error handling would be great Err(e) => match e.downcast::<BalError>() { @@ -149,7 +156,7 @@ impl Instance { .map(|s| vec![ImapRes::Status(s)]) .map_err(|e| BalError::Text(e.to_string())) } - } + }, }; //@FIXME I think we should quit this thread on error and having our manager watch it, @@ -157,9 +164,9 @@ impl Instance { msg.tx.send(res).unwrap_or_else(|e| { tracing::warn!("failed to send imap response to manager: {:#?}", e) }); - } + } - //@FIXME add more info about the runner - tracing::debug!("exiting runner"); -} + //@FIXME add more info about the runner + tracing::debug!("exiting runner"); + } } diff --git a/src/server.rs b/src/server.rs index cbe434c..9b83ad4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,10 +7,10 @@ use rusoto_signature::Region; use tokio::sync::watch; use crate::config::*; -use crate::lmtp::*; -use crate::login::{ldap_provider::*, static_provider::*, *}; use crate::imap; +use crate::lmtp::*; use crate::login::ArcLoginProvider; +use crate::login::{ldap_provider::*, static_provider::*, *}; pub struct Server { lmtp_server: Option<Arc<LmtpServer>>, @@ -27,7 +27,10 @@ impl Server { None => None, }; - Ok(Self { lmtp_server, imap_server }) + Ok(Self { + lmtp_server, + imap_server, + }) } pub async fn run(self) -> Result<()> { |