Implement some IMAP extensions #50

Merged
quentin merged 15 commits from feat/more-ext into main 2024-01-04 11:11:02 +00:00
8 changed files with 81 additions and 18 deletions
Showing only changes of commit 6d37924399 - Show all commits

30
src/imap/capability.rs Normal file
View file

@ -0,0 +1,30 @@
use imap_codec::imap_types::core::NonEmptyVec;
use imap_codec::imap_types::response::Capability;
#[derive(Debug, Clone)]
pub struct ServerCapability {
r#move: bool,
unselect: bool,
}
impl Default for ServerCapability {
fn default() -> Self {
Self {
r#move: true,
unselect: true,
}
}
}
impl ServerCapability {
pub fn to_vec(&self) -> NonEmptyVec<Capability<'static>> {
let mut acc = vec![Capability::Imap4Rev1];
if self.r#move {
acc.push(Capability::Move);
}
if self.unselect {
acc.push(Capability::try_from("UNSELECT").unwrap());
}
acc.try_into().unwrap()
}
}

View file

@ -1,10 +1,12 @@
use anyhow::Result; use anyhow::Result;
use imap_codec::imap_types::command::{Command, CommandBody}; use imap_codec::imap_types::command::{Command, CommandBody};
use imap_codec::imap_types::core::AString; use imap_codec::imap_types::core::AString;
use imap_codec::imap_types::response::Code;
use imap_codec::imap_types::secret::Secret; use imap_codec::imap_types::secret::Secret;
use crate::imap::command::anystate; use crate::imap::command::anystate;
use crate::imap::flow; use crate::imap::flow;
use crate::imap::capability::ServerCapability;
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::login::ArcLoginProvider; use crate::login::ArcLoginProvider;
use crate::mail::user::User; use crate::mail::user::User;
@ -13,6 +15,7 @@ use crate::mail::user::User;
pub struct AnonymousContext<'a> { pub struct AnonymousContext<'a> {
pub req: &'a Command<'static>, pub req: &'a Command<'static>,
pub server_capabilities: &'a ServerCapability,
pub login_provider: &'a ArcLoginProvider, pub login_provider: &'a ArcLoginProvider,
} }
@ -20,7 +23,9 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, f
match &ctx.req.body { match &ctx.req.body {
// Any State // Any State
CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()),
CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(
ctx.req.tag.clone(),
ctx.server_capabilities),
CommandBody::Logout => anystate::logout(), CommandBody::Logout => anystate::logout(),
// Specific to anonymous context (3 commands) // Specific to anonymous context (3 commands)
@ -69,6 +74,7 @@ impl<'a> AnonymousContext<'a> {
Ok(( Ok((
Response::build() Response::build()
.to_req(self.req) .to_req(self.req)
.code(Code::Capability(self.server_capabilities.to_vec()))
.message("Completed") .message("Completed")
.ok()?, .ok()?,
flow::Transition::Authenticate(user), flow::Transition::Authenticate(user),

View file

@ -1,20 +1,16 @@
use anyhow::Result; use anyhow::Result;
use imap_codec::imap_types::core::{NonEmptyVec, Tag}; use imap_codec::imap_types::core::Tag;
use imap_codec::imap_types::response::{Capability, Data}; use imap_codec::imap_types::response::Data;
use crate::imap::flow; use crate::imap::flow;
use crate::imap::capability::ServerCapability;
use crate::imap::response::Response; use crate::imap::response::Response;
pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { pub(crate) fn capability(tag: Tag<'static>, cap: &ServerCapability) -> Result<(Response<'static>, flow::Transition)> {
let capabilities: NonEmptyVec<Capability> = (vec![
Capability::Imap4Rev1,
Capability::try_from("UNSELECT").unwrap(),
])
.try_into()?;
let res = Response::build() let res = Response::build()
.tag(tag) .tag(tag)
.message("Server capabilities") .message("Server capabilities")
.data(Data::Capability(capabilities)) .data(Data::Capability(cap.to_vec()))
.ok()?; .ok()?;
Ok((res, flow::Transition::None)) Ok((res, flow::Transition::None))

View file

@ -14,6 +14,7 @@ use crate::imap::command::{anystate, MailboxName};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::MailboxView;
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::imap::capability::ServerCapability;
use crate::mail::mailbox::Mailbox; use crate::mail::mailbox::Mailbox;
use crate::mail::uidindex::*; use crate::mail::uidindex::*;
@ -22,6 +23,7 @@ use crate::mail::IMF;
pub struct AuthenticatedContext<'a> { pub struct AuthenticatedContext<'a> {
pub req: &'a Command<'static>, pub req: &'a Command<'static>,
pub server_capabilities: &'a ServerCapability,
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
} }
@ -31,7 +33,9 @@ pub async fn dispatch<'a>(
match &ctx.req.body { match &ctx.req.body {
// Any state // Any state
CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()),
CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(
ctx.req.tag.clone(),
ctx.server_capabilities),
CommandBody::Logout => anystate::logout(), CommandBody::Logout => anystate::logout(),
// Specific to this state (11 commands) // Specific to this state (11 commands)

View file

@ -11,19 +11,24 @@ use crate::imap::command::{anystate, authenticated};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::MailboxView;
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::imap::capability::ServerCapability;
use crate::mail::user::User; use crate::mail::user::User;
pub struct ExaminedContext<'a> { pub struct ExaminedContext<'a> {
pub req: &'a Command<'static>, pub req: &'a Command<'static>,
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
pub mailbox: &'a mut MailboxView, pub mailbox: &'a mut MailboxView,
pub server_capabilities: &'a ServerCapability,
} }
pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> { pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> {
match &ctx.req.body { match &ctx.req.body {
// Any State // Any State
// noop is specific to this state // noop is specific to this state
CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(
ctx.req.tag.clone(),
ctx.server_capabilities,
),
CommandBody::Logout => anystate::logout(), CommandBody::Logout => anystate::logout(),
// Specific to the EXAMINE state (specialization of the SELECTED state) // Specific to the EXAMINE state (specialization of the SELECTED state)
@ -55,6 +60,7 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl
_ => { _ => {
authenticated::dispatch(authenticated::AuthenticatedContext { authenticated::dispatch(authenticated::AuthenticatedContext {
req: ctx.req, req: ctx.req,
server_capabilities: ctx.server_capabilities,
user: ctx.user, user: ctx.user,
}) })
.await .await

View file

@ -14,6 +14,7 @@ use crate::imap::command::{anystate, authenticated, MailboxName};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::MailboxView;
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::imap::capability::ServerCapability;
use crate::mail::user::User; use crate::mail::user::User;
@ -21,6 +22,7 @@ pub struct SelectedContext<'a> {
pub req: &'a Command<'static>, pub req: &'a Command<'static>,
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
pub mailbox: &'a mut MailboxView, pub mailbox: &'a mut MailboxView,
pub server_capabilities: &'a ServerCapability,
} }
pub async fn dispatch<'a>( pub async fn dispatch<'a>(
@ -29,7 +31,10 @@ pub async fn dispatch<'a>(
match &ctx.req.body { match &ctx.req.body {
// Any State // Any State
// noop is specific to this state // noop is specific to this state
CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(
ctx.req.tag.clone(),
ctx.server_capabilities,
),
CommandBody::Logout => anystate::logout(), CommandBody::Logout => anystate::logout(),
// Specific to this state (7 commands + NOOP) // Specific to this state (7 commands + NOOP)
@ -66,6 +71,7 @@ pub async fn dispatch<'a>(
_ => { _ => {
authenticated::dispatch(authenticated::AuthenticatedContext { authenticated::dispatch(authenticated::AuthenticatedContext {
req: ctx.req, req: ctx.req,
server_capabilities: ctx.server_capabilities,
user: ctx.user, user: ctx.user,
}) })
.await .await

View file

@ -3,6 +3,7 @@ mod flow;
mod mailbox_view; mod mailbox_view;
mod response; mod response;
mod session; mod session;
mod capability;
use std::net::SocketAddr; use std::net::SocketAddr;
@ -17,12 +18,14 @@ use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions};
use imap_flow::stream::AnyStream; use imap_flow::stream::AnyStream;
use crate::config::ImapConfig; use crate::config::ImapConfig;
use crate::imap::capability::ServerCapability;
use crate::login::ArcLoginProvider; 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 { pub struct Server {
bind_addr: SocketAddr, bind_addr: SocketAddr,
login_provider: ArcLoginProvider, login_provider: ArcLoginProvider,
capabilities: ServerCapability,
} }
struct ClientContext { struct ClientContext {
@ -30,12 +33,14 @@ struct ClientContext {
addr: SocketAddr, addr: SocketAddr,
login_provider: ArcLoginProvider, login_provider: ArcLoginProvider,
must_exit: watch::Receiver<bool>, must_exit: watch::Receiver<bool>,
server_capabilities: ServerCapability,
} }
pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server { pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server {
Server { Server {
bind_addr: config.bind_addr, bind_addr: config.bind_addr,
login_provider: login, login_provider: login,
capabilities: ServerCapability::default(),
} }
} }
@ -66,6 +71,7 @@ impl Server {
addr: remote_addr.clone(), addr: remote_addr.clone(),
login_provider: self.login_provider.clone(), login_provider: self.login_provider.clone(),
must_exit: must_exit.clone(), must_exit: must_exit.clone(),
server_capabilities: self.capabilities.clone(),
}; };
let conn = tokio::spawn(client_wrapper(client)); let conn = tokio::spawn(client_wrapper(client));
connections.push(conn); connections.push(conn);
@ -96,21 +102,21 @@ async fn client(mut ctx: ClientContext) -> Result<()> {
let (mut server, _) = ServerFlow::send_greeting( let (mut server, _) = ServerFlow::send_greeting(
ctx.stream, ctx.stream,
ServerFlowOptions::default(), ServerFlowOptions::default(),
Greeting::ok(None, "Aerogramme").unwrap(), Greeting::ok(Some(Code::Capability(ctx.server_capabilities.to_vec())), "Aerogramme").unwrap(),
) )
.await?; .await?;
use crate::imap::response::{Body, Response as MyResponse}; use crate::imap::response::{Body, Response as MyResponse};
use crate::imap::session::Instance; use crate::imap::session::Instance;
use imap_codec::imap_types::command::Command; use imap_codec::imap_types::command::Command;
use imap_codec::imap_types::response::{Response, Status}; use imap_codec::imap_types::response::{Response, Code, Status};
use tokio::sync::mpsc; use tokio::sync::mpsc;
let (cmd_tx, mut cmd_rx) = mpsc::channel::<Command<'static>>(10); let (cmd_tx, mut cmd_rx) = mpsc::channel::<Command<'static>>(10);
let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::<MyResponse<'static>>(); let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::<MyResponse<'static>>();
let bckgrnd = tokio::spawn(async move { let bckgrnd = tokio::spawn(async move {
let mut session = Instance::new(ctx.login_provider); let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities);
loop { loop {
let cmd = match cmd_rx.recv().await { let cmd = match cmd_rx.recv().await {
None => break, None => break,

View file

@ -1,19 +1,22 @@
use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::command::{anonymous, authenticated, examined, selected};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::imap::capability::ServerCapability;
use crate::login::ArcLoginProvider; use crate::login::ArcLoginProvider;
use imap_codec::imap_types::command::Command; use imap_codec::imap_types::command::Command;
//----- //-----
pub struct Instance { pub struct Instance {
pub login_provider: ArcLoginProvider, pub login_provider: ArcLoginProvider,
pub server_capabilities: ServerCapability,
pub state: flow::State, pub state: flow::State,
} }
impl Instance { impl Instance {
pub fn new(login_provider: ArcLoginProvider) -> Self { pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self {
Self { Self {
login_provider, login_provider,
state: flow::State::NotAuthenticated, state: flow::State::NotAuthenticated,
server_capabilities: cap,
} }
} }
@ -25,16 +28,21 @@ impl Instance {
let ctx = anonymous::AnonymousContext { let ctx = anonymous::AnonymousContext {
req: &cmd, req: &cmd,
login_provider: &self.login_provider, login_provider: &self.login_provider,
server_capabilities: &self.server_capabilities,
}; };
anonymous::dispatch(ctx).await anonymous::dispatch(ctx).await
} }
flow::State::Authenticated(ref user) => { flow::State::Authenticated(ref user) => {
let ctx = authenticated::AuthenticatedContext { req: &cmd, user }; let ctx = authenticated::AuthenticatedContext {
req: &cmd,
server_capabilities: &self.server_capabilities,
user };
authenticated::dispatch(ctx).await authenticated::dispatch(ctx).await
} }
flow::State::Selected(ref user, ref mut mailbox) => { flow::State::Selected(ref user, ref mut mailbox) => {
let ctx = selected::SelectedContext { let ctx = selected::SelectedContext {
req: &cmd, req: &cmd,
server_capabilities: &self.server_capabilities,
user, user,
mailbox, mailbox,
}; };
@ -43,6 +51,7 @@ impl Instance {
flow::State::Examined(ref user, ref mut mailbox) => { flow::State::Examined(ref user, ref mut mailbox) => {
let ctx = examined::ExaminedContext { let ctx = examined::ExaminedContext {
req: &cmd, req: &cmd,
server_capabilities: &self.server_capabilities,
user, user,
mailbox, mailbox,
}; };