diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 55e701b..c6af354 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -10,19 +10,19 @@ use crate::imap::session::InnerContext; //--- dispatching -pub async fn dispatch<'a>(ctx: &'a InnerContext<'a>) -> Result { - match ctx.req.body { +pub async fn dispatch<'a>(ctx: InnerContext<'a>) -> Result<(Response, flow::Transition)> { + match &ctx.req.body { CommandBody::Capability => capability(ctx).await, - CommandBody::Login { username, password } => login(ctx, username, password).await, + CommandBody::Login { username, password } => login(ctx, username.clone(), password.clone()).await, _ => Status::no(Some(ctx.req.tag.clone()), None, "This command is not available in the ANONYMOUS state.") - .map(|s| vec![ImapRes::Status(s)]) + .map(|s| (vec![ImapRes::Status(s)], flow::Transition::No)) .map_err(Error::msg), } } //--- Command controllers, private -async fn capability<'a>(ctx: InnerContext<'a>) -> Result { +async fn capability<'a>(ctx: InnerContext<'a>) -> Result<(Response, flow::Transition)> { let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; let res = vec![ ImapRes::Data(Data::Capability(capabilities)), @@ -31,20 +31,20 @@ async fn capability<'a>(ctx: InnerContext<'a>) -> Result { .map_err(Error::msg)?, ), ]; - Ok(res) + Ok((res, flow::Transition::No)) } -async fn login<'a>(ctx: InnerContext<'a>, username: AString, password: AString) -> Result { +async fn login<'a>(ctx: InnerContext<'a>, username: AString, password: AString) -> Result<(Response, flow::Transition)> { let (u, p) = (String::try_from(username)?, String::try_from(password)?); tracing::info!(user = %u, "command.login"); - let creds = match ctx.login_provider.login(&u, &p).await { + 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)); } Ok(c) => c, }; @@ -53,13 +53,13 @@ async fn login<'a>(ctx: InnerContext<'a>, username: AString, password: AString) creds, name: u.clone(), }; - ctx.state.authenticate(user)?; + let tr = flow::Transition::Authenticate(user); tracing::info!(username=%u, "connected"); - Ok(vec![ + 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 49bfa9c..093521a 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -11,21 +11,22 @@ use crate::imap::session::InnerContext; use crate::imap::flow::User; use crate::mailbox::Mailbox; -pub async fn dispatch<'a>(inner: &'a InnerContext<'a>, user: &'a User) -> Result { +/*pub async fn dispatch<'a>(inner: &'a mut InnerContext<'a>, user: &'a User) -> Result { let ctx = StateContext { inner, user, tag: &inner.req.tag }; - match ctx.req.body.as_ref() { - 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, + match &ctx.inner.req.body { + CommandBody::Lsub { reference, mailbox_wildcard, } => ctx.lsub(reference.clone(), mailbox_wildcard.clone()).await, + CommandBody::List { reference, mailbox_wildcard, } => ctx.list(reference.clone(), mailbox_wildcard.clone()).await, + CommandBody::Select { mailbox } => ctx.select(mailbox.clone()).await, _ => anonymous::dispatch(ctx.inner).await, } -} +}*/ // --- PRIVATE --- +/* struct StateContext<'a> { - inner: InnerContext<'a>, + inner: &'a mut InnerContext<'a>, user: &'a User, tag: &'a Tag, } @@ -70,7 +71,7 @@ impl<'a> StateContext<'a> { async fn select(&self, mailbox: MailboxCodec) -> Result { let name = String::try_from(mailbox)?; - let mut mb = Mailbox::new(self.user.creds, name.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?; @@ -80,7 +81,7 @@ impl<'a> StateContext<'a> { self.inner.state.select(mb)?; - let r_unseen = Status::ok(None, Some(Code::Unseen(0)), "").map_err(Error::msg)?; + let r_unseen = Status::ok(None, Some(Code::Unseen(std::num::NonZeroU32::new(1)?)), "").map_err(Error::msg)?; //let r_permanentflags = Status::ok(None, Some(Code:: Ok(vec![ @@ -99,3 +100,4 @@ impl<'a> StateContext<'a> { ]) } } +*/ diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 61e9c1a..93592c1 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -12,7 +12,8 @@ use crate::imap::session::InnerContext; use crate::imap::flow::User; use crate::mailbox::Mailbox; -pub async fn dispatch<'a>(inner: &'a InnerContext<'a>, user: &'a User, mailbox: &'a Mailbox) -> Result { +/* +pub async fn dispatch<'a>(inner: InnerContext<'a>, user: &'a User, mailbox: &'a Mailbox) -> Result { let ctx = StateContext { inner, user, mailbox, tag: &inner.req.tag }; match ctx.inner.req.body { @@ -20,9 +21,11 @@ pub async fn dispatch<'a>(inner: &'a InnerContext<'a>, user: &'a User, mailbox: _ => authenticated::dispatch(inner, user).await, } } +*/ // --- PRIVATE --- +/* struct StateContext<'a> { inner: InnerContext<'a>, user: &'a User, @@ -43,3 +46,4 @@ impl<'a> StateContext<'a> { ]) } } +*/ diff --git a/src/imap/flow.rs b/src/imap/flow.rs index 6d8b581..9a2a2ba 100644 --- a/src/imap/flow.rs +++ b/src/imap/flow.rs @@ -1,4 +1,5 @@ - +use std::fmt; +use std::error::Error as StdError; use crate::login::Credentials; use crate::mailbox::Mailbox; @@ -8,46 +9,45 @@ pub struct User { pub creds: Credentials, } +#[derive(Debug)] +pub enum Error { + ForbiddenTransition, +} +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Forbidden Transition") + } +} +impl StdError for Error { } + + pub enum State { NotAuthenticated, Authenticated(User), Selected(User, Mailbox), Logout } -pub enum Error { - ForbiddenTransition, + +pub enum Transition { + No, + Authenticate(User), + Select(Mailbox), + Unselect, + Logout, } // See RFC3501 section 3. // https://datatracker.ietf.org/doc/html/rfc3501#page-13 impl State { - pub fn authenticate(&mut self, user: User) -> Result<(), Error> { - self = match self { - State::NotAuthenticated => State::Authenticated(user), - _ => return Err(Error::ForbiddenTransition), - }; - Ok(()) - } - - pub fn logout(&mut self) -> Self { - self = State::Logout; - Ok(()) - } - - pub fn select(&mut self, mailbox: Mailbox) -> Result<(), Error> { - self = match self { - State::Authenticated(user) => State::Selected(user, mailbox), - _ => return Err(Error::ForbiddenTransition), - }; - Ok(()) - } - - pub fn unselect(&mut self) -> Result<(), Error> { - self = match self { - State::Selected(user, _) => State::Authenticated(user), - _ => return Err(Error::ForbiddenTransition), - }; - Ok(()) + pub fn apply(self, tr: Transition) -> Result { + match (self, tr) { + (s, Transition::No) => Ok(s), + (State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)), + (State::Authenticated(u), Transition::Select(m)) => Ok(State::Selected(u, m)), + (State::Selected(u, _), Transition::Unselect) => Ok(State::Authenticated(u)), + (_, Transition::Logout) => Ok(State::Logout), + _ => Err(Error::ForbiddenTransition), + } } } diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 7e042d5..7cc4f68 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -27,7 +27,7 @@ pub async fn new( //@FIXME add a configuration parameter let incoming = AddrIncoming::new(config.bind_addr).await?; - tracing::info!("IMAP activated, will listen on {:#}", imap.incoming.local_addr); + tracing::info!("IMAP activated, will listen on {:#}", incoming.local_addr); let imap = ImapServer::new(incoming).serve(Instance::new(login.clone())); Ok(Server(imap)) diff --git a/src/imap/session.rs b/src/imap/session.rs index fccd4bf..33b138b 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -86,9 +86,9 @@ impl Manager { //----- pub struct InnerContext<'a> { - req: &'a Request, - state: &'a flow::State, - login: &'a ArcLoginProvider, + pub req: &'a Request, + pub state: &'a flow::State, + pub login: &'a ArcLoginProvider, } pub struct Instance { @@ -113,7 +113,7 @@ impl Instance { // to ease debug // fn name(&self) -> String { } - async fn start(&mut self) { + async fn start(mut self) { //@FIXME add more info about the runner tracing::debug!("starting runner"); @@ -123,28 +123,38 @@ impl Instance { // Command behavior is modulated by the state. // To prevent state error, we handle the same command in separate code path depending // on the State. - let cmd_res = match ctx.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, - flow::State::Logout => Status::bad(Some(ctx.req.tag.clone()), None, "No commands are allowed in the LOGOUT state.") - .map(|s| vec![ImapRes::Status(s)]) + 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), }; - let imap_res = cmd_res.or_else(|e| match e.downcast::() { - Ok(be) => Err(be), - Err(e) => { - tracing::warn!(error=%e, "internal.error"); - Status::bad(Some(msg.req.tag.clone()), None, "Internal error") - .map(|s| vec![ImapRes::Status(s)]) - .map_err(|e| BalError::Text(e.to_string())) + // Process result + let res = match ctrl { + Ok((res, tr)) => { + //@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::() { + Ok(be) => Err(be), + Err(e) => { + tracing::warn!(error=%e, "internal.error"); + Status::bad(Some(msg.req.tag.clone()), None, "Internal error") + .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, // and then abort the session as it is corrupted. - msg.tx.send(imap_res).unwrap_or_else(|e| { + msg.tx.send(res).unwrap_or_else(|e| { tracing::warn!("failed to send imap response to manager: {:#?}", e) }); } diff --git a/src/server.rs b/src/server.rs index 908ed11..cbe434c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -22,7 +22,10 @@ impl Server { let (login, lmtp_conf, imap_conf) = build(config)?; let lmtp_server = lmtp_conf.map(|cfg| LmtpServer::new(cfg, login.clone())); - let imap_server = imap_conf.map(|cfg| imap::new(cfg, login.clone())); + let imap_server = match imap_conf { + Some(cfg) => Some(imap::new(cfg, login.clone()).await?), + None => None, + }; Ok(Self { lmtp_server, imap_server }) } @@ -44,7 +47,7 @@ impl Server { } }, async { - match self.imap_server.as_ref() { + match self.imap_server { None => Ok(()), Some(s) => s.run(exit_signal.clone()).await, } @@ -74,7 +77,7 @@ fn build(config: Config) -> Result<(ArcLoginProvider, Option, Option (None, None) => bail!("No login provider is set up in config file"), }; - Ok(lp, config.lmtp_config, config.imap_config) + Ok((lp, config.lmtp, config.imap)) } pub fn watch_ctrl_c() -> (watch::Receiver, Arc>) {