Merge pull request 'Implement imap-flow' (#34) from refactor/imap-flow into main

Reviewed-on: #34
This commit is contained in:
Quentin 2024-01-02 22:44:29 +00:00
commit b9a0c1e6ec
31 changed files with 2415 additions and 1647 deletions

1373
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,48 +7,67 @@ license = "AGPL-3.0"
description = "Encrypted mail storage over Garage" description = "Encrypted mail storage over Garage"
[dependencies] [dependencies]
aws-config = { version = "1.1.1", features = ["behavior-version-latest"] } # async runtime
aws-sdk-s3 = "1.9.0"
anyhow = "1.0.28"
argon2 = "0.5"
async-trait = "0.1"
backtrace = "0.3"
base64 = "0.21"
clap = { version = "3.1.18", features = ["derive", "env"] }
duplexify = "1.1.0"
eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" }
hex = "0.4"
futures = "0.3"
im = "15"
itertools = "0.10"
lazy_static = "1.4"
ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] }
log = "0.4"
hyper-rustls = { version = "0.24", features = ["http2"] }
nix = { version = "0.27", features = ["signal"] }
serde = "1.0.137"
rand = "0.8.5"
rmp-serde = "0.15"
rpassword = "7.0"
sodiumoxide = "0.2"
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
tokio-util = { version = "0.7", features = [ "compat" ] } tokio-util = { version = "0.7", features = [ "compat" ] }
toml = "0.5" futures = "0.3"
zstd = { version = "0.9", default-features = false }
# debug
log = "0.4"
backtrace = "0.3"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
tracing = "0.1" tracing = "0.1"
tower = "0.4"
imap-codec = { git = "https://github.com/superboum/imap-codec.git", branch = "v0.5.x" } # language extensions
lazy_static = "1.4"
duplexify = "1.1.0"
im = "15"
anyhow = "1.0.28"
async-trait = "0.1"
itertools = "0.10"
chrono = { version = "0.4", default-features = false, features = ["alloc"] } chrono = { version = "0.4", default-features = false, features = ["alloc"] }
# process related
nix = { version = "0.27", features = ["signal"] }
clap = { version = "3.1.18", features = ["derive", "env"] }
# serialization
serde = "1.0.137"
rmp-serde = "0.15"
toml = "0.5"
base64 = "0.21"
hex = "0.4"
zstd = { version = "0.9", default-features = false }
# cryptography & security
sodiumoxide = "0.2"
argon2 = "0.5"
rand = "0.8.5"
hyper-rustls = { version = "0.24", features = ["http2"] }
rpassword = "7.0"
# login
ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] }
# storage
k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", tag = "v0.9.0" } k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", tag = "v0.9.0" }
boitalettres = { git = "https://git.deuxfleurs.fr/quentin/boitalettres.git", branch = "expose-mydatetime" } aws-config = { version = "1.1.1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1.9.0"
# email protocols
eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" }
smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
imap-codec = { version = "1.0.0", features = ["quirk_crlf_relaxed", "bounded-static"] }
#k2v-client = { path = "../garage/src/k2v-client" } imap-flow = { git = "https://github.com/duesee/imap-flow.git", rev = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" }
[dev-dependencies] [dev-dependencies]
[patch.crates-io]
imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" }
imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" }
[[test]]
name = "imap_features"
path = "tests/imap_features.rs"
harness = false

View file

@ -450,10 +450,7 @@ impl K2vWatch {
) { ) {
let mut row = match Weak::upgrade(&self_weak) { let mut row = match Weak::upgrade(&self_weak) {
Some(this) => this.target.clone(), Some(this) => this.target.clone(),
None => { None => return,
error!("can't start loop");
return;
}
}; };
while let Some(this) = Weak::upgrade(&self_weak) { while let Some(this) = Weak::upgrade(&self_weak) {

View file

@ -26,6 +26,7 @@ pub struct ProviderConfig {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "user_driver")] #[serde(tag = "user_driver")]
pub enum UserManagement { pub enum UserManagement {
Demo,
Static(LoginStaticConfig), Static(LoginStaticConfig),
Ldap(LoginLdapConfig), Ldap(LoginLdapConfig),
} }

View file

@ -1,174 +0,0 @@
Command::FirstLogin {
creds,
user_secrets,
} => {
let creds = make_storage_creds(creds);
let user_secrets = make_user_secrets(user_secrets);
println!("Please enter your password for key decryption.");
println!("If you are using LDAP login, this must be your LDAP password.");
println!("If you are using the static login provider, enter any password, and this will also become your password for local IMAP access.");
let password = rpassword::prompt_password("Enter password: ")?;
let password_confirm = rpassword::prompt_password("Confirm password: ")?;
if password != password_confirm {
bail!("Passwords don't match.");
}
CryptoKeys::init(&creds, &user_secrets, &password).await?;
println!("");
println!("Cryptographic key setup is complete.");
println!("");
println!("If you are using the static login provider, add the following section to your .toml configuration file:");
println!("");
dump_config(&password, &creds);
}
Command::InitializeLocalKeys { creds } => {
let creds = make_storage_creds(creds);
println!("Please enter a password for local IMAP access.");
println!("This password is not used for key decryption, your keys will be printed below (do not lose them!)");
println!(
"If you plan on using LDAP login, stop right here and use `first-login` instead"
);
let password = rpassword::prompt_password("Enter password: ")?;
let password_confirm = rpassword::prompt_password("Confirm password: ")?;
if password != password_confirm {
bail!("Passwords don't match.");
}
let master = gen_key();
let (_, secret) = gen_keypair();
let keys = CryptoKeys::init_without_password(&creds, &master, &secret).await?;
println!("");
println!("Cryptographic key setup is complete.");
println!("");
println!("Add the following section to your .toml configuration file:");
println!("");
dump_config(&password, &creds);
dump_keys(&keys);
}
Command::AddPassword {
creds,
user_secrets,
gen,
} => {
let creds = make_storage_creds(creds);
let user_secrets = make_user_secrets(user_secrets);
let existing_password =
rpassword::prompt_password("Enter existing password to decrypt keys: ")?;
let new_password = if gen {
let password = base64::encode_config(
&u128::to_be_bytes(thread_rng().gen())[..10],
base64::URL_SAFE_NO_PAD,
);
println!("Your new password: {}", password);
println!("Keep it safe!");
password
} else {
let password = rpassword::prompt_password("Enter new password: ")?;
let password_confirm = rpassword::prompt_password("Confirm new password: ")?;
if password != password_confirm {
bail!("Passwords don't match.");
}
password
};
let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?;
keys.add_password(&creds, &user_secrets, &new_password)
.await?;
println!("");
println!("New password added successfully.");
}
Command::DeletePassword {
creds,
user_secrets,
allow_delete_all,
} => {
let creds = make_storage_creds(creds);
let user_secrets = make_user_secrets(user_secrets);
let existing_password = rpassword::prompt_password("Enter password to delete: ")?;
let keys = match allow_delete_all {
true => Some(CryptoKeys::open(&creds, &user_secrets, &existing_password).await?),
false => None,
};
CryptoKeys::delete_password(&creds, &existing_password, allow_delete_all).await?;
println!("");
println!("Password was deleted successfully.");
if let Some(keys) = keys {
println!("As a reminder, here are your cryptographic keys:");
dump_keys(&keys);
}
}
Command::ShowKeys {
creds,
user_secrets,
} => {
let creds = make_storage_creds(creds);
let user_secrets = make_user_secrets(user_secrets);
let existing_password = rpassword::prompt_password("Enter key decryption password: ")?;
let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?;
dump_keys(&keys);
}
}
Ok(())
}
fn make_storage_creds(c: StorageCredsArgs) -> StorageCredentials {
let s3_region = Region {
name: c.region.clone(),
endpoint: c.s3_endpoint,
};
let k2v_region = Region {
name: c.region,
endpoint: c.k2v_endpoint,
};
StorageCredentials {
k2v_region,
s3_region,
aws_access_key_id: c.aws_access_key_id,
aws_secret_access_key: c.aws_secret_access_key,
bucket: c.bucket,
}
}
fn make_user_secrets(c: UserSecretsArgs) -> UserSecrets {
UserSecrets {
user_secret: c.user_secret,
alternate_user_secrets: c
.alternate_user_secrets
.split(',')
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.map(|x| x.to_string())
.collect(),
}
}
fn dump_config(password: &str, creds: &StorageCredentials) {
println!("[login_static.users.<username>]");
println!(
"password = \"{}\"",
hash_password(password).expect("unable to hash password")
);
println!("aws_access_key_id = \"{}\"", creds.aws_access_key_id);
println!(
"aws_secret_access_key = \"{}\"",
creds.aws_secret_access_key
);
}
fn dump_keys(keys: &CryptoKeys) {
println!("master_key = \"{}\"", base64::encode(&keys.master));
println!("secret_key = \"{}\"", base64::encode(&keys.secret));
}

View file

@ -1,92 +1,77 @@
use anyhow::{Error, Result}; use anyhow::Result;
use boitalettres::proto::{res::body::Data as Body, Request, Response}; use imap_codec::imap_types::command::{Command, CommandBody};
use imap_codec::types::command::CommandBody; use imap_codec::imap_types::core::AString;
use imap_codec::types::core::AString; use imap_codec::imap_types::secret::Secret;
use imap_codec::types::response::{Capability, Data, Status};
use crate::imap::command::anystate;
use crate::imap::flow; use crate::imap::flow;
use crate::imap::response::Response;
use crate::login::ArcLoginProvider; use crate::login::ArcLoginProvider;
use crate::mail::user::User; use crate::mail::user::User;
//--- dispatching //--- dispatching
pub struct AnonymousContext<'a> { pub struct AnonymousContext<'a> {
pub req: &'a Request, pub req: &'a Command<'static>,
pub login_provider: Option<&'a ArcLoginProvider>, pub login_provider: &'a ArcLoginProvider,
} }
pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, flow::Transition)> {
match &ctx.req.command.body { match &ctx.req.body {
CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)), // Any State
CommandBody::Capability => ctx.capability().await, CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()),
CommandBody::Logout => ctx.logout().await, CommandBody::Capability => anystate::capability(ctx.req.tag.clone()),
CommandBody::Logout => anystate::logout(),
// Specific to anonymous context (3 commands)
CommandBody::Login { username, password } => ctx.login(username, password).await, CommandBody::Login { username, password } => ctx.login(username, password).await,
_ => Ok((Response::no("Command unavailable")?, flow::Transition::None)), CommandBody::Authenticate { .. } => {
anystate::not_implemented(ctx.req.tag.clone(), "authenticate")
}
//StartTLS is not implemented for now, we will probably go full TLS.
// Collect other commands
_ => anystate::wrong_state(ctx.req.tag.clone()),
} }
} }
//--- Command controllers, private //--- Command controllers, private
impl<'a> AnonymousContext<'a> { impl<'a> AnonymousContext<'a> {
async fn capability(self) -> Result<(Response, flow::Transition)> {
let capabilities = vec![Capability::Imap4Rev1, Capability::Idle];
let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities));
Ok((res, flow::Transition::None))
}
async fn login( async fn login(
self, self,
username: &AString, username: &AString<'a>,
password: &AString, password: &Secret<AString<'a>>,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let (u, p) = ( let (u, p) = (
String::try_from(username.clone())?, std::str::from_utf8(username.as_ref())?,
String::try_from(password.clone())?, std::str::from_utf8(password.declassify().as_ref())?,
); );
tracing::info!(user = %u, "command.login"); tracing::info!(user = %u, "command.login");
let login_provider = match &self.login_provider { let creds = match self.login_provider.login(&u, &p).await {
Some(lp) => lp,
None => {
return Ok((
Response::no("Login command not available (already logged in)")?,
flow::Transition::None,
))
}
};
let creds = match login_provider.login(&u, &p).await {
Err(e) => { Err(e) => {
tracing::debug!(error=%e, "authentication failed"); tracing::debug!(error=%e, "authentication failed");
return Ok(( return Ok((
Response::no("Authentication failed")?, Response::build()
.to_req(self.req)
.message("Authentication failed")
.no()?,
flow::Transition::None, flow::Transition::None,
)); ));
} }
Ok(c) => c, Ok(c) => c,
}; };
let user = User::new(u.clone(), creds).await?; let user = User::new(u.to_string(), creds).await?;
tracing::info!(username=%u, "connected"); tracing::info!(username=%u, "connected");
Ok(( Ok((
Response::ok("Completed")?, Response::build()
.to_req(self.req)
.message("Completed")
.ok()?,
flow::Transition::Authenticate(user), 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

@ -0,0 +1,52 @@
use anyhow::Result;
use imap_codec::imap_types::core::{NonEmptyVec, Tag};
use imap_codec::imap_types::response::{Capability, Data};
use crate::imap::flow;
use crate::imap::response::Response;
pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> {
let capabilities: NonEmptyVec<Capability> =
(vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?;
let res = Response::build()
.tag(tag)
.message("Server capabilities")
.data(Data::Capability(capabilities))
.ok()?;
Ok((res, flow::Transition::None))
}
pub(crate) fn noop_nothing(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> {
Ok((
Response::build().tag(tag).message("Noop completed.").ok()?,
flow::Transition::None,
))
}
pub(crate) fn logout() -> Result<(Response<'static>, flow::Transition)> {
Ok((Response::bye()?, flow::Transition::Logout))
}
pub(crate) fn not_implemented<'a>(
tag: Tag<'a>,
what: &str,
) -> Result<(Response<'a>, flow::Transition)> {
Ok((
Response::build()
.tag(tag)
.message(format!("Command not implemented {}", what))
.bad()?,
flow::Transition::None,
))
}
pub(crate) fn wrong_state(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> {
Ok((
Response::build()
.tag(tag)
.message("Command not authorized in this state")
.bad()?,
flow::Transition::None,
))
}

View file

@ -2,37 +2,42 @@ use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use boitalettres::proto::res::body::Data as Body; use imap_codec::imap_types::command::{Command, CommandBody};
use boitalettres::proto::{Request, Response}; use imap_codec::imap_types::core::{Atom, Literal, QuotedChar};
use imap_codec::types::command::{CommandBody, StatusAttribute}; use imap_codec::imap_types::datetime::DateTime;
use imap_codec::types::core::NonZeroBytes; use imap_codec::imap_types::flag::{Flag, FlagNameAttribute};
use imap_codec::types::datetime::MyDateTime; use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
use imap_codec::types::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::response::{Code, CodeOther, Data};
use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName};
use imap_codec::types::response::{Code, Data, StatusAttributeValue};
use crate::imap::command::anonymous; 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::mail::mailbox::Mailbox; use crate::mail::mailbox::Mailbox;
use crate::mail::uidindex::*; use crate::mail::uidindex::*;
use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER}; use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW};
use crate::mail::IMF; use crate::mail::IMF;
pub struct AuthenticatedContext<'a> { pub struct AuthenticatedContext<'a> {
pub req: &'a Request, pub req: &'a Command<'static>,
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
} }
pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { pub async fn dispatch<'a>(
match &ctx.req.command.body { ctx: AuthenticatedContext<'a>,
) -> Result<(Response<'static>, flow::Transition)> {
match &ctx.req.body {
// Any state
CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()),
CommandBody::Capability => anystate::capability(ctx.req.tag.clone()),
CommandBody::Logout => anystate::logout(),
// Specific to this state (11 commands)
CommandBody::Create { mailbox } => ctx.create(mailbox).await, CommandBody::Create { mailbox } => ctx.create(mailbox).await,
CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, CommandBody::Delete { mailbox } => ctx.delete(mailbox).await,
CommandBody::Rename { CommandBody::Rename { from, to } => ctx.rename(from, to).await,
mailbox,
new_mailbox,
} => ctx.rename(mailbox, new_mailbox).await,
CommandBody::Lsub { CommandBody::Lsub {
reference, reference,
mailbox_wildcard, mailbox_wildcard,
@ -43,8 +48,8 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::
} => ctx.list(reference, mailbox_wildcard, false).await, } => ctx.list(reference, mailbox_wildcard, false).await,
CommandBody::Status { CommandBody::Status {
mailbox, mailbox,
attributes, item_names,
} => ctx.status(mailbox, attributes).await, } => ctx.status(mailbox, item_names).await,
CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await,
CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await,
CommandBody::Select { mailbox } => ctx.select(mailbox).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await,
@ -55,90 +60,148 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::
date, date,
message, message,
} => ctx.append(mailbox, flags, date, message).await, } => ctx.append(mailbox, flags, date, message).await,
_ => {
let ctx = anonymous::AnonymousContext { // Collect other commands
req: ctx.req, _ => anystate::wrong_state(ctx.req.tag.clone()),
login_provider: None,
};
anonymous::dispatch(ctx).await
}
} }
} }
// --- PRIVATE --- // --- PRIVATE ---
impl<'a> AuthenticatedContext<'a> { impl<'a> AuthenticatedContext<'a> {
async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { async fn create(
let name = String::try_from(mailbox.clone())?; self,
mailbox: &MailboxCodec<'a>,
if name == INBOX { ) -> Result<(Response<'static>, flow::Transition)> {
let name = match mailbox {
MailboxCodec::Inbox => {
return Ok(( return Ok((
Response::bad("Cannot create INBOX")?, Response::build()
.to_req(self.req)
.message("Cannot create INBOX")
.bad()?,
flow::Transition::None, flow::Transition::None,
)); ));
} }
MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?,
};
match self.user.create_mailbox(&name).await { match self.user.create_mailbox(&name).await {
Ok(()) => Ok((Response::ok("CREATE complete")?, flow::Transition::None)), Ok(()) => Ok((
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), Response::build()
.to_req(self.req)
.message("CREATE complete")
.ok()?,
flow::Transition::None,
)),
Err(e) => Ok((
Response::build()
.to_req(self.req)
.message(&e.to_string())
.no()?,
flow::Transition::None,
)),
} }
} }
async fn delete(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { async fn delete(
let name = String::try_from(mailbox.clone())?; self,
mailbox: &MailboxCodec<'a>,
) -> Result<(Response<'static>, flow::Transition)> {
let name: &str = MailboxName(mailbox).try_into()?;
match self.user.delete_mailbox(&name).await { match self.user.delete_mailbox(&name).await {
Ok(()) => Ok((Response::ok("DELETE complete")?, flow::Transition::None)), Ok(()) => Ok((
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), Response::build()
.to_req(self.req)
.message("DELETE complete")
.ok()?,
flow::Transition::None,
)),
Err(e) => Ok((
Response::build()
.to_req(self.req)
.message(e.to_string())
.no()?,
flow::Transition::None,
)),
} }
} }
async fn rename( async fn rename(
self, self,
mailbox: &MailboxCodec, from: &MailboxCodec<'a>,
new_mailbox: &MailboxCodec, to: &MailboxCodec<'a>,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let name = String::try_from(mailbox.clone())?; let name: &str = MailboxName(from).try_into()?;
let new_name = String::try_from(new_mailbox.clone())?; let new_name: &str = MailboxName(to).try_into()?;
match self.user.rename_mailbox(&name, &new_name).await { match self.user.rename_mailbox(&name, &new_name).await {
Ok(()) => Ok((Response::ok("RENAME complete")?, flow::Transition::None)), Ok(()) => Ok((
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), Response::build()
.to_req(self.req)
.message("RENAME complete")
.ok()?,
flow::Transition::None,
)),
Err(e) => Ok((
Response::build()
.to_req(self.req)
.message(e.to_string())
.no()?,
flow::Transition::None,
)),
} }
} }
async fn list( async fn list(
self, self,
reference: &MailboxCodec, reference: &MailboxCodec<'a>,
mailbox_wildcard: &ListMailbox, mailbox_wildcard: &ListMailbox<'a>,
is_lsub: bool, is_lsub: bool,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let reference = String::try_from(reference.clone())?; let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW);
let reference: &str = MailboxName(reference).try_into()?;
if !reference.is_empty() { if !reference.is_empty() {
return Ok(( return Ok((
Response::bad("References not supported")?, Response::build()
.to_req(self.req)
.message("References not supported")
.bad()?,
flow::Transition::None, flow::Transition::None,
)); ));
} }
let wildcard = String::try_from(mailbox_wildcard.clone())?; // @FIXME would probably need a rewrite to better use the imap_codec library
let wildcard = match mailbox_wildcard {
ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?,
ListMailbox::String(v) => std::str::from_utf8(v.as_ref())?,
};
if wildcard.is_empty() { if wildcard.is_empty() {
if is_lsub { if is_lsub {
return Ok(( return Ok((
Response::ok("LSUB complete")?.with_body(vec![Data::Lsub { Response::build()
.to_req(self.req)
.message("LSUB complete")
.data(Data::Lsub {
items: vec![], items: vec![],
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), delimiter: Some(mbx_hier_delim),
mailbox: "".try_into().unwrap(), mailbox: "".try_into().unwrap(),
}]), })
.ok()?,
flow::Transition::None, flow::Transition::None,
)); ));
} else { } else {
return Ok(( return Ok((
Response::ok("LIST complete")?.with_body(vec![Data::List { Response::build()
.to_req(self.req)
.message("LIST complete")
.data(Data::List {
items: vec![], items: vec![],
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), delimiter: Some(mbx_hier_delim),
mailbox: "".try_into().unwrap(), mailbox: "".try_into().unwrap(),
}]), })
.ok()?,
flow::Transition::None, flow::Transition::None,
)); ));
} }
@ -147,7 +210,7 @@ impl<'a> AuthenticatedContext<'a> {
let mailboxes = self.user.list_mailboxes().await?; let mailboxes = self.user.list_mailboxes().await?;
let mut vmailboxes = BTreeMap::new(); let mut vmailboxes = BTreeMap::new();
for mb in mailboxes.iter() { for mb in mailboxes.iter() {
for (i, _) in mb.match_indices(MAILBOX_HIERARCHY_DELIMITER) { for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) {
if i > 0 { if i > 0 {
let smb = &mb[..i]; let smb = &mb[..i];
vmailboxes.entry(smb).or_insert(false); vmailboxes.entry(smb).or_insert(false);
@ -163,22 +226,22 @@ impl<'a> AuthenticatedContext<'a> {
.to_string() .to_string()
.try_into() .try_into()
.map_err(|_| anyhow!("invalid mailbox name"))?; .map_err(|_| anyhow!("invalid mailbox name"))?;
let mut items = vec![FlagNameAttribute::Extension( let mut items = vec![FlagNameAttribute::try_from(Atom::unvalidated(
"Subscribed".try_into().unwrap(), "Subscribed",
)]; ))?];
if !*is_real { if !*is_real {
items.push(FlagNameAttribute::Noselect); items.push(FlagNameAttribute::Noselect);
} }
if is_lsub { if is_lsub {
ret.push(Data::Lsub { ret.push(Data::Lsub {
items, items,
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), delimiter: Some(mbx_hier_delim),
mailbox, mailbox,
}); });
} else { } else {
ret.push(Data::List { ret.push(Data::List {
items, items,
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), delimiter: Some(mbx_hier_delim),
mailbox, mailbox,
}); });
} }
@ -190,79 +253,120 @@ impl<'a> AuthenticatedContext<'a> {
} else { } else {
"LIST completed" "LIST completed"
}; };
Ok((Response::ok(msg)?.with_body(ret), flow::Transition::None)) Ok((
Response::build()
.to_req(self.req)
.message(msg)
.many_data(ret)
.ok()?,
flow::Transition::None,
))
} }
async fn status( async fn status(
self, self,
mailbox: &MailboxCodec, mailbox: &MailboxCodec<'static>,
attributes: &[StatusAttribute], attributes: &[StatusDataItemName],
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let name = String::try_from(mailbox.clone())?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(name).await?;
let mb = match mb_opt { let mb = match mb_opt {
Some(mb) => mb, Some(mb) => mb,
None => { None => {
return Ok(( return Ok((
Response::no("Mailbox does not exist")?, Response::build()
.to_req(self.req)
.message("Mailbox does not exist")
.no()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
}; };
let (view, _data) = MailboxView::new(mb).await?; let view = MailboxView::new(mb).await;
let mut ret_attrs = vec![]; let mut ret_attrs = vec![];
for attr in attributes.iter() { for attr in attributes.iter() {
ret_attrs.push(match attr { ret_attrs.push(match attr {
StatusAttribute::Messages => StatusAttributeValue::Messages(view.exists()?), StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?),
StatusAttribute::Unseen => StatusAttributeValue::Unseen(view.unseen_count() as u32), StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32),
StatusAttribute::Recent => StatusAttributeValue::Recent(view.recent()?), StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?),
StatusAttribute::UidNext => StatusAttributeValue::UidNext(view.uidnext()), StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()),
StatusAttribute::UidValidity => { StatusDataItemName::UidValidity => {
StatusAttributeValue::UidValidity(view.uidvalidity()) StatusDataItem::UidValidity(view.uidvalidity())
} }
StatusDataItemName::Deleted => {
bail!("quota not implemented, can't return deleted elements waiting for EXPUNGE");
},
StatusDataItemName::DeletedStorage => {
bail!("quota not implemented, can't return freed storage after EXPUNGE will be run");
},
}); });
} }
let data = vec![Body::Data(Data::Status { let data = Data::Status {
mailbox: mailbox.clone(), mailbox: mailbox.clone(),
attributes: ret_attrs, items: ret_attrs.into(),
})]; };
Ok(( Ok((
Response::ok("STATUS completed")?.with_body(data), Response::build()
.to_req(self.req)
.message("STATUS completed")
.data(data)
.ok()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
async fn subscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { async fn subscribe(
let name = String::try_from(mailbox.clone())?; self,
mailbox: &MailboxCodec<'a>,
) -> Result<(Response<'static>, flow::Transition)> {
let name: &str = MailboxName(mailbox).try_into()?;
if self.user.has_mailbox(&name).await? { if self.user.has_mailbox(&name).await? {
Ok((Response::ok("SUBSCRIBE complete")?, flow::Transition::None)) Ok((
Response::build()
.to_req(self.req)
.message("SUBSCRIBE complete")
.ok()?,
flow::Transition::None,
))
} else { } else {
Ok(( Ok((
Response::bad(&format!("Mailbox {} does not exist", name))?, Response::build()
.to_req(self.req)
.message(format!("Mailbox {} does not exist", name))
.bad()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
} }
async fn unsubscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { async fn unsubscribe(
let name = String::try_from(mailbox.clone())?; self,
mailbox: &MailboxCodec<'a>,
) -> Result<(Response<'static>, flow::Transition)> {
let name: &str = MailboxName(mailbox).try_into()?;
if self.user.has_mailbox(&name).await? { if self.user.has_mailbox(&name).await? {
Ok(( Ok((
Response::bad(&format!( Response::build()
.to_req(self.req)
.message(format!(
"Cannot unsubscribe from mailbox {}: not supported by Aerogramme", "Cannot unsubscribe from mailbox {}: not supported by Aerogramme",
name name
))?, ))
.bad()?,
flow::Transition::None, flow::Transition::None,
)) ))
} else { } else {
Ok(( Ok((
Response::bad(&format!("Mailbox {} does not exist", name))?, Response::build()
.to_req(self.req)
.message(format!("Mailbox {} does not exist", name))
.no()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
@ -301,83 +405,113 @@ impl<'a> AuthenticatedContext<'a> {
* TRACE END --- * TRACE END ---
*/ */
async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { async fn select(
let name = String::try_from(mailbox.clone())?; self,
mailbox: &MailboxCodec<'a>,
) -> Result<(Response<'static>, flow::Transition)> {
let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
let mb = match mb_opt { let mb = match mb_opt {
Some(mb) => mb, Some(mb) => mb,
None => { None => {
return Ok(( return Ok((
Response::no("Mailbox does not exist")?, Response::build()
.to_req(self.req)
.message("Mailbox does not exist")
.no()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
}; };
tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected");
let (mb, data) = MailboxView::new(mb).await?; let mb = MailboxView::new(mb).await;
let data = mb.summary()?;
Ok(( Ok((
Response::ok("Select completed")? Response::build()
.with_extra_code(Code::ReadWrite) .message("Select completed")
.with_body(data), .to_req(self.req)
.code(Code::ReadWrite)
.set_body(data)
.ok()?,
flow::Transition::Select(mb), flow::Transition::Select(mb),
)) ))
} }
async fn examine(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { async fn examine(
let name = String::try_from(mailbox.clone())?; self,
mailbox: &MailboxCodec<'a>,
) -> Result<(Response<'static>, flow::Transition)> {
let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
let mb = match mb_opt { let mb = match mb_opt {
Some(mb) => mb, Some(mb) => mb,
None => { None => {
return Ok(( return Ok((
Response::no("Mailbox does not exist")?, Response::build()
.to_req(self.req)
.message("Mailbox does not exist")
.no()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
}; };
tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined");
let (mb, data) = MailboxView::new(mb).await?; let mb = MailboxView::new(mb).await;
let data = mb.summary()?;
Ok(( Ok((
Response::ok("Examine completed")? Response::build()
.with_extra_code(Code::ReadOnly) .to_req(self.req)
.with_body(data), .message("Examine completed")
.code(Code::ReadOnly)
.set_body(data)
.ok()?,
flow::Transition::Examine(mb), flow::Transition::Examine(mb),
)) ))
} }
async fn append( async fn append(
self, self,
mailbox: &MailboxCodec, mailbox: &MailboxCodec<'a>,
flags: &[Flag], flags: &[Flag<'a>],
date: &Option<MyDateTime>, date: &Option<DateTime>,
message: &NonZeroBytes, message: &Literal<'a>,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let append_tag = self.req.tag.clone();
match self.append_internal(mailbox, flags, date, message).await { match self.append_internal(mailbox, flags, date, message).await {
Ok((_mb, uidvalidity, uid)) => Ok(( Ok((_mb, uidvalidity, uid)) => Ok((
Response::ok("APPEND completed")?.with_extra_code(Code::Other( Response::build()
"APPENDUID".try_into().unwrap(), .tag(append_tag)
Some(format!("{} {}", uidvalidity, uid)), .message("APPEND completed")
)), .code(Code::Other(CodeOther::unvalidated(
format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(),
)))
.ok()?,
flow::Transition::None,
)),
Err(e) => Ok((
Response::build()
.tag(append_tag)
.message(e.to_string())
.no()?,
flow::Transition::None, flow::Transition::None,
)), )),
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
} }
} }
pub(crate) async fn append_internal( pub(crate) async fn append_internal(
self, self,
mailbox: &MailboxCodec, mailbox: &MailboxCodec<'a>,
flags: &[Flag], flags: &[Flag<'a>],
date: &Option<MyDateTime>, date: &Option<DateTime>,
message: &NonZeroBytes, message: &Literal<'a>,
) -> Result<(Arc<Mailbox>, ImapUidvalidity, ImapUidvalidity)> { ) -> Result<(Arc<Mailbox>, ImapUidvalidity, ImapUidvalidity)> {
let name = String::try_from(mailbox.clone())?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
let mb = match mb_opt { let mb = match mb_opt {
@ -389,8 +523,8 @@ impl<'a> AuthenticatedContext<'a> {
bail!("Cannot set date when appending message"); bail!("Cannot set date when appending message");
} }
let msg = IMF::try_from(message.as_slice()) let msg =
.map_err(|_| anyhow!("Could not parse e-mail message"))?; IMF::try_from(message.data()).map_err(|_| anyhow!("Could not parse e-mail message"))?;
let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>();
// TODO: filter allowed flags? ping @Quentin // TODO: filter allowed flags? ping @Quentin
@ -422,7 +556,7 @@ fn matches_wildcard(wildcard: &str, name: &str) -> bool {
&& j > 0 && j > 0
&& matches[i - 1][j] && matches[i - 1][j]
&& (wildcard[j - 1] == '*' && (wildcard[j - 1] == '*'
|| (wildcard[j - 1] == '%' && name[i - 1] != MAILBOX_HIERARCHY_DELIMITER))); || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW)));
} }
} }

View file

@ -1,56 +1,60 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use boitalettres::proto::Request; use imap_codec::imap_types::command::{Command, CommandBody};
use boitalettres::proto::Response; use imap_codec::imap_types::core::Charset;
use imap_codec::types::command::{CommandBody, SearchKey}; use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
use imap_codec::types::core::{Charset, NonZeroBytes}; use imap_codec::imap_types::search::SearchKey;
use imap_codec::types::datetime::MyDateTime; use imap_codec::imap_types::sequence::SequenceSet;
use imap_codec::types::fetch_attributes::MacroOrFetchAttributes;
use imap_codec::types::flag::Flag;
use imap_codec::types::mailbox::Mailbox as MailboxCodec;
use imap_codec::types::response::Code;
use imap_codec::types::sequence::SequenceSet;
use crate::imap::command::authenticated; 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::mail::user::User; use crate::mail::user::User;
pub struct ExaminedContext<'a> { pub struct ExaminedContext<'a> {
pub req: &'a Request, 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 async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Transition)> { pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> {
match &ctx.req.command.body { match &ctx.req.body {
// CLOSE in examined state is not the same as in selected state // Any State
// (in selected state it also does an EXPUNGE, here it doesn't) // noop is specific to this state
CommandBody::Capability => anystate::capability(ctx.req.tag.clone()),
CommandBody::Logout => anystate::logout(),
// Specific to the EXAMINE state (specialization of the SELECTED state)
// ~3 commands -> close, fetch, search + NOOP
CommandBody::Close => ctx.close().await, CommandBody::Close => ctx.close().await,
CommandBody::Fetch { CommandBody::Fetch {
sequence_set, sequence_set,
attributes, macro_or_item_names,
uid, uid,
} => ctx.fetch(sequence_set, attributes, uid).await, } => ctx.fetch(sequence_set, macro_or_item_names, uid).await,
CommandBody::Search { CommandBody::Search {
charset, charset,
criteria, criteria,
uid, uid,
} => ctx.search(charset, criteria, uid).await, } => ctx.search(charset, criteria, uid).await,
CommandBody::Noop => ctx.noop().await, CommandBody::Noop | CommandBody::Check => ctx.noop().await,
CommandBody::Append { CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok((
mailbox, Response::build()
flags, .to_req(ctx.req)
date, .message("Forbidden command: can't write in read-only mode (EXAMINE)")
message, .bad()?,
} => ctx.append(mailbox, flags, date, message).await, flow::Transition::None,
)),
// In examined mode, we fallback to authenticated when needed
_ => { _ => {
let ctx = authenticated::AuthenticatedContext { authenticated::dispatch(authenticated::AuthenticatedContext {
req: ctx.req, req: ctx.req,
user: ctx.user, user: ctx.user,
}; })
authenticated::dispatch(ctx).await .await
} }
} }
} }
@ -58,71 +62,69 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Trans
// --- PRIVATE --- // --- PRIVATE ---
impl<'a> ExaminedContext<'a> { impl<'a> ExaminedContext<'a> {
async fn close(self) -> Result<(Response, flow::Transition)> { /// CLOSE in examined state is not the same as in selected state
Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) /// (in selected state it also does an EXPUNGE, here it doesn't)
async fn close(self) -> Result<(Response<'static>, flow::Transition)> {
Ok((
Response::build()
.to_req(self.req)
.message("CLOSE completed")
.ok()?,
flow::Transition::Unselect,
))
} }
pub async fn fetch( pub async fn fetch(
self, self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
attributes: &MacroOrFetchAttributes, attributes: &'a MacroOrMessageDataItemNames<'static>,
uid: &bool, uid: &bool,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
match self.mailbox.fetch(sequence_set, attributes, uid).await { match self.mailbox.fetch(sequence_set, attributes, uid).await {
Ok(resp) => Ok(( Ok(resp) => Ok((
Response::ok("FETCH completed")?.with_body(resp), Response::build()
.to_req(self.req)
.message("FETCH completed")
.set_body(resp)
.ok()?,
flow::Transition::None,
)),
Err(e) => Ok((
Response::build()
.to_req(self.req)
.message(e.to_string())
.no()?,
flow::Transition::None, flow::Transition::None,
)), )),
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
} }
} }
pub async fn search( pub async fn search(
self, self,
_charset: &Option<Charset>, _charset: &Option<Charset<'a>>,
_criteria: &SearchKey, _criteria: &SearchKey<'a>,
_uid: &bool, _uid: &bool,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
Ok((Response::bad("Not implemented")?, flow::Transition::None))
}
pub async fn noop(self) -> Result<(Response, flow::Transition)> {
self.mailbox.mailbox.force_sync().await?;
let updates = self.mailbox.update().await?;
Ok(( Ok((
Response::ok("NOOP completed.")?.with_body(updates), Response::build()
.to_req(self.req)
.message("Not implemented")
.bad()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
async fn append( pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> {
self, self.mailbox.mailbox.force_sync().await?;
mailbox: &MailboxCodec,
flags: &[Flag],
date: &Option<MyDateTime>,
message: &NonZeroBytes,
) -> Result<(Response, flow::Transition)> {
let ctx2 = authenticated::AuthenticatedContext {
req: self.req,
user: self.user,
};
match ctx2.append_internal(mailbox, flags, date, message).await { let updates = self.mailbox.update().await?;
Ok((mb, uidvalidity, uid)) => { Ok((
let resp = Response::ok("APPEND completed")?.with_extra_code(Code::Other( Response::build()
"APPENDUID".try_into().unwrap(), .to_req(self.req)
Some(format!("{} {}", uidvalidity, uid)), .message("NOOP completed.")
)); .set_body(updates)
.ok()?,
if Arc::ptr_eq(&mb, &self.mailbox.mailbox) { flow::Transition::None,
let data = self.mailbox.update().await?; ))
Ok((resp.with_body(data), flow::Transition::None))
} else {
Ok((resp, flow::Transition::None))
}
}
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
}
} }
} }

View file

@ -1,4 +1,21 @@
pub mod anonymous; pub mod anonymous;
pub mod anystate;
pub mod authenticated; pub mod authenticated;
pub mod examined; pub mod examined;
pub mod selected; pub mod selected;
use crate::mail::user::INBOX;
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
/// Convert an IMAP mailbox name/identifier representation
/// to an utf-8 string that is used internally in Aerogramme
struct MailboxName<'a>(&'a MailboxCodec<'a>);
impl<'a> TryInto<&'a str> for MailboxName<'a> {
type Error = std::str::Utf8Error;
fn try_into(self) -> Result<&'a str, Self::Error> {
match self.0 {
MailboxCodec::Inbox => Ok(INBOX),
MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?),
}
}
}

View file

@ -1,31 +1,50 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use boitalettres::proto::Request; use imap_codec::imap_types::command::{Command, CommandBody};
use boitalettres::proto::Response; use imap_codec::imap_types::core::Charset;
use imap_codec::types::command::CommandBody; use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType};
use imap_codec::types::mailbox::Mailbox as MailboxCodec; use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
use imap_codec::types::response::Code; use imap_codec::imap_types::response::{Code, CodeOther};
use imap_codec::types::sequence::SequenceSet; use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::SequenceSet;
use crate::imap::command::examined; 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::mail::user::User; use crate::mail::user::User;
pub struct SelectedContext<'a> { pub struct SelectedContext<'a> {
pub req: &'a Request, 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 async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Transition)> { pub async fn dispatch<'a>(
match &ctx.req.command.body { ctx: SelectedContext<'a>,
// Only write commands here, read commands are handled in ) -> Result<(Response<'static>, flow::Transition)> {
// `examined.rs` match &ctx.req.body {
// Any State
// noop is specific to this state
CommandBody::Capability => anystate::capability(ctx.req.tag.clone()),
CommandBody::Logout => anystate::logout(),
// Specific to this state (7 commands + NOOP)
CommandBody::Close => ctx.close().await, CommandBody::Close => ctx.close().await,
CommandBody::Noop | CommandBody::Check => ctx.noop().await,
CommandBody::Fetch {
sequence_set,
macro_or_item_names,
uid,
} => ctx.fetch(sequence_set, macro_or_item_names, uid).await,
CommandBody::Search {
charset,
criteria,
uid,
} => ctx.search(charset, criteria, uid).await,
CommandBody::Expunge => ctx.expunge().await, CommandBody::Expunge => ctx.expunge().await,
CommandBody::Store { CommandBody::Store {
sequence_set, sequence_set,
@ -39,13 +58,14 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans
mailbox, mailbox,
uid, uid,
} => ctx.copy(sequence_set, mailbox, uid).await, } => ctx.copy(sequence_set, mailbox, uid).await,
// In selected mode, we fallback to authenticated when needed
_ => { _ => {
let ctx = examined::ExaminedContext { authenticated::dispatch(authenticated::AuthenticatedContext {
req: ctx.req, req: ctx.req,
user: ctx.user, user: ctx.user,
mailbox: ctx.mailbox, })
}; .await
examined::dispatch(ctx).await
} }
} }
} }
@ -53,18 +73,81 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans
// --- PRIVATE --- // --- PRIVATE ---
impl<'a> SelectedContext<'a> { impl<'a> SelectedContext<'a> {
async fn close(self) -> Result<(Response, flow::Transition)> { async fn close(self) -> Result<(Response<'static>, flow::Transition)> {
// We expunge messages, // We expunge messages,
// but we don't send the untagged EXPUNGE responses // but we don't send the untagged EXPUNGE responses
let tag = self.req.tag.clone();
self.expunge().await?; self.expunge().await?;
Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) Ok((
Response::build().tag(tag).message("CLOSE completed").ok()?,
flow::Transition::Unselect,
))
} }
async fn expunge(self) -> Result<(Response, flow::Transition)> { pub async fn fetch(
self,
sequence_set: &SequenceSet,
attributes: &'a MacroOrMessageDataItemNames<'static>,
uid: &bool,
) -> Result<(Response<'static>, flow::Transition)> {
match self.mailbox.fetch(sequence_set, attributes, uid).await {
Ok(resp) => Ok((
Response::build()
.to_req(self.req)
.message("FETCH completed")
.set_body(resp)
.ok()?,
flow::Transition::None,
)),
Err(e) => Ok((
Response::build()
.to_req(self.req)
.message(e.to_string())
.no()?,
flow::Transition::None,
)),
}
}
pub async fn search(
self,
_charset: &Option<Charset<'a>>,
_criteria: &SearchKey<'a>,
_uid: &bool,
) -> Result<(Response<'static>, flow::Transition)> {
Ok((
Response::build()
.to_req(self.req)
.message("Not implemented")
.bad()?,
flow::Transition::None,
))
}
pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> {
self.mailbox.mailbox.force_sync().await?;
let updates = self.mailbox.update().await?;
Ok((
Response::build()
.to_req(self.req)
.message("NOOP completed.")
.set_body(updates)
.ok()?,
flow::Transition::None,
))
}
async fn expunge(self) -> Result<(Response<'static>, flow::Transition)> {
let tag = self.req.tag.clone();
let data = self.mailbox.expunge().await?; let data = self.mailbox.expunge().await?;
Ok(( Ok((
Response::ok("EXPUNGE completed")?.with_body(data), Response::build()
.tag(tag)
.message("EXPUNGE completed")
.set_body(data)
.ok()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
@ -74,16 +157,20 @@ impl<'a> SelectedContext<'a> {
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
kind: &StoreType, kind: &StoreType,
response: &StoreResponse, response: &StoreResponse,
flags: &[Flag], flags: &[Flag<'a>],
uid: &bool, uid: &bool,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let data = self let data = self
.mailbox .mailbox
.store(sequence_set, kind, response, flags, uid) .store(sequence_set, kind, response, flags, uid)
.await?; .await?;
Ok(( Ok((
Response::ok("STORE completed")?.with_body(data), Response::build()
.to_req(self.req)
.message("STORE completed")
.set_body(data)
.ok()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
@ -91,18 +178,21 @@ impl<'a> SelectedContext<'a> {
async fn copy( async fn copy(
self, self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
mailbox: &MailboxCodec, mailbox: &MailboxCodec<'a>,
uid: &bool, uid: &bool,
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let name = String::try_from(mailbox.clone())?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
let mb = match mb_opt { let mb = match mb_opt {
Some(mb) => mb, Some(mb) => mb,
None => { None => {
return Ok(( return Ok((
Response::no("Destination mailbox does not exist")? Response::build()
.with_extra_code(Code::TryCreate), .to_req(self.req)
.message("Destination mailbox does not exist")
.code(Code::TryCreate)
.no()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }
@ -126,10 +216,13 @@ impl<'a> SelectedContext<'a> {
); );
Ok(( Ok((
Response::ok("COPY completed")?.with_extra_code(Code::Other( Response::build()
"COPYUID".try_into().unwrap(), .to_req(self.req)
Some(copyuid_str), .message("COPY completed")
)), .code(Code::Other(CodeOther::unvalidated(
format!("COPYUID {}", copyuid_str).into_bytes(),
)))
.ok()?,
flow::Transition::None, flow::Transition::None,
)) ))
} }

View file

@ -37,23 +37,27 @@ pub enum Transition {
// See RFC3501 section 3. // See RFC3501 section 3.
// https://datatracker.ietf.org/doc/html/rfc3501#page-13 // https://datatracker.ietf.org/doc/html/rfc3501#page-13
impl State { impl State {
pub fn apply(self, tr: Transition) -> Result<Self, Error> { pub fn apply(&mut self, tr: Transition) -> Result<(), Error> {
match (self, tr) { let new_state = match (&self, tr) {
(s, Transition::None) => Ok(s), (_s, Transition::None) => return Ok(()),
(State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)), (State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u),
( (
State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _), State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _),
Transition::Select(m), Transition::Select(m),
) => Ok(State::Selected(u, m)), ) => State::Selected(u.clone(), m),
( (
State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _), State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _),
Transition::Examine(m), Transition::Examine(m),
) => Ok(State::Examined(u, m)), ) => State::Examined(u.clone(), m),
(State::Selected(u, _) | State::Examined(u, _), Transition::Unselect) => { (State::Selected(u, _) | State::Examined(u, _), Transition::Unselect) => {
Ok(State::Authenticated(u)) State::Authenticated(u.clone())
}
(_, Transition::Logout) => Ok(State::Logout),
_ => Err(Error::ForbiddenTransition),
} }
(_, Transition::Logout) => State::Logout,
_ => return Err(Error::ForbiddenTransition),
};
*self = new_state;
Ok(())
} }
} }

View file

@ -4,22 +4,20 @@ use std::num::NonZeroU32;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, bail, Error, Result}; use anyhow::{anyhow, bail, Error, Result};
use boitalettres::proto::res::body::Data as Body;
use chrono::{Offset, TimeZone, Utc}; use chrono::{Offset, TimeZone, Utc};
use futures::stream::{FuturesOrdered, StreamExt}; use futures::stream::{FuturesOrdered, StreamExt};
use imap_codec::types::address::Address; use imap_codec::imap_types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields};
use imap_codec::types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields}; use imap_codec::imap_types::core::{AString, Atom, IString, NString, NonEmptyVec};
use imap_codec::types::core::{AString, Atom, IString, NString}; use imap_codec::imap_types::datetime::DateTime;
use imap_codec::types::datetime::MyDateTime; use imap_codec::imap_types::envelope::{Address, Envelope};
use imap_codec::types::envelope::Envelope; use imap_codec::imap_types::fetch::{
use imap_codec::types::fetch_attributes::{ MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName, Section as FetchSection,
FetchAttribute, MacroOrFetchAttributes, Section as FetchSection,
}; };
use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType};
use imap_codec::types::response::{Code, Data, MessageAttribute, Status}; use imap_codec::imap_types::response::{Code, Data, Status};
use imap_codec::types::sequence::{self, SequenceSet}; use imap_codec::imap_types::sequence::{self, SequenceSet};
use eml_codec::{ use eml_codec::{
header, imf, mime, header, imf, mime,
@ -28,6 +26,7 @@ use eml_codec::{
}; };
use crate::cryptoblob::Key; use crate::cryptoblob::Key;
use crate::imap::response::Body;
use crate::mail::mailbox::{MailMeta, Mailbox}; use crate::mail::mailbox::{MailMeta, Mailbox};
use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex}; use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex};
use crate::mail::unique_ident::UniqueIdent; use crate::mail::unique_ident::UniqueIdent;
@ -77,19 +76,31 @@ impl<'a> FetchedMail<'a> {
} }
pub struct AttributesProxy { pub struct AttributesProxy {
attrs: Vec<FetchAttribute>, attrs: Vec<MessageDataItemName<'static>>,
} }
impl AttributesProxy { impl AttributesProxy {
fn new(attrs: &MacroOrFetchAttributes, is_uid_fetch: bool) -> Self { fn new(attrs: &MacroOrMessageDataItemNames<'static>, is_uid_fetch: bool) -> Self {
// Expand macros // Expand macros
let mut fetch_attrs = match attrs { let mut fetch_attrs = match attrs {
MacroOrFetchAttributes::Macro(m) => m.expand(), MacroOrMessageDataItemNames::Macro(m) => {
MacroOrFetchAttributes::FetchAttributes(a) => a.clone(), use imap_codec::imap_types::fetch::Macro;
use MessageDataItemName::*;
match m {
Macro::All => vec![Flags, InternalDate, Rfc822Size, Envelope],
Macro::Fast => vec![Flags, InternalDate, Rfc822Size],
Macro::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body],
_ => {
tracing::error!("unimplemented macro");
vec![]
}
}
}
MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(),
}; };
// Handle uids // Handle uids
if is_uid_fetch && !fetch_attrs.contains(&FetchAttribute::Uid) { if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) {
fetch_attrs.push(FetchAttribute::Uid); fetch_attrs.push(MessageDataItemName::Uid);
} }
Self { attrs: fetch_attrs } Self { attrs: fetch_attrs }
@ -99,11 +110,11 @@ impl AttributesProxy {
self.attrs.iter().any(|x| { self.attrs.iter().any(|x| {
matches!( matches!(
x, x,
FetchAttribute::Body MessageDataItemName::Body
| FetchAttribute::BodyExt { .. } | MessageDataItemName::BodyExt { .. }
| FetchAttribute::Rfc822 | MessageDataItemName::Rfc822
| FetchAttribute::Rfc822Text | MessageDataItemName::Rfc822Text
| FetchAttribute::BodyStructure | MessageDataItemName::BodyStructure
) )
}) })
} }
@ -127,16 +138,20 @@ pub struct MailView<'a> {
meta: &'a MailMeta, meta: &'a MailMeta,
flags: &'a Vec<String>, flags: &'a Vec<String>,
content: FetchedMail<'a>, content: FetchedMail<'a>,
add_seen: bool, }
enum SeenFlag {
DoNothing,
MustAdd,
} }
impl<'a> MailView<'a> { impl<'a> MailView<'a> {
fn uid(&self) -> MessageAttribute { fn uid(&self) -> MessageDataItem<'static> {
MessageAttribute::Uid(self.ids.uid) MessageDataItem::Uid(self.ids.uid.clone())
} }
fn flags(&self) -> MessageAttribute { fn flags(&self) -> MessageDataItem<'static> {
MessageAttribute::Flags( MessageDataItem::Flags(
self.flags self.flags
.iter() .iter()
.filter_map(|f| string_to_flag(f)) .filter_map(|f| string_to_flag(f))
@ -144,12 +159,12 @@ impl<'a> MailView<'a> {
) )
} }
fn rfc_822_size(&self) -> MessageAttribute { fn rfc_822_size(&self) -> MessageDataItem<'static> {
MessageAttribute::Rfc822Size(self.meta.rfc822_size as u32) MessageDataItem::Rfc822Size(self.meta.rfc822_size as u32)
} }
fn rfc_822_header(&self) -> MessageAttribute { fn rfc_822_header(&self) -> MessageDataItem<'static> {
MessageAttribute::Rfc822Header(NString( MessageDataItem::Rfc822Header(NString(
self.meta self.meta
.headers .headers
.to_vec() .to_vec()
@ -159,41 +174,42 @@ impl<'a> MailView<'a> {
)) ))
} }
fn rfc_822_text(&self) -> Result<MessageAttribute> { fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> {
Ok(MessageAttribute::Rfc822Text(NString( Ok(MessageDataItem::Rfc822Text(NString(
self.content self.content
.as_full()? .as_full()?
.raw_body .raw_body
.to_vec()
.try_into() .try_into()
.ok() .ok()
.map(IString::Literal), .map(IString::Literal),
))) )))
} }
fn rfc822(&self) -> Result<MessageAttribute> { fn rfc822(&self) -> Result<MessageDataItem<'static>> {
Ok(MessageAttribute::Rfc822(NString( Ok(MessageDataItem::Rfc822(NString(
self.content self.content
.as_full()? .as_full()?
.raw_body .raw_part
.clone() .to_vec()
.try_into() .try_into()
.ok() .ok()
.map(IString::Literal), .map(IString::Literal),
))) )))
} }
fn envelope(&self) -> MessageAttribute { fn envelope(&self) -> MessageDataItem<'static> {
MessageAttribute::Envelope(message_envelope(self.content.imf())) MessageDataItem::Envelope(message_envelope(self.content.imf().clone()))
} }
fn body(&self) -> Result<MessageAttribute> { fn body(&self) -> Result<MessageDataItem<'static>> {
Ok(MessageAttribute::Body(build_imap_email_struct( Ok(MessageDataItem::Body(build_imap_email_struct(
self.content.as_full()?.child.as_ref(), self.content.as_full()?.child.as_ref(),
)?)) )?))
} }
fn body_structure(&self) -> Result<MessageAttribute> { fn body_structure(&self) -> Result<MessageDataItem<'static>> {
Ok(MessageAttribute::Body(build_imap_email_struct( Ok(MessageDataItem::Body(build_imap_email_struct(
self.content.as_full()?.child.as_ref(), self.content.as_full()?.child.as_ref(),
)?)) )?))
} }
@ -202,12 +218,14 @@ impl<'a> MailView<'a> {
/// peek does not implicitly set the \Seen flag /// peek does not implicitly set the \Seen flag
/// eg. BODY[HEADER.FIELDS (DATE FROM)] /// eg. BODY[HEADER.FIELDS (DATE FROM)]
/// eg. BODY[]<0.2048> /// eg. BODY[]<0.2048>
fn body_ext( fn body_ext<'b>(
&mut self, &self,
section: &Option<FetchSection>, section: &Option<FetchSection<'b>>,
partial: &Option<(u32, NonZeroU32)>, partial: &Option<(u32, NonZeroU32)>,
peek: &bool, peek: &bool,
) -> Result<MessageAttribute> { ) -> Result<(MessageDataItem<'b>, SeenFlag)> {
let mut seen = SeenFlag::DoNothing;
// Extract message section // Extract message section
let text = get_message_section(self.content.as_anypart()?, section)?; let text = get_message_section(self.content.as_anypart()?, section)?;
@ -215,7 +233,7 @@ impl<'a> MailView<'a> {
if !peek && !self.flags.iter().any(|x| *x == seen_flag) { if !peek && !self.flags.iter().any(|x| *x == seen_flag) {
// Add \Seen flag // Add \Seen flag
//self.mailbox.add_flags(uuid, &[seen_flag]).await?; //self.mailbox.add_flags(uuid, &[seen_flag]).await?;
self.add_seen = true; seen = SeenFlag::MustAdd;
} }
// Handle <<partial>> which cut the message bytes // Handle <<partial>> which cut the message bytes
@ -223,49 +241,60 @@ impl<'a> MailView<'a> {
let data = NString(text.to_vec().try_into().ok().map(IString::Literal)); let data = NString(text.to_vec().try_into().ok().map(IString::Literal));
return Ok(MessageAttribute::BodyExt { return Ok((
section: section.clone(), MessageDataItem::BodyExt {
section: section.as_ref().map(|fs| fs.clone()),
origin, origin,
data, data,
}); },
seen,
));
} }
fn internal_date(&self) -> Result<MessageAttribute> { fn internal_date(&self) -> Result<MessageDataItem<'static>> {
let dt = Utc let dt = Utc
.fix() .fix()
.timestamp_opt(i64::try_from(self.meta.internaldate / 1000)?, 0) .timestamp_opt(i64::try_from(self.meta.internaldate / 1000)?, 0)
.earliest() .earliest()
.ok_or(anyhow!("Unable to parse internal date"))?; .ok_or(anyhow!("Unable to parse internal date"))?;
Ok(MessageAttribute::InternalDate(MyDateTime(dt))) Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt)))
} }
fn filter(&mut self, ap: &AttributesProxy) -> Result<Body> { fn filter<'b>(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> {
let mut seen = SeenFlag::DoNothing;
let res_attrs = ap let res_attrs = ap
.attrs .attrs
.iter() .iter()
.map(|attr| match attr { .map(|attr| match attr {
FetchAttribute::Uid => Ok(self.uid()), MessageDataItemName::Uid => Ok(self.uid()),
FetchAttribute::Flags => Ok(self.flags()), MessageDataItemName::Flags => Ok(self.flags()),
FetchAttribute::Rfc822Size => Ok(self.rfc_822_size()), MessageDataItemName::Rfc822Size => Ok(self.rfc_822_size()),
FetchAttribute::Rfc822Header => Ok(self.rfc_822_header()), MessageDataItemName::Rfc822Header => Ok(self.rfc_822_header()),
FetchAttribute::Rfc822Text => self.rfc_822_text(), MessageDataItemName::Rfc822Text => self.rfc_822_text(),
FetchAttribute::Rfc822 => self.rfc822(), MessageDataItemName::Rfc822 => self.rfc822(),
FetchAttribute::Envelope => Ok(self.envelope()), MessageDataItemName::Envelope => Ok(self.envelope()),
FetchAttribute::Body => self.body(), MessageDataItemName::Body => self.body(),
FetchAttribute::BodyStructure => self.body_structure(), MessageDataItemName::BodyStructure => self.body_structure(),
FetchAttribute::BodyExt { MessageDataItemName::BodyExt {
section, section,
partial, partial,
peek, peek,
} => self.body_ext(section, partial, peek), } => {
FetchAttribute::InternalDate => self.internal_date(), let (body, has_seen) = self.body_ext(section, partial, peek)?;
seen = has_seen;
Ok(body)
}
MessageDataItemName::InternalDate => self.internal_date(),
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Ok(Body::Data(Data::Fetch { Ok((
seq_or_uid: self.ids.i, Body::Data(Data::Fetch {
attributes: res_attrs, seq: self.ids.i,
})) items: res_attrs.try_into()?,
}),
seen,
))
} }
} }
@ -376,7 +405,6 @@ impl<'a> MailSelectionBuilder<'a> {
meta, meta,
flags, flags,
content, content,
add_seen: false,
}) })
.collect()) .collect())
} }
@ -396,35 +424,26 @@ pub struct MailboxView {
impl MailboxView { impl MailboxView {
/// Creates a new IMAP view into a mailbox. /// Creates a new IMAP view into a mailbox.
/// Generates the necessary IMAP messages so that the client pub async fn new(mailbox: Arc<Mailbox>) -> Self {
/// has a satisfactory summary of the current mailbox's state.
/// These are the messages that are sent in response to a SELECT command.
pub async fn new(mailbox: Arc<Mailbox>) -> Result<(Self, Vec<Body>)> {
let state = mailbox.current_uid_index().await; let state = mailbox.current_uid_index().await;
let new_view = Self { Self {
mailbox, mailbox,
known_state: state, known_state: state,
}; }
let mut data = Vec::<Body>::new();
data.push(new_view.exists_status()?);
data.push(new_view.recent_status()?);
data.extend(new_view.flags_status()?.into_iter());
data.push(new_view.uidvalidity_status()?);
data.push(new_view.uidnext_status()?);
Ok((new_view, data))
} }
/// Create an updated view, useful to make a diff
/// between what the client knows and new stuff
/// Produces a set of IMAP responses describing the change between /// Produces a set of IMAP responses describing the change between
/// what the client knows and what is actually in the mailbox. /// what the client knows and what is actually in the mailbox.
/// This does NOT trigger a sync, it bases itself on what is currently /// This does NOT trigger a sync, it bases itself on what is currently
/// loaded in RAM by Bayou. /// loaded in RAM by Bayou.
pub async fn update(&mut self) -> Result<Vec<Body>> { pub async fn update(&mut self) -> Result<Vec<Body<'static>>> {
let new_view = MailboxView { let old_view: &mut Self = self;
mailbox: self.mailbox.clone(), let new_view = Self {
known_state: self.mailbox.current_uid_index().await, mailbox: old_view.mailbox.clone(),
known_state: old_view.mailbox.current_uid_index().await,
}; };
let mut data = Vec::<Body>::new(); let mut data = Vec::<Body>::new();
@ -446,7 +465,7 @@ impl MailboxView {
// - notify client of expunged mails // - notify client of expunged mails
let mut n_expunge = 0; let mut n_expunge = 0;
for (i, (_uid, uuid)) in self.known_state.idx_by_uid.iter().enumerate() { for (i, (_uid, uuid)) in old_view.known_state.idx_by_uid.iter().enumerate() {
if !new_view.known_state.table.contains_key(uuid) { if !new_view.known_state.table.contains_key(uuid) {
data.push(Body::Data(Data::Expunge( data.push(Body::Data(Data::Expunge(
NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(),
@ -456,49 +475,63 @@ impl MailboxView {
} }
// - if new mails arrived, notify client of number of existing mails // - if new mails arrived, notify client of number of existing mails
if new_view.known_state.table.len() != self.known_state.table.len() - n_expunge if new_view.known_state.table.len() != old_view.known_state.table.len() - n_expunge
|| new_view.known_state.uidvalidity != self.known_state.uidvalidity || new_view.known_state.uidvalidity != old_view.known_state.uidvalidity
{ {
data.push(new_view.exists_status()?); data.push(new_view.exists_status()?);
} }
if new_view.known_state.uidvalidity != self.known_state.uidvalidity { if new_view.known_state.uidvalidity != old_view.known_state.uidvalidity {
// TODO: do we want to push less/more info than this? // TODO: do we want to push less/more info than this?
data.push(new_view.uidvalidity_status()?); data.push(new_view.uidvalidity_status()?);
data.push(new_view.uidnext_status()?); data.push(new_view.uidnext_status()?);
} else { } else {
// - if flags changed for existing mails, tell client // - if flags changed for existing mails, tell client
for (i, (_uid, uuid)) in new_view.known_state.idx_by_uid.iter().enumerate() { for (i, (_uid, uuid)) in new_view.known_state.idx_by_uid.iter().enumerate() {
let old_mail = self.known_state.table.get(uuid); let old_mail = old_view.known_state.table.get(uuid);
let new_mail = new_view.known_state.table.get(uuid); let new_mail = new_view.known_state.table.get(uuid);
if old_mail.is_some() && old_mail != new_mail { if old_mail.is_some() && old_mail != new_mail {
if let Some((uid, flags)) = new_mail { if let Some((uid, flags)) = new_mail {
data.push(Body::Data(Data::Fetch { data.push(Body::Data(Data::Fetch {
seq_or_uid: NonZeroU32::try_from((i + 1) as u32).unwrap(), seq: NonZeroU32::try_from((i + 1) as u32).unwrap(),
attributes: vec![ items: vec![
MessageAttribute::Uid(*uid), MessageDataItem::Uid(*uid),
MessageAttribute::Flags( MessageDataItem::Flags(
flags.iter().filter_map(|f| string_to_flag(f)).collect(), flags.iter().filter_map(|f| string_to_flag(f)).collect(),
), ),
], ]
.try_into()?,
})); }));
} }
} }
} }
} }
*old_view = new_view;
*self = new_view;
Ok(data) Ok(data)
} }
pub async fn store( /// Generates the necessary IMAP messages so that the client
/// has a satisfactory summary of the current mailbox's state.
/// These are the messages that are sent in response to a SELECT command.
pub fn summary(&self) -> Result<Vec<Body<'static>>> {
let mut data = Vec::<Body>::new();
data.push(self.exists_status()?);
data.push(self.recent_status()?);
data.extend(self.flags_status()?.into_iter());
data.push(self.uidvalidity_status()?);
data.push(self.uidnext_status()?);
Ok(data)
}
pub async fn store<'a>(
&mut self, &mut self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
kind: &StoreType, kind: &StoreType,
_response: &StoreResponse, _response: &StoreResponse,
flags: &[Flag], flags: &[Flag<'a>],
is_uid_store: &bool, is_uid_store: &bool,
) -> Result<Vec<Body>> { ) -> Result<Vec<Body<'static>>> {
self.mailbox.opportunistic_sync().await?; self.mailbox.opportunistic_sync().await?;
let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>();
@ -522,7 +555,7 @@ impl MailboxView {
self.update().await self.update().await
} }
pub async fn expunge(&mut self) -> Result<Vec<Body>> { pub async fn expunge(&mut self) -> Result<Vec<Body<'static>>> {
self.mailbox.opportunistic_sync().await?; self.mailbox.opportunistic_sync().await?;
let deleted_flag = Flag::Deleted.to_string(); let deleted_flag = Flag::Deleted.to_string();
@ -569,12 +602,12 @@ impl MailboxView {
/// Looks up state changes in the mailbox and produces a set of IMAP /// Looks up state changes in the mailbox and produces a set of IMAP
/// responses describing the new state. /// responses describing the new state.
pub async fn fetch( pub async fn fetch<'b>(
&self, &self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
attributes: &MacroOrFetchAttributes, attributes: &'b MacroOrMessageDataItemNames<'static>,
is_uid_fetch: &bool, is_uid_fetch: &bool,
) -> Result<Vec<Body>> { ) -> Result<Vec<Body<'static>>> {
let ap = AttributesProxy::new(attributes, *is_uid_fetch); let ap = AttributesProxy::new(attributes, *is_uid_fetch);
// Prepare data // Prepare data
@ -619,31 +652,37 @@ impl MailboxView {
selection.with_bodies(bodies.as_slice()); selection.with_bodies(bodies.as_slice());
// Build mail selection views // Build mail selection views
let mut views = selection.build()?; let views = selection.build()?;
// Filter views to build the result // Filter views to build the result
let ret = views // Also identify what must be put as seen
.iter_mut() let filtered_view = views
.filter_map(|mv| mv.filter(&ap).ok())
.collect::<Vec<_>>();
// Register seen flags
let future_flags = views
.iter() .iter()
.filter(|mv| mv.add_seen) .filter_map(|mv| mv.filter(&ap).ok().map(|(body, seen)| (mv, body, seen)))
.map(|mv| async move { .collect::<Vec<_>>();
// Register seen flags
let future_flags = filtered_view
.iter()
.filter(|(_mv, _body, seen)| matches!(seen, SeenFlag::MustAdd))
.map(|(mv, _body, _seen)| async move {
let seen_flag = Flag::Seen.to_string(); let seen_flag = Flag::Seen.to_string();
self.mailbox.add_flags(mv.ids.uuid, &[seen_flag]).await?; self.mailbox.add_flags(mv.ids.uuid, &[seen_flag]).await?;
Ok::<_, anyhow::Error>(()) Ok::<_, anyhow::Error>(())
}) })
.collect::<FuturesOrdered<_>>(); .collect::<FuturesOrdered<_>>();
future_flags future_flags
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await .await
.into_iter() .into_iter()
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
Ok(ret) let command_body = filtered_view
.into_iter()
.map(|(_mv, body, _seen)| body)
.collect::<Vec<_>>();
Ok(command_body)
} }
// ---- // ----
@ -717,7 +756,7 @@ impl MailboxView {
// ---- // ----
/// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state`
fn uidvalidity_status(&self) -> Result<Body> { fn uidvalidity_status(&self) -> Result<Body<'static>> {
let uid_validity = Status::ok( let uid_validity = Status::ok(
None, None,
Some(Code::UidValidity(self.uidvalidity())), Some(Code::UidValidity(self.uidvalidity())),
@ -732,7 +771,7 @@ impl MailboxView {
} }
/// Produce an OK [UIDNEXT _] message corresponding to `known_state` /// Produce an OK [UIDNEXT _] message corresponding to `known_state`
fn uidnext_status(&self) -> Result<Body> { fn uidnext_status(&self) -> Result<Body<'static>> {
let next_uid = Status::ok( let next_uid = Status::ok(
None, None,
Some(Code::UidNext(self.uidnext())), Some(Code::UidNext(self.uidnext())),
@ -748,7 +787,7 @@ impl MailboxView {
/// Produce an EXISTS message corresponding to the number of mails /// Produce an EXISTS message corresponding to the number of mails
/// in `known_state` /// in `known_state`
fn exists_status(&self) -> Result<Body> { fn exists_status(&self) -> Result<Body<'static>> {
Ok(Body::Data(Data::Exists(self.exists()?))) Ok(Body::Data(Data::Exists(self.exists()?)))
} }
@ -758,7 +797,7 @@ impl MailboxView {
/// Produce a RECENT message corresponding to the number of /// Produce a RECENT message corresponding to the number of
/// recent mails in `known_state` /// recent mails in `known_state`
fn recent_status(&self) -> Result<Body> { fn recent_status(&self) -> Result<Body<'static>> {
Ok(Body::Data(Data::Recent(self.recent()?))) Ok(Body::Data(Data::Recent(self.recent()?)))
} }
@ -774,27 +813,48 @@ impl MailboxView {
/// Produce a FLAGS and a PERMANENTFLAGS message that indicates /// Produce a FLAGS and a PERMANENTFLAGS message that indicates
/// the flags that are in `known_state` + default flags /// the flags that are in `known_state` + default flags
fn flags_status(&self) -> Result<Vec<Body>> { fn flags_status(&self) -> Result<Vec<Body<'static>>> {
let mut flags: Vec<Flag> = self let mut body = vec![];
// 1. Collecting all the possible flags in the mailbox
// 1.a Fetch them from our index
let mut known_flags: Vec<Flag> = self
.known_state .known_state
.idx_by_flag .idx_by_flag
.flags() .flags()
.filter_map(|f| string_to_flag(f)) .filter_map(|f| match string_to_flag(f) {
Some(FlagFetch::Flag(fl)) => Some(fl),
_ => None,
})
.collect(); .collect();
// 1.b Merge it with our default flags list
for f in DEFAULT_FLAGS.iter() { for f in DEFAULT_FLAGS.iter() {
if !flags.contains(f) { if !known_flags.contains(f) {
flags.push(f.clone()); known_flags.push(f.clone());
} }
} }
let mut ret = vec![Body::Data(Data::Flags(flags.clone()))]; // 1.c Create the IMAP message
body.push(Body::Data(Data::Flags(known_flags.clone())));
flags.push(Flag::Permanent); // 2. Returning flags that are persisted
let permanent_flags = // 2.a Always advertise our default flags
Status::ok(None, Some(Code::PermanentFlags(flags)), "Flags permitted") let mut permanent = DEFAULT_FLAGS
.iter()
.map(|f| FlagPerm::Flag(f.clone()))
.collect::<Vec<_>>();
// 2.b Say that we support any keyword flag
permanent.push(FlagPerm::Asterisk);
// 2.c Create the IMAP message
let permanent_flags = Status::ok(
None,
Some(Code::PermanentFlags(permanent)),
"Flags permitted",
)
.map_err(Error::msg)?; .map_err(Error::msg)?;
ret.push(Body::Status(permanent_flags)); body.push(Body::Status(permanent_flags));
Ok(ret) // Done!
Ok(body)
} }
pub(crate) fn unseen_count(&self) -> usize { pub(crate) fn unseen_count(&self) -> usize {
@ -809,21 +869,21 @@ impl MailboxView {
} }
} }
fn string_to_flag(f: &str) -> Option<Flag> { fn string_to_flag(f: &str) -> Option<FlagFetch<'static>> {
match f.chars().next() { match f.chars().next() {
Some('\\') => match f { Some('\\') => match f {
"\\Seen" => Some(Flag::Seen), "\\Seen" => Some(FlagFetch::Flag(Flag::Seen)),
"\\Answered" => Some(Flag::Answered), "\\Answered" => Some(FlagFetch::Flag(Flag::Answered)),
"\\Flagged" => Some(Flag::Flagged), "\\Flagged" => Some(FlagFetch::Flag(Flag::Flagged)),
"\\Deleted" => Some(Flag::Deleted), "\\Deleted" => Some(FlagFetch::Flag(Flag::Deleted)),
"\\Draft" => Some(Flag::Draft), "\\Draft" => Some(FlagFetch::Flag(Flag::Draft)),
"\\Recent" => Some(Flag::Recent), "\\Recent" => Some(FlagFetch::Recent),
_ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) { _ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) {
Err(_) => { Err(_) => {
tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); tracing::error!(flag=%f, "Unable to encode flag as IMAP atom");
None None
} }
Ok(a) => Some(Flag::Extension(a)), Ok(a) => Some(FlagFetch::Flag(Flag::system(a))),
}, },
}, },
Some(_) => match Atom::try_from(f.to_string()) { Some(_) => match Atom::try_from(f.to_string()) {
@ -831,7 +891,7 @@ fn string_to_flag(f: &str) -> Option<Flag> {
tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); tracing::error!(flag=%f, "Unable to encode flag as IMAP atom");
None None
} }
Ok(a) => Some(Flag::Keyword(a)), Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))),
}, },
None => None, None => None,
} }
@ -858,7 +918,7 @@ fn string_to_flag(f: &str) -> Option<Flag> {
//@FIXME return an error if the envelope is invalid instead of panicking //@FIXME return an error if the envelope is invalid instead of panicking
//@FIXME some fields must be defaulted if there are not set. //@FIXME some fields must be defaulted if there are not set.
fn message_envelope(msg: &imf::Imf) -> Envelope { fn message_envelope(msg: &imf::Imf) -> Envelope<'static> {
let from = msg.from.iter().map(convert_mbx).collect::<Vec<_>>(); let from = msg.from.iter().map(convert_mbx).collect::<Vec<_>>();
Envelope { Envelope {
@ -900,7 +960,7 @@ fn message_envelope(msg: &imf::Imf) -> Envelope {
} }
} }
fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address> { fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address<'static>> {
let mut acc = vec![]; let mut acc = vec![];
for item in addrlist { for item in addrlist {
match item { match item {
@ -911,23 +971,23 @@ fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address> {
return acc; return acc;
} }
fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address { fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address<'static> {
Address::new( Address {
NString( name: NString(
addr.name addr.name
.as_ref() .as_ref()
.map(|x| IString::try_from(x.to_string()).unwrap()), .map(|x| IString::try_from(x.to_string()).unwrap()),
), ),
// SMTP at-domain-list (source route) seems obsolete since at least 1991 // SMTP at-domain-list (source route) seems obsolete since at least 1991
// https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html // https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html
NString(None), adl: NString(None),
NString(Some( mailbox: NString(Some(
IString::try_from(addr.addrspec.local_part.to_string()).unwrap(), IString::try_from(addr.addrspec.local_part.to_string()).unwrap(),
)), )),
NString(Some( host: NString(Some(
IString::try_from(addr.addrspec.domain.to_string()).unwrap(), IString::try_from(addr.addrspec.domain.to_string()).unwrap(),
)), )),
) }
} }
/* /*
@ -945,19 +1005,23 @@ b fetch 29878:29879 (BODY)
b OK Fetch completed (0.001 + 0.000 secs). b OK Fetch completed (0.001 + 0.000 secs).
*/ */
fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> { fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure<'static>> {
match part { match part {
AnyPart::Mult(x) => { AnyPart::Mult(x) => {
let itype = &x.mime.interpreted_type; let itype = &x.mime.interpreted_type;
let subtype = IString::try_from(itype.subtype.to_string()) let subtype = IString::try_from(itype.subtype.to_string())
.unwrap_or(unchecked_istring("alternative")); .unwrap_or(unchecked_istring("alternative"));
Ok(BodyStructure::Multi { let inner_bodies = x
bodies: x
.children .children
.iter() .iter()
.filter_map(|inner| build_imap_email_struct(&inner).ok()) .filter_map(|inner| build_imap_email_struct(&inner).ok())
.collect(), .collect::<Vec<_>>();
NonEmptyVec::validate(&inner_bodies)?;
let bodies = NonEmptyVec::unvalidated(inner_bodies);
Ok(BodyStructure::Multi {
bodies,
subtype, subtype,
extension_data: None, extension_data: None,
/*Some(MultipartExtensionData { /*Some(MultipartExtensionData {
@ -996,7 +1060,7 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
number_of_lines: nol(x.body), number_of_lines: nol(x.body),
}, },
}, },
extension: None, extension_data: None,
}) })
} }
AnyPart::Bin(x) => { AnyPart::Bin(x) => {
@ -1009,7 +1073,8 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
}; };
let ct = x.mime.fields.ctype.as_ref().unwrap_or(&default); let ct = x.mime.fields.ctype.as_ref().unwrap_or(&default);
let type_ = IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err( let r#type =
IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err(
anyhow!("Unable to build IString from given Content-Type type given"), anyhow!("Unable to build IString from given Content-Type type given"),
))?; ))?;
@ -1021,9 +1086,9 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
Ok(BodyStructure::Single { Ok(BodyStructure::Single {
body: FetchBody { body: FetchBody {
basic, basic,
specific: SpecificFields::Basic { type_, subtype }, specific: SpecificFields::Basic { r#type, subtype },
}, },
extension: None, extension_data: None,
}) })
} }
AnyPart::Msg(x) => { AnyPart::Msg(x) => {
@ -1033,12 +1098,12 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
body: FetchBody { body: FetchBody {
basic, basic,
specific: SpecificFields::Message { specific: SpecificFields::Message {
envelope: message_envelope(&x.imf), envelope: Box::new(message_envelope(&x.imf)),
body_structure: Box::new(build_imap_email_struct(x.child.as_ref())?), body_structure: Box::new(build_imap_email_struct(x.child.as_ref())?),
number_of_lines: nol(x.raw_part), number_of_lines: nol(x.raw_part),
}, },
}, },
extension: None, extension_data: None,
}) })
} }
} }
@ -1059,7 +1124,7 @@ fn unchecked_istring(s: &'static str) -> IString {
IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString")
} }
fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result<BasicFields> { fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result<BasicFields<'static>> {
let parameter_list = m let parameter_list = m
.ctype .ctype
.as_ref() .as_ref()
@ -1136,8 +1201,7 @@ fn get_message_section<'a>(
.ok_or(anyhow!("Part must be a message"))?; .ok_or(anyhow!("Part must be a message"))?;
match section { match section {
Some(FetchSection::Text(None)) => Ok(msg.raw_body.into()), Some(FetchSection::Text(None)) => Ok(msg.raw_body.into()),
Some(FetchSection::Text(Some(part))) => { Some(FetchSection::Text(Some(part))) => map_subpart(parsed, part.0.as_ref(), |part_msg| {
map_subpart(parsed, part.0.as_slice(), |part_msg| {
Ok(part_msg Ok(part_msg
.as_message() .as_message()
.ok_or(Error::msg( .ok_or(Error::msg(
@ -1145,11 +1209,10 @@ fn get_message_section<'a>(
))? ))?
.raw_body .raw_body
.into()) .into())
}) }),
}
Some(FetchSection::Header(part)) => map_subpart( Some(FetchSection::Header(part)) => map_subpart(
parsed, parsed,
part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]),
|part_msg| { |part_msg| {
Ok(part_msg Ok(part_msg
.as_message() .as_message()
@ -1165,17 +1228,18 @@ fn get_message_section<'a>(
) => { ) => {
let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _))); let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _)));
let fields = fields let fields = fields
.as_ref()
.iter() .iter()
.map(|x| match x { .map(|x| match x {
AString::Atom(a) => a.as_bytes(), AString::Atom(a) => a.inner().as_bytes(),
AString::String(IString::Literal(l)) => l.as_slice(), AString::String(IString::Literal(l)) => l.as_ref(),
AString::String(IString::Quoted(q)) => q.as_bytes(), AString::String(IString::Quoted(q)) => q.inner().as_bytes(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
map_subpart( map_subpart(
parsed, parsed,
part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]),
|part_msg| { |part_msg| {
let mut ret = vec![]; let mut ret = vec![];
for f in &part_msg.mime().kv { for f in &part_msg.mime().kv {
@ -1195,7 +1259,7 @@ fn get_message_section<'a>(
}, },
) )
} }
Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_slice(), |part| { Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_ref(), |part| {
let bytes = match &part { let bytes = match &part {
AnyPart::Txt(p) => p.body, AnyPart::Txt(p) => p.body,
AnyPart::Bin(p) => p.body, AnyPart::Bin(p) => p.body,
@ -1204,7 +1268,7 @@ fn get_message_section<'a>(
}; };
Ok(bytes.to_vec().into()) Ok(bytes.to_vec().into())
}), }),
Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_slice(), |part| { Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_ref(), |part| {
let bytes = match &part { let bytes = match &part {
AnyPart::Txt(p) => p.mime.fields.raw, AnyPart::Txt(p) => p.mime.fields.raw,
AnyPart::Bin(p) => p.mime.fields.raw, AnyPart::Bin(p) => p.mime.fields.raw,
@ -1245,18 +1309,22 @@ mod tests {
use super::*; use super::*;
use crate::cryptoblob; use crate::cryptoblob;
use crate::mail::unique_ident; use crate::mail::unique_ident;
use imap_codec::codec::Encode; use imap_codec::encode::Encoder;
use imap_codec::types::fetch_attributes::Section; use imap_codec::imap_types::fetch::Section;
use imap_codec::imap_types::response::Response;
use imap_codec::ResponseCodec;
use std::fs; use std::fs;
#[test] #[test]
fn mailview_body_ext() -> Result<()> { fn mailview_body_ext() -> Result<()> {
let ap = AttributesProxy::new( let ap = AttributesProxy::new(
&MacroOrFetchAttributes::FetchAttributes(vec![FetchAttribute::BodyExt { &MacroOrMessageDataItemNames::MessageDataItemNames(vec![
MessageDataItemName::BodyExt {
section: Some(Section::Header(None)), section: Some(Section::Header(None)),
partial: None, partial: None,
peek: false, peek: false,
}]), },
]),
false, false,
); );
@ -1276,27 +1344,26 @@ mod tests {
let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world";
let content = FetchedMail::new_from_message(eml_codec::parse_message(rfc822)?.1); let content = FetchedMail::new_from_message(eml_codec::parse_message(rfc822)?.1);
let mut mv = MailView { let mv = MailView {
ids: &ids, ids: &ids,
content, content,
meta: &meta, meta: &meta,
flags: &flags, flags: &flags,
add_seen: false,
}; };
let res_body = mv.filter(&ap)?; let (res_body, _seen) = mv.filter(&ap)?;
let fattr = match res_body { let fattr = match res_body {
Body::Data(Data::Fetch { Body::Data(Data::Fetch {
seq_or_uid: _seq, seq: _seq,
attributes: attr, items: attr,
}) => Ok(attr), }) => Ok(attr),
_ => Err(anyhow!("Not a fetch body")), _ => Err(anyhow!("Not a fetch body")),
}?; }?;
assert_eq!(fattr.len(), 1); assert_eq!(fattr.as_ref().len(), 1);
let (sec, _orig, _data) = match &fattr[0] { let (sec, _orig, _data) = match &fattr.as_ref()[0] {
MessageAttribute::BodyExt { MessageDataItem::BodyExt {
section, section,
origin, origin,
data, data,
@ -1345,22 +1412,24 @@ mod tests {
for pref in prefixes.iter() { for pref in prefixes.iter() {
println!("{}", pref); println!("{}", pref);
let txt = fs::read(format!("{}.eml", pref))?; let txt = fs::read(format!("{}.eml", pref))?;
let exp = fs::read(format!("{}.dovecot.body", pref))?; let oracle = fs::read(format!("{}.dovecot.body", pref))?;
let message = eml_codec::parse_message(&txt).unwrap().1; let message = eml_codec::parse_message(&txt).unwrap().1;
let mut resp = Vec::new(); let test_repr = Response::Data(Data::Fetch {
MessageAttribute::Body(build_imap_email_struct(&message.child)?) seq: NonZeroU32::new(1).unwrap(),
.encode(&mut resp) items: NonEmptyVec::from(MessageDataItem::Body(build_imap_email_struct(
.unwrap(); &message.child,
)?)),
});
let test_bytes = ResponseCodec::new().encode(&test_repr).dump();
let test_str = String::from_utf8_lossy(&test_bytes).to_lowercase();
let resp_str = String::from_utf8_lossy(&resp).to_lowercase(); let oracle_str =
format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).to_lowercase();
let exp_no_parenthesis = &exp[1..exp.len() - 1]; println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str);
let exp_str = String::from_utf8_lossy(exp_no_parenthesis).to_lowercase();
println!("aerogramme: {}\n\ndovecot: {}\n\n", resp_str, exp_str);
//println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp));
assert_eq!(resp_str, exp_str); assert_eq!(test_str, oracle_str);
} }
Ok(()) Ok(())

View file

@ -1,105 +1,186 @@
mod command; mod command;
mod flow; mod flow;
mod mailbox_view; mod mailbox_view;
mod response;
mod session; mod session;
use std::task::{Context, Poll}; use std::net::SocketAddr;
use anyhow::Result; use anyhow::Result;
use boitalettres::errors::Error as BalError; use futures::stream::{FuturesUnordered, StreamExt};
use boitalettres::proto::{Request, Response};
use boitalettres::server::accept::addr::AddrIncoming; use tokio::net::TcpListener;
use boitalettres::server::accept::addr::AddrStream;
use boitalettres::server::Server as ImapServer;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use tokio::sync::watch; use tokio::sync::watch;
use tower::Service;
use imap_codec::imap_types::response::Greeting;
use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions};
use imap_flow::stream::AnyStream;
use crate::config::ImapConfig; use crate::config::ImapConfig;
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(ImapServer<AddrIncoming, Instance>); pub struct Server {
bind_addr: SocketAddr,
login_provider: ArcLoginProvider,
}
pub async fn new(config: ImapConfig, login: ArcLoginProvider) -> Result<Server> { struct ClientContext {
//@FIXME add a configuration parameter stream: AnyStream,
let incoming = AddrIncoming::new(config.bind_addr).await?; addr: SocketAddr,
tracing::info!("IMAP activated, will listen on {:#}", incoming.local_addr); login_provider: ArcLoginProvider,
must_exit: watch::Receiver<bool>,
}
let imap = ImapServer::new(incoming).serve(Instance::new(login.clone())); pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server {
Ok(Server(imap)) Server {
bind_addr: config.bind_addr,
login_provider: login,
}
} }
impl Server { impl Server {
pub async fn run(self, mut must_exit: watch::Receiver<bool>) -> Result<()> { pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
tracing::info!("IMAP started!"); let tcp = TcpListener::bind(self.bind_addr).await?;
tokio::select! { tracing::info!("IMAP server listening on {:#}", self.bind_addr);
s = self.0 => s?,
_ = must_exit.changed() => tracing::info!("Stopped IMAP server"), let mut connections = FuturesUnordered::new();
while !*must_exit.borrow() {
let wait_conn_finished = async {
if connections.is_empty() {
futures::future::pending().await
} else {
connections.next().await
} }
};
let (socket, remote_addr) = tokio::select! {
a = tcp.accept() => a?,
_ = wait_conn_finished => continue,
_ = must_exit.changed() => continue,
};
tracing::info!("IMAP: accepted connection from {}", remote_addr);
let client = ClientContext {
stream: AnyStream::new(socket),
addr: remote_addr.clone(),
login_provider: self.login_provider.clone(),
must_exit: must_exit.clone(),
};
let conn = tokio::spawn(client_wrapper(client));
connections.push(conn);
}
drop(tcp);
tracing::info!("IMAP server shutting down, draining remaining connections...");
while connections.next().await.is_some() {}
Ok(()) Ok(())
} }
} }
//--- async fn client_wrapper(ctx: ClientContext) {
let addr = ctx.addr.clone();
/// Instance is the main Tokio Tower service that we register in BàL. match client(ctx).await {
/// It receives new connection demands and spawn a dedicated service. Ok(()) => {
struct Instance { tracing::info!("closing successful session for {:?}", addr);
login_provider: ArcLoginProvider,
}
impl Instance {
pub fn new(login_provider: ArcLoginProvider) -> Self {
Self { login_provider }
} }
} Err(e) => {
tracing::error!("closing errored session for {:?}: {}", addr, e);
impl<'a> Service<&'a AddrStream> for Instance {
type Response = Connection;
type Error = anyhow::Error;
type Future = BoxFuture<'static, Result<Self::Response>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, addr: &'a AddrStream) -> Self::Future {
tracing::info!(remote_addr = %addr.remote_addr, local_addr = %addr.local_addr, "accept");
let lp = self.login_provider.clone();
async { Ok(Connection::new(lp)) }.boxed()
}
}
//---
/// Connection is the per-connection Tokio Tower service we register in BàL.
/// It handles a single TCP connection, and thus has a business logic.
struct Connection {
session: session::Manager,
}
impl Connection {
pub fn new(login_provider: ArcLoginProvider) -> Self {
Self {
session: session::Manager::new(login_provider),
} }
} }
} }
impl Service<Request> for Connection { async fn client(mut ctx: ClientContext) -> Result<()> {
type Response = Response; // Send greeting
type Error = BalError; let (mut server, _) = ServerFlow::send_greeting(
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>; ctx.stream,
ServerFlowOptions::default(),
Greeting::ok(None, "Aerogramme").unwrap(),
)
.await?;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { use crate::imap::response::{Body, Response as MyResponse};
Poll::Ready(Ok(())) use crate::imap::session::Instance;
use imap_codec::imap_types::command::Command;
use imap_codec::imap_types::response::{Response, Status};
use tokio::sync::mpsc;
let (cmd_tx, mut cmd_rx) = mpsc::channel::<Command<'static>>(10);
let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::<MyResponse<'static>>();
let bckgrnd = tokio::spawn(async move {
let mut session = Instance::new(ctx.login_provider);
loop {
let cmd = match cmd_rx.recv().await {
None => break,
Some(cmd_recv) => cmd_recv,
};
let maybe_response = session.command(cmd).await;
match resp_tx.send(maybe_response) {
Err(_) => break,
Ok(_) => (),
};
}
tracing::info!("runner is quitting");
});
// Main loop
loop {
tokio::select! {
// Managing imap_flow stuff
srv_evt = server.progress() => match srv_evt? {
ServerFlowEvent::ResponseSent { handle: _handle, response } => {
match response {
Response::Status(Status::Bye(_)) => break,
_ => tracing::trace!("sent to {} content {:?}", ctx.addr, response),
}
},
ServerFlowEvent::CommandReceived { command } => {
match cmd_tx.try_send(command) {
Ok(_) => (),
Err(mpsc::error::TrySendError::Full(_)) => {
server.enqueue_status(Status::bye(None, "Too fast").unwrap());
tracing::error!("client {:?} is sending commands too fast, closing.", ctx.addr);
}
_ => {
server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", ctx.addr);
}
}
},
},
// Managing response generated by Aerogramme
maybe_msg = resp_rx.recv() => {
let response = match maybe_msg {
None => {
server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", ctx.addr);
continue
},
Some(r) => r,
};
for body_elem in response.body.into_iter() {
let _handle = match body_elem {
Body::Data(d) => server.enqueue_data(d),
Body::Status(s) => server.enqueue_status(s),
};
}
server.enqueue_status(response.completion);
},
// When receiving a CTRL+C
_ = ctx.must_exit.changed() => {
server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
},
};
} }
fn call(&mut self, req: Request) -> Self::Future { drop(cmd_tx);
tracing::debug!("Got request: {:#?}", req.command); bckgrnd.await?;
self.session.process(req) Ok(())
}
} }

112
src/imap/response.rs Normal file
View file

@ -0,0 +1,112 @@
use anyhow::Result;
use imap_codec::imap_types::command::Command;
use imap_codec::imap_types::core::Tag;
use imap_codec::imap_types::response::{Code, Data, Status};
pub enum Body<'a> {
Data(Data<'a>),
Status(Status<'a>),
}
pub struct ResponseBuilder<'a> {
tag: Option<Tag<'a>>,
code: Option<Code<'a>>,
text: String,
body: Vec<Body<'a>>,
}
impl<'a> ResponseBuilder<'a> {
pub fn to_req(mut self, cmd: &Command<'a>) -> Self {
self.tag = Some(cmd.tag.clone());
self
}
pub fn tag(mut self, tag: Tag<'a>) -> Self {
self.tag = Some(tag);
self
}
pub fn message(mut self, txt: impl Into<String>) -> Self {
self.text = txt.into();
self
}
pub fn code(mut self, code: Code<'a>) -> Self {
self.code = Some(code);
self
}
pub fn data(mut self, data: Data<'a>) -> Self {
self.body.push(Body::Data(data));
self
}
pub fn many_data(mut self, data: Vec<Data<'a>>) -> Self {
for d in data.into_iter() {
self = self.data(d);
}
self
}
#[allow(dead_code)]
pub fn info(mut self, status: Status<'a>) -> Self {
self.body.push(Body::Status(status));
self
}
#[allow(dead_code)]
pub fn many_info(mut self, status: Vec<Status<'a>>) -> Self {
for d in status.into_iter() {
self = self.info(d);
}
self
}
pub fn set_body(mut self, body: Vec<Body<'a>>) -> Self {
self.body = body;
self
}
pub fn ok(self) -> Result<Response<'a>> {
Ok(Response {
completion: Status::ok(self.tag, self.code, self.text)?,
body: self.body,
})
}
pub fn no(self) -> Result<Response<'a>> {
Ok(Response {
completion: Status::no(self.tag, self.code, self.text)?,
body: self.body,
})
}
pub fn bad(self) -> Result<Response<'a>> {
Ok(Response {
completion: Status::bad(self.tag, self.code, self.text)?,
body: self.body,
})
}
}
pub struct Response<'a> {
pub body: Vec<Body<'a>>,
pub completion: Status<'a>,
}
impl<'a> Response<'a> {
pub fn build() -> ResponseBuilder<'a> {
ResponseBuilder {
tag: None,
code: None,
text: "".to_string(),
body: vec![],
}
}
pub fn bye() -> Result<Response<'a>> {
Ok(Response {
completion: Status::bye(None, "bye")?,
body: vec![],
})
}
}

View file

@ -1,121 +1,40 @@
use anyhow::Error;
use boitalettres::errors::Error as BalError;
use boitalettres::proto::{Request, Response};
use futures::future::BoxFuture;
use futures::future::FutureExt;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::{mpsc, oneshot};
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::login::ArcLoginProvider; use crate::login::ArcLoginProvider;
use imap_codec::imap_types::command::Command;
/* This constant configures backpressure in the system,
* or more specifically, how many pipelined messages are allowed
* before refusing them
*/
const MAX_PIPELINED_COMMANDS: usize = 10;
struct Message {
req: Request,
tx: oneshot::Sender<Result<Response, BalError>>,
}
//----- //-----
pub struct Manager {
tx: mpsc::Sender<Message>,
}
impl Manager {
pub fn new(login_provider: ArcLoginProvider) -> Self {
let (tx, rx) = mpsc::channel(MAX_PIPELINED_COMMANDS);
tokio::spawn(async move {
let instance = Instance::new(login_provider, rx);
instance.start().await;
});
Self { tx }
}
pub fn process(&self, req: Request) -> BoxFuture<'static, Result<Response, BalError>> {
let (tx, rx) = oneshot::channel();
let msg = Message { req, tx };
// We use try_send on a bounded channel to protect the daemons from DoS.
// Pipelining requests in IMAP are a special case: they should not occure often
// and in a limited number (like 3 requests). Someone filling the channel
// will probably be malicious so we "rate limit" them.
match self.tx.try_send(msg) {
Ok(()) => (),
Err(TrySendError::Full(_)) => {
return async { Response::bad("Too fast! Send less pipelined requests.") }.boxed()
}
Err(TrySendError::Closed(_)) => {
return async { Err(BalError::Text("Terminated session".to_string())) }.boxed()
}
};
// @FIXME add a timeout, handle a session that fails.
async {
match rx.await {
Ok(r) => r,
Err(e) => {
tracing::warn!("Got error {:#?}", e);
Response::bad("No response from the session handler")
}
}
}
.boxed()
}
}
//-----
pub struct Instance { pub struct Instance {
rx: mpsc::Receiver<Message>,
pub login_provider: ArcLoginProvider, pub login_provider: ArcLoginProvider,
pub state: flow::State, pub state: flow::State,
} }
impl Instance { impl Instance {
fn new(login_provider: ArcLoginProvider, rx: mpsc::Receiver<Message>) -> Self { pub fn new(login_provider: ArcLoginProvider) -> Self {
Self { Self {
login_provider, login_provider,
rx,
state: flow::State::NotAuthenticated, state: flow::State::NotAuthenticated,
} }
} }
//@FIXME add a function that compute the runner's name from its local info pub async fn command(&mut self, cmd: Command<'static>) -> Response<'static> {
// to ease debug
// fn name(&self) -> String { }
async fn start(mut self) {
//@FIXME add more info about the runner
tracing::debug!("starting runner");
while let Some(msg) = self.rx.recv().await {
// 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 &mut self.state { let (resp, tr) = match &mut self.state {
flow::State::NotAuthenticated => { flow::State::NotAuthenticated => {
let ctx = anonymous::AnonymousContext { let ctx = anonymous::AnonymousContext {
req: &msg.req, req: &cmd,
login_provider: Some(&self.login_provider), login_provider: &self.login_provider,
}; };
anonymous::dispatch(ctx).await anonymous::dispatch(ctx).await
} }
flow::State::Authenticated(ref user) => { flow::State::Authenticated(ref user) => {
let ctx = authenticated::AuthenticatedContext { let ctx = authenticated::AuthenticatedContext { req: &cmd, user };
req: &msg.req,
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: &msg.req, req: &cmd,
user, user,
mailbox, mailbox,
}; };
@ -123,58 +42,45 @@ 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: &msg.req, req: &cmd,
user, user,
mailbox, mailbox,
}; };
examined::dispatch(ctx).await examined::dispatch(ctx).await
} }
flow::State::Logout => { flow::State::Logout => Response::build()
Response::bad("No commands are allowed in the LOGOUT state.") .tag(cmd.tag.clone())
.map(|r| (r, flow::Transition::None)) .message("No commands are allowed in the LOGOUT state.")
.map_err(Error::msg) .bad()
.map(|r| (r, flow::Transition::None)),
} }
}; .unwrap_or_else(|err| {
tracing::error!("Command error {:?} occured while processing {:?}", err, cmd);
// Process result (
let res = match ctrl { Response::build()
Ok((res, tr)) => { .to_req(&cmd)
//@FIXME remove unwrap .message("Internal error while processing command")
self.state = match self.state.apply(tr) { .bad()
Ok(new_state) => new_state, .unwrap(),
Err(e) => { flow::Transition::None,
tracing::error!("Invalid transition: {}, exiting", e); )
break;
}
};
//@FIXME enrich here the command with some global status
Ok(res)
}
// Cast from anyhow::Error to Bal::Error
// @FIXME proper error handling would be great
Err(e) => match e.downcast::<BalError>() {
Ok(be) => Err(be),
Err(e) => {
tracing::warn!(error=%e, "internal.error");
Response::bad("Internal error")
}
},
};
//@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(res).unwrap_or_else(|e| {
tracing::warn!("failed to send imap response to manager: {:#?}", e)
}); });
if let flow::State::Logout = &self.state { if let Err(e) = self.state.apply(tr) {
break; tracing::error!(
} "Transition error {:?} occured while processing on command {:?}",
e,
cmd
);
return Response::build()
.to_req(&cmd)
.message(
"Internal error, processing command triggered an illegal IMAP state transition",
)
.bad()
.unwrap();
} }
//@FIXME add more info about the runner resp
tracing::debug!("exiting runner");
} }
} }

View file

@ -0,0 +1,51 @@
use crate::login::*;
use crate::storage::*;
pub struct DemoLoginProvider {
keys: CryptoKeys,
in_memory_store: in_memory::MemDb,
}
impl DemoLoginProvider {
pub fn new() -> Self {
Self {
keys: CryptoKeys::init(),
in_memory_store: in_memory::MemDb::new(),
}
}
}
#[async_trait]
impl LoginProvider for DemoLoginProvider {
async fn login(&self, username: &str, password: &str) -> Result<Credentials> {
tracing::debug!(user=%username, "login");
if username != "alice" {
bail!("user does not exist");
}
if password != "hunter2" {
bail!("wrong password");
}
let storage = self.in_memory_store.builder("alice").await;
let keys = self.keys.clone();
Ok(Credentials { storage, keys })
}
async fn public_login(&self, email: &str) -> Result<PublicCredentials> {
tracing::debug!(user=%email, "public_login");
if email != "alice@example.tld" {
bail!("invalid email address");
}
let storage = self.in_memory_store.builder("alice").await;
let public_key = self.keys.public.clone();
Ok(PublicCredentials {
storage,
public_key,
})
}
}

View file

@ -1,3 +1,4 @@
pub mod demo_provider;
pub mod ldap_provider; pub mod ldap_provider;
pub mod static_provider; pub mod static_provider;

View file

@ -29,7 +29,12 @@ struct Args {
#[clap(subcommand)] #[clap(subcommand)]
command: Command, command: Command,
/// A special mode dedicated to developers, NOT INTENDED FOR PRODUCTION
#[clap(long)]
dev: bool,
#[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")] #[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")]
/// Path to the main Aerogramme configuration file
config_file: PathBuf, config_file: PathBuf,
} }
@ -158,7 +163,22 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let args = Args::parse(); let args = Args::parse();
let any_config = read_config(args.config_file)?; let any_config = if args.dev {
use std::net::*;
AnyConfig::Provider(ProviderConfig {
pid: None,
imap: ImapConfig {
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143),
},
lmtp: LmtpConfig {
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025),
hostname: "example.tld".to_string(),
},
users: UserManagement::Demo,
})
} else {
read_config(args.config_file)?
};
match (&args.command, any_config) { match (&args.command, any_config) {
(Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand { (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand {
@ -184,8 +204,8 @@ async fn main() -> Result<()> {
ProviderCommand::Account(cmd) => { ProviderCommand::Account(cmd) => {
let user_file = match config.users { let user_file = match config.users {
UserManagement::Static(conf) => conf.user_list, UserManagement::Static(conf) => conf.user_list,
UserManagement::Ldap(_) => { _ => {
panic!("LDAP account management is not supported from Aerogramme.") panic!("Only static account management is supported from Aerogramme.")
} }
}; };
account_management(&args.command, cmd, user_file)?; account_management(&args.command, cmd, user_file)?;

View file

@ -11,7 +11,7 @@ use crate::config::*;
use crate::imap; use crate::imap;
use crate::lmtp::*; use crate::lmtp::*;
use crate::login::ArcLoginProvider; use crate::login::ArcLoginProvider;
use crate::login::{ldap_provider::*, static_provider::*}; use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*};
pub struct Server { pub struct Server {
lmtp_server: Option<Arc<LmtpServer>>, lmtp_server: Option<Arc<LmtpServer>>,
@ -25,7 +25,7 @@ impl Server {
let login = Arc::new(StaticLoginProvider::new(config.users).await?); let login = Arc::new(StaticLoginProvider::new(config.users).await?);
let lmtp_server = None; let lmtp_server = None;
let imap_server = Some(imap::new(config.imap, login.clone()).await?); let imap_server = Some(imap::new(config.imap, login.clone()));
Ok(Self { Ok(Self {
lmtp_server, lmtp_server,
imap_server, imap_server,
@ -36,12 +36,13 @@ impl Server {
pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> { pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> {
tracing::info!("Init as provider"); tracing::info!("Init as provider");
let login: ArcLoginProvider = match config.users { let login: ArcLoginProvider = match config.users {
UserManagement::Demo => Arc::new(DemoLoginProvider::new()),
UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?), UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?),
UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?), UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?),
}; };
let lmtp_server = Some(LmtpServer::new(config.lmtp, login.clone())); let lmtp_server = Some(LmtpServer::new(config.lmtp, login.clone()));
let imap_server = Some(imap::new(config.imap, login.clone()).await?); let imap_server = Some(imap::new(config.imap, login.clone()));
Ok(Self { Ok(Self {
lmtp_server, lmtp_server,

394
tests/imap_features.rs Normal file
View file

@ -0,0 +1,394 @@
use anyhow::{bail, Context, Result};
use std::io::{Read, Write};
use std::net::{Shutdown, TcpStream};
use std::process::Command;
use std::{thread, time};
static SMALL_DELAY: time::Duration = time::Duration::from_millis(200);
static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r
From: Bob Robert <bob@example.tld>\r
To: Alice Malice <alice@example.tld>\r
CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\r
Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r
=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r
X-Unknown: something something\r
Bad entry\r
on multiple lines\r
Message-ID: <NTAxNzA2AC47634Y366BAMTY4ODc5MzQyODY0ODY5@www.grrrndzero.org>\r
MIME-Version: 1.0\r
Content-Type: multipart/alternative;\r
boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r
Content-Transfer-Encoding: 7bit\r
\r
This is a multi-part message in MIME format.\r
\r
--b1_e376dc71bafc953c0b0fdeb9983a9956\r
Content-Type: text/plain; charset=utf-8\r
Content-Transfer-Encoding: quoted-printable\r
\r
GZ\r
OoOoO\r
oOoOoOoOo\r
oOoOoOoOoOoOoOoOo\r
oOoOoOoOoOoOoOoOoOoOoOo\r
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r
\r
--b1_e376dc71bafc953c0b0fdeb9983a9956\r
Content-Type: text/html; charset=us-ascii\r
\r
<div style=\"text-align: center;\"><strong>GZ</strong><br />\r
OoOoO<br />\r
oOoOoOoOo<br />\r
oOoOoOoOoOoOoOoOo<br />\r
oOoOoOoOoOoOoOoOoOoOoOo<br />\r
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo<br />\r
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />\r
</div>\r
\r
--b1_e376dc71bafc953c0b0fdeb9983a9956--\r
";
static EMAIL2: &[u8] = b"From: alice@example.com\r
To: alice@example.tld\r
Subject: Test\r
\r
Hello world!\r
";
fn main() {
let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme"))
.arg("--dev")
.arg("provider")
.arg("daemon")
.spawn()
.expect("daemon should be started");
let mut max_retry = 20;
let mut imap_socket = loop {
max_retry -= 1;
match (TcpStream::connect("[::1]:1143"), max_retry) {
(Err(e), 0) => panic!("no more retry, last error is: {}", e),
(Err(e), _) => {
println!("unable to connect: {} ; will retry in 1 sec", e);
}
(Ok(v), _) => break v,
}
thread::sleep(SMALL_DELAY);
};
let mut lmtp_socket = TcpStream::connect("[::1]:1025").expect("lmtp socket must be connected");
println!("-- ready to test imap features --");
let result = generic_test(&mut imap_socket, &mut lmtp_socket);
println!("-- test teardown --");
imap_socket
.shutdown(Shutdown::Both)
.expect("closing imap socket at the end of the test");
lmtp_socket
.shutdown(Shutdown::Both)
.expect("closing lmtp socket at the end of the test");
daemon.kill().expect("daemon should be killed");
result.expect("all tests passed");
}
fn generic_test(imap_socket: &mut TcpStream, lmtp_socket: &mut TcpStream) -> Result<()> {
connect(imap_socket).context("server says hello")?;
capability(imap_socket).context("check server capabilities")?;
login(imap_socket).context("login test")?;
create_mailbox(imap_socket).context("created mailbox archive")?;
// UNSUBSCRIBE IS NOT IMPLEMENTED YET
//unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?;
select_inbox(imap_socket).context("select inbox")?;
check(imap_socket).context("check must run")?;
status_mailbox(imap_socket).context("status of archive from inbox")?;
lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
lmtp_deliver_email(lmtp_socket, EMAIL1).context("mail delivered successfully")?;
noop_exists(imap_socket).context("noop loop must detect a new email")?;
fetch_rfc822(imap_socket, EMAIL1).context("fetch rfc822 message")?;
copy_email(imap_socket).context("copy message to the archive mailbox")?;
append_email(imap_socket, EMAIL2).context("insert email in INBOX")?;
// SEARCH IS NOT IMPLEMENTED YET
//search(imap_socket).expect("search should return something");
add_flags_email(imap_socket).context("should add delete and important flags to the email")?;
expunge(imap_socket).context("expunge emails")?;
rename_mailbox(imap_socket).context("archive mailbox is renamed my-archives")?;
delete_mailbox(imap_socket).context("my-archives mailbox is deleted")?;
Ok(())
}
fn connect(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 1500] = [0; 1500];
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..4], &b"* OK"[..]);
Ok(())
}
fn capability(imap: &mut TcpStream) -> Result<()> {
imap.write(&b"5 capability\r\n"[..])?;
let mut buffer: [u8; 1500] = [0; 1500];
let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
assert!(srv_msg.contains("IMAP4REV1"));
assert!(srv_msg.contains("IDLE"));
Ok(())
}
fn login(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 1500] = [0; 1500];
imap.write(&b"10 login alice hunter2\r\n"[..])?;
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..5], &b"10 OK"[..]);
Ok(())
}
fn create_mailbox(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 1500] = [0; 1500];
imap.write(&b"15 create archive\r\n"[..])?;
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..12], &b"15 OK CREATE"[..]);
Ok(())
}
#[allow(dead_code)]
fn unsubscribe_mailbox(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 6000] = [0; 6000];
imap.write(&b"16 lsub \"\" *\r\n"[..])?;
let read = read_lines(imap, &mut buffer, Some(&b"16 OK LSUB"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
assert!(srv_msg.contains(" INBOX\r\n"));
assert!(srv_msg.contains(" archive\r\n"));
imap.write(&b"17 unsubscribe archive\r\n"[..])?;
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..5], &b"17 OK"[..]);
imap.write(&b"18 lsub \"\" *\r\n"[..])?;
let read = read_lines(imap, &mut buffer, Some(&b"18 OK LSUB"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
assert!(srv_msg.contains(" INBOX\r\n"));
assert!(!srv_msg.contains(" archive\r\n"));
Ok(())
}
fn select_inbox(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 6000] = [0; 6000];
imap.write(&b"20 select inbox\r\n"[..])?;
let _read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?;
Ok(())
}
fn check(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 1500] = [0; 1500];
imap.write(&b"21 check\r\n"[..])?;
let _read = read_lines(imap, &mut buffer, Some(&b"21 OK"[..]))?;
Ok(())
}
fn status_mailbox(imap: &mut TcpStream) -> Result<()> {
imap.write(&b"25 STATUS archive (UIDNEXT MESSAGES)\r\n"[..])?;
let mut buffer: [u8; 6000] = [0; 6000];
let _read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?;
Ok(())
}
fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 1500] = [0; 1500];
let _read = read_lines(lmtp, &mut buffer, None)?;
assert_eq!(&buffer[..4], &b"220 "[..]);
lmtp.write(&b"LHLO example.tld\r\n"[..])?;
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?;
Ok(())
}
fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> {
let mut buffer: [u8; 1500] = [0; 1500];
lmtp.write(&b"MAIL FROM:<bob@example.tld>\r\n"[..])?;
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?;
lmtp.write(&b"RCPT TO:<alice@example.tld>\r\n"[..])?;
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?;
lmtp.write(&b"DATA\r\n"[..])?;
let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?;
lmtp.write(email)?;
lmtp.write(&b"\r\n.\r\n"[..])?;
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?;
Ok(())
}
fn noop_exists(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 6000] = [0; 6000];
let mut max_retry = 20;
loop {
max_retry -= 1;
imap.write(&b"30 NOOP\r\n"[..])?;
let read = read_lines(imap, &mut buffer, Some(&b"30 OK NOOP"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
match (max_retry, srv_msg.contains("* 1 EXISTS")) {
(_, true) => break,
(0, _) => bail!("no more retry"),
_ => (),
}
thread::sleep(SMALL_DELAY);
}
Ok(())
}
fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> {
let mut buffer: [u8; 65535] = [0; 65535];
imap.write(&b"40 fetch 1 rfc822\r\n"[..])?;
let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
let orig_email = std::str::from_utf8(ref_mail)?;
assert!(srv_msg.contains(orig_email));
Ok(())
}
fn copy_email(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 65535] = [0; 65535];
imap.write(&b"45 copy 1 archive\r\n"[..])?;
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..5], &b"45 OK"[..]);
Ok(())
}
fn append_email(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> {
let mut buffer: [u8; 6000] = [0; 6000];
assert_ne!(ref_mail.len(), 0);
let append_cmd = format!("47 append inbox (\\Seen) {{{}}}\r\n", ref_mail.len());
println!("append cmd: {}", append_cmd);
imap.write(append_cmd.as_bytes())?;
// wait for continuation
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(read[0], b'+');
// write our stuff
imap.write(ref_mail)?;
imap.write(&b"\r\n"[..])?;
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..5], &b"47 OK"[..]);
// noop to force a sync
imap.write(&b"48 NOOP\r\n"[..])?;
let _read = read_lines(imap, &mut buffer, Some(&b"48 OK NOOP"[..]))?;
// check it is stored successfully
imap.write(&b"49 fetch 2 rfc822.size\r\n"[..])?;
let read = read_lines(imap, &mut buffer, Some(&b"49 OK"[..]))?;
let expected = format!("* 2 FETCH (RFC822.SIZE {})", ref_mail.len());
let expbytes = expected.as_bytes();
assert_eq!(&read[..expbytes.len()], expbytes);
Ok(())
}
fn add_flags_email(imap: &mut TcpStream) -> Result<()> {
let mut buffer: [u8; 1500] = [0; 1500];
imap.write(&b"50 store 1 +FLAGS (\\Deleted \\Important)\r\n"[..])?;
let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?;
Ok(())
}
#[allow(dead_code)]
/// Not yet implemented
fn search(imap: &mut TcpStream) -> Result<()> {
imap.write(&b"55 search text \"OoOoO\"\r\n"[..])?;
let mut buffer: [u8; 1500] = [0; 1500];
let _read = read_lines(imap, &mut buffer, Some(&b"55 OK SEARCH"[..]))?;
Ok(())
}
fn expunge(imap: &mut TcpStream) -> Result<()> {
imap.write(&b"60 expunge\r\n"[..])?;
let mut buffer: [u8; 1500] = [0; 1500];
let _read = read_lines(imap, &mut buffer, Some(&b"60 OK EXPUNGE"[..]))?;
Ok(())
}
fn rename_mailbox(imap: &mut TcpStream) -> Result<()> {
imap.write(&b"70 rename archive my-archives\r\n"[..])?;
let mut buffer: [u8; 1500] = [0; 1500];
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..5], &b"70 OK"[..]);
imap.write(&b"71 list \"\" *\r\n"[..])?;
let read = read_lines(imap, &mut buffer, Some(&b"71 OK LIST"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
assert!(!srv_msg.contains(" archive\r\n"));
assert!(srv_msg.contains(" INBOX\r\n"));
assert!(srv_msg.contains(" my-archives\r\n"));
Ok(())
}
fn delete_mailbox(imap: &mut TcpStream) -> Result<()> {
imap.write(&b"80 delete my-archives\r\n"[..])?;
let mut buffer: [u8; 1500] = [0; 1500];
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..5], &b"80 OK"[..]);
imap.write(&b"81 list \"\" *\r\n"[..])?;
let read = read_lines(imap, &mut buffer, Some(&b"81 OK LIST"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
assert!(!srv_msg.contains(" archive\r\n"));
assert!(!srv_msg.contains(" my-archives\r\n"));
assert!(srv_msg.contains(" INBOX\r\n"));
Ok(())
}
fn read_lines<'a, F: Read>(
reader: &mut F,
buffer: &'a mut [u8],
stop_marker: Option<&[u8]>,
) -> Result<&'a [u8]> {
let mut nbytes = 0;
loop {
nbytes += reader.read(&mut buffer[nbytes..])?;
//println!("partial read: {}", std::str::from_utf8(&buffer[..nbytes])?);
let pre_condition = match stop_marker {
None => true,
Some(mark) => buffer[..nbytes].windows(mark.len()).any(|w| w == mark),
};
if pre_condition && &buffer[nbytes - 2..nbytes] == &b"\r\n"[..] {
break;
}
}
println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?);
Ok(&buffer[..nbytes])
}