Implement some IMAP extensions #50
8 changed files with 81 additions and 18 deletions
30
src/imap/capability.rs
Normal file
30
src/imap/capability.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue