Refactor to allow mutability

This commit is contained in:
Alex 2022-06-29 12:50:44 +02:00
parent 9979671b00
commit 90b143e1c5
Signed by: lx
GPG Key ID: 0E496D15096376BE
9 changed files with 163 additions and 125 deletions

6
Cargo.lock generated
View File

@ -2106,7 +2106,7 @@ dependencies = [
[[package]] [[package]]
name = "smtp-message" name = "smtp-message"
version = "0.1.0" version = "0.1.0"
source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#245cd13212db727d4085768b813a0ee09a137bc3" source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0a5ceb0f9a99d76d72bf105ee4df1f11629d812a"
dependencies = [ dependencies = [
"auto_enums", "auto_enums",
"futures", "futures",
@ -2121,7 +2121,7 @@ dependencies = [
[[package]] [[package]]
name = "smtp-server" name = "smtp-server"
version = "0.1.0" version = "0.1.0"
source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#245cd13212db727d4085768b813a0ee09a137bc3" source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0a5ceb0f9a99d76d72bf105ee4df1f11629d812a"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@ -2135,7 +2135,7 @@ dependencies = [
[[package]] [[package]]
name = "smtp-server-types" name = "smtp-server-types"
version = "0.1.0" version = "0.1.0"
source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#245cd13212db727d4085768b813a0ee09a137bc3" source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0a5ceb0f9a99d76d72bf105ee4df1f11629d812a"
dependencies = [ dependencies = [
"serde", "serde",
"smtp-message", "smtp-message",

View File

@ -1,76 +1,97 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use boitalettres::proto::{res::body::Data as Body, Response}; use boitalettres::proto::{res::body::Data as Body, Request, Response};
use imap_codec::types::command::CommandBody; use imap_codec::types::command::CommandBody;
use imap_codec::types::core::{AString, Atom}; use imap_codec::types::core::{AString, Atom};
use imap_codec::types::response::{Capability, Code, Data, Response as ImapRes, Status}; use imap_codec::types::response::{Capability, Code, Data, Response as ImapRes, Status};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::session::InnerContext; use crate::login::ArcLoginProvider;
//--- dispatching //--- dispatching
pub async fn dispatch<'a>(ctx: InnerContext<'a>) -> Result<(Response, flow::Transition)> { pub struct AnonymousContext<'a> {
pub req: &'a Request,
pub login_provider: Option<&'a ArcLoginProvider>,
}
pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response, flow::Transition)> {
match &ctx.req.command.body { match &ctx.req.command.body {
CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::No)), CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)),
CommandBody::Capability => capability(ctx).await, CommandBody::Capability => ctx.capability().await,
CommandBody::Logout => logout(ctx).await, CommandBody::Logout => ctx.logout().await,
CommandBody::Login { username, password } => login(ctx, username, password).await, CommandBody::Login { username, password } => ctx.login(username, password).await,
_ => Ok(( _ => Ok((
Response::no("This command is not available in the ANONYMOUS state.")?, Response::no("This command is not available in the ANONYMOUS state.")?,
flow::Transition::No, flow::Transition::None,
)), )),
} }
} }
//--- Command controllers, private //--- Command controllers, private
async fn capability<'a>(ctx: InnerContext<'a>) -> Result<(Response, flow::Transition)> { impl<'a> AnonymousContext<'a> {
let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; async fn capability(self) -> Result<(Response, flow::Transition)> {
let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities)); let capabilities = vec![Capability::Imap4Rev1, Capability::Idle];
Ok((res, flow::Transition::No)) let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities));
} Ok((res, flow::Transition::None))
}
async fn login<'a>(
ctx: InnerContext<'a>, async fn login(
username: &AString, self,
password: &AString, username: &AString,
) -> Result<(Response, flow::Transition)> { password: &AString,
let (u, p) = ( ) -> Result<(Response, flow::Transition)> {
String::try_from(username.clone())?, let (u, p) = (
String::try_from(password.clone())?, String::try_from(username.clone())?,
); String::try_from(password.clone())?,
tracing::info!(user = %u, "command.login"); );
tracing::info!(user = %u, "command.login");
let creds = match ctx.login.login(&u, &p).await {
Err(e) => { let login_provider = match &self.login_provider {
tracing::debug!(error=%e, "authentication failed"); Some(lp) => lp,
return Ok((Response::no("Authentication failed")?, flow::Transition::No)); None => {
} return Ok((
Ok(c) => c, Response::no("Login command not available (already logged in)")?,
}; flow::Transition::None,
))
let user = flow::User { }
creds, };
name: u.clone(),
}; let creds = match login_provider.login(&u, &p).await {
Err(e) => {
tracing::info!(username=%u, "connected"); tracing::debug!(error=%e, "authentication failed");
Ok(( return Ok((
Response::ok("Completed")?, Response::no("Authentication failed")?,
flow::Transition::Authenticate(user), flow::Transition::None,
)) ));
} }
// C: 10 logout Ok(c) => c,
// S: * BYE Logging out };
// S: 10 OK Logout completed.
async fn logout<'a>(ctx: InnerContext<'a>) -> Result<(Response, flow::Transition)> { let user = flow::User {
// @FIXME we should implement From<Vec<Status>> and From<Vec<ImapStatus>> in creds,
// boitalettres/src/proto/res/body.rs name: u.clone(),
Ok(( };
Response::ok("Logout completed")?.with_body(vec![Body::Status(
Status::bye(None, "Logging out") tracing::info!(username=%u, "connected");
.map_err(|e| Error::msg(e).context("Unable to generate IMAP status"))?, Ok((
)]), Response::ok("Completed")?,
flow::Transition::Logout, 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,
))
}
} }

View File

@ -1,5 +1,5 @@
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use boitalettres::proto::{res::body::Data as Body, Response}; use boitalettres::proto::{res::body::Data as Body, Request, Response};
use imap_codec::types::command::CommandBody; use imap_codec::types::command::CommandBody;
use imap_codec::types::core::Atom; use imap_codec::types::core::Atom;
use imap_codec::types::flag::Flag; use imap_codec::types::flag::Flag;
@ -19,13 +19,13 @@ const DEFAULT_FLAGS: [Flag; 5] = [
Flag::Draft, Flag::Draft,
]; ];
pub async fn dispatch<'a>( pub struct AuthenticatedContext<'a> {
inner: InnerContext<'a>, pub req: &'a Request,
user: &'a flow::User, pub user: &'a flow::User,
) -> Result<(Response, flow::Transition)> { }
let ctx = StateContext { user, inner };
match &ctx.inner.req.command.body { pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, flow::Transition)> {
match &ctx.req.command.body {
CommandBody::Lsub { CommandBody::Lsub {
reference, reference,
mailbox_wildcard, mailbox_wildcard,
@ -35,32 +35,33 @@ pub async fn dispatch<'a>(
mailbox_wildcard, mailbox_wildcard,
} => ctx.list(reference, mailbox_wildcard).await, } => ctx.list(reference, mailbox_wildcard).await,
CommandBody::Select { mailbox } => ctx.select(mailbox).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await,
_ => anonymous::dispatch(ctx.inner).await, _ => {
let ctx = anonymous::AnonymousContext {
req: ctx.req,
login_provider: None,
};
anonymous::dispatch(ctx).await
}
} }
} }
// --- PRIVATE --- // --- PRIVATE ---
struct StateContext<'a> { impl<'a> AuthenticatedContext<'a> {
inner: InnerContext<'a>,
user: &'a flow::User,
}
impl<'a> StateContext<'a> {
async fn lsub( async fn lsub(
&self, self,
reference: &MailboxCodec, reference: &MailboxCodec,
mailbox_wildcard: &ListMailbox, mailbox_wildcard: &ListMailbox,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response, flow::Transition)> {
Ok((Response::bad("Not implemented")?, flow::Transition::No)) Ok((Response::bad("Not implemented")?, flow::Transition::None))
} }
async fn list( async fn list(
&self, self,
reference: &MailboxCodec, reference: &MailboxCodec,
mailbox_wildcard: &ListMailbox, mailbox_wildcard: &ListMailbox,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response, flow::Transition)> {
Ok((Response::bad("Not implemented")?, flow::Transition::No)) Ok((Response::bad("Not implemented")?, flow::Transition::None))
} }
/* /*
@ -91,7 +92,7 @@ impl<'a> StateContext<'a> {
* TRACE END --- * TRACE END ---
*/ */
async fn select(&self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
let name = String::try_from(mailbox.clone())?; let name = String::try_from(mailbox.clone())?;
let mut mb = Mailbox::new(&self.user.creds, name.clone())?; let mut mb = Mailbox::new(&self.user.creds, name.clone())?;

View File

@ -1,4 +1,5 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use boitalettres::proto::Request;
use boitalettres::proto::Response; use boitalettres::proto::Response;
use imap_codec::types::command::CommandBody; use imap_codec::types::command::CommandBody;
use imap_codec::types::core::Tag; use imap_codec::types::core::Tag;
@ -11,42 +12,38 @@ use crate::imap::flow;
use crate::imap::session::InnerContext; use crate::imap::session::InnerContext;
use crate::mail::Mailbox; use crate::mail::Mailbox;
pub async fn dispatch<'a>( pub struct SelectedContext<'a> {
inner: InnerContext<'a>, pub req: &'a Request,
user: &'a flow::User, pub user: &'a flow::User,
mailbox: &'a Mailbox, pub mailbox: &'a mut Mailbox,
) -> Result<(Response, flow::Transition)> { }
let ctx = StateContext {
inner,
user,
mailbox,
};
match &ctx.inner.req.command.body { pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response, flow::Transition)> {
match &ctx.req.command.body {
CommandBody::Fetch { CommandBody::Fetch {
sequence_set, sequence_set,
attributes, attributes,
uid, uid,
} => ctx.fetch(sequence_set, attributes, uid).await, } => ctx.fetch(sequence_set, attributes, uid).await,
_ => authenticated::dispatch(ctx.inner, user).await, _ => {
let ctx = authenticated::AuthenticatedContext {
req: ctx.req,
user: ctx.user,
};
authenticated::dispatch(ctx).await
}
} }
} }
// --- PRIVATE --- // --- PRIVATE ---
struct StateContext<'a> { impl<'a> SelectedContext<'a> {
inner: InnerContext<'a>,
user: &'a flow::User,
mailbox: &'a Mailbox,
}
impl<'a> StateContext<'a> {
pub async fn fetch( pub async fn fetch(
&self, self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
attributes: &MacroOrFetchAttributes, attributes: &MacroOrFetchAttributes,
uid: &bool, uid: &bool,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response, flow::Transition)> {
Ok((Response::bad("Not implemented")?, flow::Transition::No)) Ok((Response::bad("Not implemented")?, flow::Transition::None))
} }
} }

View File

@ -28,7 +28,7 @@ pub enum State {
} }
pub enum Transition { pub enum Transition {
No, None,
Authenticate(User), Authenticate(User),
Select(Mailbox), Select(Mailbox),
Unselect, Unselect,
@ -40,7 +40,7 @@ pub enum Transition {
impl State { impl State {
pub fn apply(self, tr: Transition) -> Result<Self, Error> { pub fn apply(self, tr: Transition) -> Result<Self, Error> {
match (self, tr) { match (self, tr) {
(s, Transition::No) => Ok(s), (s, Transition::None) => Ok(s),
(State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)), (State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)),
(State::Authenticated(u), Transition::Select(m)) => Ok(State::Selected(u, m)), (State::Authenticated(u), Transition::Select(m)) => Ok(State::Selected(u, m)),
(State::Selected(u, _), Transition::Unselect) => Ok(State::Authenticated(u)), (State::Selected(u, _), Transition::Unselect) => Ok(State::Authenticated(u)),

View File

@ -20,6 +20,7 @@ use crate::login::ArcLoginProvider;
/// Server is a thin wrapper to register our Services in BàL /// Server is a thin wrapper to register our Services in BàL
pub struct Server(ImapServer<AddrIncoming, Instance>); 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 //@FIXME add a configuration parameter
let incoming = AddrIncoming::new(config.bind_addr).await?; let incoming = AddrIncoming::new(config.bind_addr).await?;
@ -28,6 +29,7 @@ pub async fn new(config: ImapConfig, login: ArcLoginProvider) -> Result<Server>
let imap = ImapServer::new(incoming).serve(Instance::new(login.clone())); let imap = ImapServer::new(incoming).serve(Instance::new(login.clone()));
Ok(Server(imap)) Ok(Server(imap))
} }
impl Server { impl Server {
pub async fn run(self, mut must_exit: watch::Receiver<bool>) -> Result<()> { pub async fn run(self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
tracing::info!("IMAP started!"); tracing::info!("IMAP started!");
@ -47,11 +49,13 @@ impl Server {
struct Instance { struct Instance {
login_provider: ArcLoginProvider, login_provider: ArcLoginProvider,
} }
impl Instance { impl Instance {
pub fn new(login_provider: ArcLoginProvider) -> Self { pub fn new(login_provider: ArcLoginProvider) -> Self {
Self { login_provider } Self { login_provider }
} }
} }
impl<'a> Service<&'a AddrStream> for Instance { impl<'a> Service<&'a AddrStream> for Instance {
type Response = Connection; type Response = Connection;
type Error = anyhow::Error; type Error = anyhow::Error;
@ -75,6 +79,7 @@ impl<'a> Service<&'a AddrStream> for Instance {
struct Connection { struct Connection {
session: session::Manager, session: session::Manager,
} }
impl Connection { impl Connection {
pub fn new(login_provider: ArcLoginProvider) -> Self { pub fn new(login_provider: ArcLoginProvider) -> Self {
Self { Self {
@ -82,6 +87,7 @@ impl Connection {
} }
} }
} }
impl Service<Request> for Connection { impl Service<Request> for Connection {
type Response = Response; type Response = Response;
type Error = BalError; type Error = BalError;

View File

@ -102,23 +102,36 @@ impl Instance {
tracing::debug!("starting runner"); tracing::debug!("starting runner");
while let Some(msg) = self.rx.recv().await { while let Some(msg) = self.rx.recv().await {
let ctx = InnerContext {
req: &msg.req,
state: &self.state,
login: &self.login_provider,
};
// Command behavior is modulated by the state. // Command behavior is modulated by the state.
// To prevent state error, we handle the same command in separate code paths. // To prevent state error, we handle the same command in separate code paths.
let ctrl = match &self.state { let ctrl = match &mut self.state {
flow::State::NotAuthenticated => anonymous::dispatch(ctx).await, flow::State::NotAuthenticated => {
flow::State::Authenticated(user) => authenticated::dispatch(ctx, user).await, let ctx = anonymous::AnonymousContext {
flow::State::Selected(user, mailbox) => { req: &msg.req,
selected::dispatch(ctx, user, mailbox).await 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::Logout => {
Response::bad("No commands are allowed in the LOGOUT state.")
.map(|r| (r, flow::Transition::None))
.map_err(Error::msg)
} }
_ => Response::bad("No commands are allowed in the LOGOUT state.")
.map(|r| (r, flow::Transition::No))
.map_err(Error::msg),
}; };
// Process result // Process result

View File

@ -42,6 +42,8 @@ impl LmtpServer {
pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> { pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?; let tcp = TcpListener::bind(self.bind_addr).await?;
info!("LMTP server listening on {:#}", self.bind_addr);
let mut connections = FuturesUnordered::new(); let mut connections = FuturesUnordered::new();
while !*must_exit.borrow() { while !*must_exit.borrow() {
@ -155,17 +157,14 @@ impl Config for LmtpServer {
} }
} }
async fn handle_mail<'a, 'slife0, 'slife1, 'stream, R>( async fn handle_mail<'resp, R>(
&'slife0 self, &'resp self,
reader: &mut EscapedDataReader<'a, R>, reader: &mut EscapedDataReader<'_, R>,
meta: MailMetadata<Message>, meta: MailMetadata<Message>,
conn_meta: &'slife1 mut ConnectionMetadata<Conn>, conn_meta: &'resp mut ConnectionMetadata<Conn>,
) -> Pin<Box<dyn futures::Stream<Item = Decision<()>> + Send + 'stream>> ) -> Pin<Box<dyn futures::Stream<Item = Decision<()>> + Send + 'resp>>
where where
R: Send + Unpin + AsyncRead, R: Send + Unpin + AsyncRead,
'slife0: 'stream,
'slife1: 'stream,
Self: 'stream,
{ {
let err_response_stream = |meta: MailMetadata<Message>, msg: String| { let err_response_stream = |meta: MailMetadata<Message>, msg: String| {
Box::pin( Box::pin(

View File

@ -10,12 +10,12 @@ pub type ImapUid = NonZeroU32;
pub type ImapUidvalidity = NonZeroU32; pub type ImapUidvalidity = NonZeroU32;
pub type Flag = String; pub type Flag = String;
#[derive(Clone)]
/// A UidIndex handles the mutable part of a mailbox /// A UidIndex handles the mutable part of a mailbox
/// It is built by running the event log on it /// It is built by running the event log on it
/// Each applied log generates a new UidIndex by cloning the previous one /// Each applied log generates a new UidIndex by cloning the previous one
/// and applying the event. This is why we use immutable datastructures: /// and applying the event. This is why we use immutable datastructures:
/// they are cheap to clone. /// they are cheap to clone.
#[derive(Clone)]
pub struct UidIndex { pub struct UidIndex {
// Source of trust // Source of trust
pub table: OrdMap<MailIdent, (ImapUid, Vec<Flag>)>, pub table: OrdMap<MailIdent, (ImapUid, Vec<Flag>)>,
@ -162,6 +162,7 @@ impl BayouState for UidIndex {
} }
// ---- FlagIndex implementation ---- // ---- FlagIndex implementation ----
#[derive(Clone)] #[derive(Clone)]
pub struct FlagIndex(HashMap<Flag, OrdSet<ImapUid>>); pub struct FlagIndex(HashMap<Flag, OrdSet<ImapUid>>);
pub type FlagIter<'a> = im::hashmap::Keys<'a, Flag, OrdSet<ImapUid>>; pub type FlagIter<'a> = im::hashmap::Keys<'a, Flag, OrdSet<ImapUid>>;