Implement imap-flow #34
31 changed files with 2415 additions and 1647 deletions
1373
Cargo.lock
generated
1373
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
81
Cargo.toml
81
Cargo.toml
|
@ -7,48 +7,67 @@ license = "AGPL-3.0"
|
|||
description = "Encrypted mail storage over Garage"
|
||||
|
||||
[dependencies]
|
||||
aws-config = { version = "1.1.1", features = ["behavior-version-latest"] }
|
||||
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"
|
||||
# async runtime
|
||||
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" ] }
|
||||
toml = "0.5"
|
||||
zstd = { version = "0.9", default-features = false }
|
||||
futures = "0.3"
|
||||
|
||||
# debug
|
||||
log = "0.4"
|
||||
backtrace = "0.3"
|
||||
tracing-subscriber = "0.3"
|
||||
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"] }
|
||||
|
||||
# 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" }
|
||||
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-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
|
||||
|
||||
#k2v-client = { path = "../garage/src/k2v-client" }
|
||||
imap-codec = { version = "1.0.0", features = ["quirk_crlf_relaxed", "bounded-static"] }
|
||||
imap-flow = { git = "https://github.com/duesee/imap-flow.git", rev = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" }
|
||||
|
||||
[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
|
||||
|
|
|
@ -450,10 +450,7 @@ impl K2vWatch {
|
|||
) {
|
||||
let mut row = match Weak::upgrade(&self_weak) {
|
||||
Some(this) => this.target.clone(),
|
||||
None => {
|
||||
error!("can't start loop");
|
||||
return;
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
while let Some(this) = Weak::upgrade(&self_weak) {
|
||||
|
|
|
@ -26,6 +26,7 @@ pub struct ProviderConfig {
|
|||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "user_driver")]
|
||||
pub enum UserManagement {
|
||||
Demo,
|
||||
Static(LoginStaticConfig),
|
||||
Ldap(LoginLdapConfig),
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -1,92 +1,77 @@
|
|||
use anyhow::{Error, Result};
|
||||
use boitalettres::proto::{res::body::Data as Body, Request, Response};
|
||||
use imap_codec::types::command::CommandBody;
|
||||
use imap_codec::types::core::AString;
|
||||
use imap_codec::types::response::{Capability, Data, Status};
|
||||
use anyhow::Result;
|
||||
use imap_codec::imap_types::command::{Command, CommandBody};
|
||||
use imap_codec::imap_types::core::AString;
|
||||
use imap_codec::imap_types::secret::Secret;
|
||||
|
||||
use crate::imap::command::anystate;
|
||||
use crate::imap::flow;
|
||||
use crate::imap::response::Response;
|
||||
use crate::login::ArcLoginProvider;
|
||||
use crate::mail::user::User;
|
||||
|
||||
//--- dispatching
|
||||
|
||||
pub struct AnonymousContext<'a> {
|
||||
pub req: &'a Request,
|
||||
pub login_provider: Option<&'a ArcLoginProvider>,
|
||||
pub req: &'a Command<'static>,
|
||||
pub login_provider: &'a ArcLoginProvider,
|
||||
}
|
||||
|
||||
pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> {
|
||||
match &ctx.req.command.body {
|
||||
CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)),
|
||||
CommandBody::Capability => ctx.capability().await,
|
||||
CommandBody::Logout => ctx.logout().await,
|
||||
pub async fn dispatch(ctx: AnonymousContext<'_>) -> 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 anonymous context (3 commands)
|
||||
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
|
||||
|
||||
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(
|
||||
self,
|
||||
username: &AString,
|
||||
password: &AString,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
username: &AString<'a>,
|
||||
password: &Secret<AString<'a>>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let (u, p) = (
|
||||
String::try_from(username.clone())?,
|
||||
String::try_from(password.clone())?,
|
||||
std::str::from_utf8(username.as_ref())?,
|
||||
std::str::from_utf8(password.declassify().as_ref())?,
|
||||
);
|
||||
tracing::info!(user = %u, "command.login");
|
||||
|
||||
let login_provider = match &self.login_provider {
|
||||
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 {
|
||||
let creds = match self.login_provider.login(&u, &p).await {
|
||||
Err(e) => {
|
||||
tracing::debug!(error=%e, "authentication failed");
|
||||
return Ok((
|
||||
Response::no("Authentication failed")?,
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Authentication failed")
|
||||
.no()?,
|
||||
flow::Transition::None,
|
||||
));
|
||||
}
|
||||
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");
|
||||
Ok((
|
||||
Response::ok("Completed")?,
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Completed")
|
||||
.ok()?,
|
||||
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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
52
src/imap/command/anystate.rs
Normal file
52
src/imap/command/anystate.rs
Normal 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,
|
||||
))
|
||||
}
|
|
@ -2,37 +2,42 @@ use std::collections::BTreeMap;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use boitalettres::proto::res::body::Data as Body;
|
||||
use boitalettres::proto::{Request, Response};
|
||||
use imap_codec::types::command::{CommandBody, StatusAttribute};
|
||||
use imap_codec::types::core::NonZeroBytes;
|
||||
use imap_codec::types::datetime::MyDateTime;
|
||||
use imap_codec::types::flag::{Flag, FlagNameAttribute};
|
||||
use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
|
||||
use imap_codec::types::response::{Code, Data, StatusAttributeValue};
|
||||
use imap_codec::imap_types::command::{Command, CommandBody};
|
||||
use imap_codec::imap_types::core::{Atom, Literal, QuotedChar};
|
||||
use imap_codec::imap_types::datetime::DateTime;
|
||||
use imap_codec::imap_types::flag::{Flag, FlagNameAttribute};
|
||||
use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
|
||||
use imap_codec::imap_types::response::{Code, CodeOther, Data};
|
||||
use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName};
|
||||
|
||||
use crate::imap::command::anonymous;
|
||||
use crate::imap::command::{anystate, MailboxName};
|
||||
use crate::imap::flow;
|
||||
use crate::imap::mailbox_view::MailboxView;
|
||||
use crate::imap::response::Response;
|
||||
|
||||
use crate::mail::mailbox::Mailbox;
|
||||
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;
|
||||
|
||||
pub struct AuthenticatedContext<'a> {
|
||||
pub req: &'a Request,
|
||||
pub req: &'a Command<'static>,
|
||||
pub user: &'a Arc<User>,
|
||||
}
|
||||
|
||||
pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> {
|
||||
match &ctx.req.command.body {
|
||||
pub async fn dispatch<'a>(
|
||||
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::Delete { mailbox } => ctx.delete(mailbox).await,
|
||||
CommandBody::Rename {
|
||||
mailbox,
|
||||
new_mailbox,
|
||||
} => ctx.rename(mailbox, new_mailbox).await,
|
||||
CommandBody::Rename { from, to } => ctx.rename(from, to).await,
|
||||
CommandBody::Lsub {
|
||||
reference,
|
||||
mailbox_wildcard,
|
||||
|
@ -43,8 +48,8 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::
|
|||
} => ctx.list(reference, mailbox_wildcard, false).await,
|
||||
CommandBody::Status {
|
||||
mailbox,
|
||||
attributes,
|
||||
} => ctx.status(mailbox, attributes).await,
|
||||
item_names,
|
||||
} => ctx.status(mailbox, item_names).await,
|
||||
CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await,
|
||||
CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await,
|
||||
CommandBody::Select { mailbox } => ctx.select(mailbox).await,
|
||||
|
@ -55,90 +60,148 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::
|
|||
date,
|
||||
message,
|
||||
} => ctx.append(mailbox, flags, date, message).await,
|
||||
_ => {
|
||||
let ctx = anonymous::AnonymousContext {
|
||||
req: ctx.req,
|
||||
login_provider: None,
|
||||
};
|
||||
anonymous::dispatch(ctx).await
|
||||
}
|
||||
|
||||
// Collect other commands
|
||||
_ => anystate::wrong_state(ctx.req.tag.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
// --- PRIVATE ---
|
||||
|
||||
impl<'a> AuthenticatedContext<'a> {
|
||||
async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
|
||||
if name == INBOX {
|
||||
return Ok((
|
||||
Response::bad("Cannot create INBOX")?,
|
||||
flow::Transition::None,
|
||||
));
|
||||
}
|
||||
async fn create(
|
||||
self,
|
||||
mailbox: &MailboxCodec<'a>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let name = match mailbox {
|
||||
MailboxCodec::Inbox => {
|
||||
return Ok((
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Cannot create INBOX")
|
||||
.bad()?,
|
||||
flow::Transition::None,
|
||||
));
|
||||
}
|
||||
MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?,
|
||||
};
|
||||
|
||||
match self.user.create_mailbox(&name).await {
|
||||
Ok(()) => Ok((Response::ok("CREATE complete")?, flow::Transition::None)),
|
||||
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
|
||||
Ok(()) => Ok((
|
||||
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)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
async fn delete(
|
||||
self,
|
||||
mailbox: &MailboxCodec<'a>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let name: &str = MailboxName(mailbox).try_into()?;
|
||||
|
||||
match self.user.delete_mailbox(&name).await {
|
||||
Ok(()) => Ok((Response::ok("DELETE complete")?, flow::Transition::None)),
|
||||
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
|
||||
Ok(()) => Ok((
|
||||
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(
|
||||
self,
|
||||
mailbox: &MailboxCodec,
|
||||
new_mailbox: &MailboxCodec,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
let new_name = String::try_from(new_mailbox.clone())?;
|
||||
from: &MailboxCodec<'a>,
|
||||
to: &MailboxCodec<'a>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let name: &str = MailboxName(from).try_into()?;
|
||||
let new_name: &str = MailboxName(to).try_into()?;
|
||||
|
||||
match self.user.rename_mailbox(&name, &new_name).await {
|
||||
Ok(()) => Ok((Response::ok("RENAME complete")?, flow::Transition::None)),
|
||||
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
|
||||
Ok(()) => Ok((
|
||||
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(
|
||||
self,
|
||||
reference: &MailboxCodec,
|
||||
mailbox_wildcard: &ListMailbox,
|
||||
reference: &MailboxCodec<'a>,
|
||||
mailbox_wildcard: &ListMailbox<'a>,
|
||||
is_lsub: bool,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
let reference = String::try_from(reference.clone())?;
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW);
|
||||
|
||||
let reference: &str = MailboxName(reference).try_into()?;
|
||||
if !reference.is_empty() {
|
||||
return Ok((
|
||||
Response::bad("References not supported")?,
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("References not supported")
|
||||
.bad()?,
|
||||
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 is_lsub {
|
||||
return Ok((
|
||||
Response::ok("LSUB complete")?.with_body(vec![Data::Lsub {
|
||||
items: vec![],
|
||||
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER),
|
||||
mailbox: "".try_into().unwrap(),
|
||||
}]),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("LSUB complete")
|
||||
.data(Data::Lsub {
|
||||
items: vec![],
|
||||
delimiter: Some(mbx_hier_delim),
|
||||
mailbox: "".try_into().unwrap(),
|
||||
})
|
||||
.ok()?,
|
||||
flow::Transition::None,
|
||||
));
|
||||
} else {
|
||||
return Ok((
|
||||
Response::ok("LIST complete")?.with_body(vec![Data::List {
|
||||
items: vec![],
|
||||
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER),
|
||||
mailbox: "".try_into().unwrap(),
|
||||
}]),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("LIST complete")
|
||||
.data(Data::List {
|
||||
items: vec![],
|
||||
delimiter: Some(mbx_hier_delim),
|
||||
mailbox: "".try_into().unwrap(),
|
||||
})
|
||||
.ok()?,
|
||||
flow::Transition::None,
|
||||
));
|
||||
}
|
||||
|
@ -147,7 +210,7 @@ impl<'a> AuthenticatedContext<'a> {
|
|||
let mailboxes = self.user.list_mailboxes().await?;
|
||||
let mut vmailboxes = BTreeMap::new();
|
||||
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 {
|
||||
let smb = &mb[..i];
|
||||
vmailboxes.entry(smb).or_insert(false);
|
||||
|
@ -163,22 +226,22 @@ impl<'a> AuthenticatedContext<'a> {
|
|||
.to_string()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("invalid mailbox name"))?;
|
||||
let mut items = vec![FlagNameAttribute::Extension(
|
||||
"Subscribed".try_into().unwrap(),
|
||||
)];
|
||||
let mut items = vec![FlagNameAttribute::try_from(Atom::unvalidated(
|
||||
"Subscribed",
|
||||
))?];
|
||||
if !*is_real {
|
||||
items.push(FlagNameAttribute::Noselect);
|
||||
}
|
||||
if is_lsub {
|
||||
ret.push(Data::Lsub {
|
||||
items,
|
||||
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER),
|
||||
delimiter: Some(mbx_hier_delim),
|
||||
mailbox,
|
||||
});
|
||||
} else {
|
||||
ret.push(Data::List {
|
||||
items,
|
||||
delimiter: Some(MAILBOX_HIERARCHY_DELIMITER),
|
||||
delimiter: Some(mbx_hier_delim),
|
||||
mailbox,
|
||||
});
|
||||
}
|
||||
|
@ -190,79 +253,120 @@ impl<'a> AuthenticatedContext<'a> {
|
|||
} else {
|
||||
"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(
|
||||
self,
|
||||
mailbox: &MailboxCodec,
|
||||
attributes: &[StatusAttribute],
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
let mb_opt = self.user.open_mailbox(&name).await?;
|
||||
mailbox: &MailboxCodec<'static>,
|
||||
attributes: &[StatusDataItemName],
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let name: &str = MailboxName(mailbox).try_into()?;
|
||||
let mb_opt = self.user.open_mailbox(name).await?;
|
||||
let mb = match mb_opt {
|
||||
Some(mb) => mb,
|
||||
None => {
|
||||
return Ok((
|
||||
Response::no("Mailbox does not exist")?,
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Mailbox does not exist")
|
||||
.no()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let (view, _data) = MailboxView::new(mb).await?;
|
||||
let view = MailboxView::new(mb).await;
|
||||
|
||||
let mut ret_attrs = vec![];
|
||||
for attr in attributes.iter() {
|
||||
ret_attrs.push(match attr {
|
||||
StatusAttribute::Messages => StatusAttributeValue::Messages(view.exists()?),
|
||||
StatusAttribute::Unseen => StatusAttributeValue::Unseen(view.unseen_count() as u32),
|
||||
StatusAttribute::Recent => StatusAttributeValue::Recent(view.recent()?),
|
||||
StatusAttribute::UidNext => StatusAttributeValue::UidNext(view.uidnext()),
|
||||
StatusAttribute::UidValidity => {
|
||||
StatusAttributeValue::UidValidity(view.uidvalidity())
|
||||
StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?),
|
||||
StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32),
|
||||
StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?),
|
||||
StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()),
|
||||
StatusDataItemName::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(),
|
||||
attributes: ret_attrs,
|
||||
})];
|
||||
items: ret_attrs.into(),
|
||||
};
|
||||
|
||||
Ok((
|
||||
Response::ok("STATUS completed")?.with_body(data),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("STATUS completed")
|
||||
.data(data)
|
||||
.ok()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
|
||||
async fn subscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
async fn subscribe(
|
||||
self,
|
||||
mailbox: &MailboxCodec<'a>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let name: &str = MailboxName(mailbox).try_into()?;
|
||||
|
||||
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 {
|
||||
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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn unsubscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
async fn unsubscribe(
|
||||
self,
|
||||
mailbox: &MailboxCodec<'a>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let name: &str = MailboxName(mailbox).try_into()?;
|
||||
|
||||
if self.user.has_mailbox(&name).await? {
|
||||
Ok((
|
||||
Response::bad(&format!(
|
||||
"Cannot unsubscribe from mailbox {}: not supported by Aerogramme",
|
||||
name
|
||||
))?,
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message(format!(
|
||||
"Cannot unsubscribe from mailbox {}: not supported by Aerogramme",
|
||||
name
|
||||
))
|
||||
.bad()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
} else {
|
||||
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,
|
||||
))
|
||||
}
|
||||
|
@ -301,83 +405,113 @@ impl<'a> AuthenticatedContext<'a> {
|
|||
|
||||
* TRACE END ---
|
||||
*/
|
||||
async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
async fn select(
|
||||
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 = match mb_opt {
|
||||
Some(mb) => mb,
|
||||
None => {
|
||||
return Ok((
|
||||
Response::no("Mailbox does not exist")?,
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Mailbox does not exist")
|
||||
.no()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
};
|
||||
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((
|
||||
Response::ok("Select completed")?
|
||||
.with_extra_code(Code::ReadWrite)
|
||||
.with_body(data),
|
||||
Response::build()
|
||||
.message("Select completed")
|
||||
.to_req(self.req)
|
||||
.code(Code::ReadWrite)
|
||||
.set_body(data)
|
||||
.ok()?,
|
||||
flow::Transition::Select(mb),
|
||||
))
|
||||
}
|
||||
|
||||
async fn examine(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
async fn examine(
|
||||
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 = match mb_opt {
|
||||
Some(mb) => mb,
|
||||
None => {
|
||||
return Ok((
|
||||
Response::no("Mailbox does not exist")?,
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Mailbox does not exist")
|
||||
.no()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
};
|
||||
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((
|
||||
Response::ok("Examine completed")?
|
||||
.with_extra_code(Code::ReadOnly)
|
||||
.with_body(data),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Examine completed")
|
||||
.code(Code::ReadOnly)
|
||||
.set_body(data)
|
||||
.ok()?,
|
||||
flow::Transition::Examine(mb),
|
||||
))
|
||||
}
|
||||
|
||||
async fn append(
|
||||
self,
|
||||
mailbox: &MailboxCodec,
|
||||
flags: &[Flag],
|
||||
date: &Option<MyDateTime>,
|
||||
message: &NonZeroBytes,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
mailbox: &MailboxCodec<'a>,
|
||||
flags: &[Flag<'a>],
|
||||
date: &Option<DateTime>,
|
||||
message: &Literal<'a>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let append_tag = self.req.tag.clone();
|
||||
match self.append_internal(mailbox, flags, date, message).await {
|
||||
Ok((_mb, uidvalidity, uid)) => Ok((
|
||||
Response::ok("APPEND completed")?.with_extra_code(Code::Other(
|
||||
"APPENDUID".try_into().unwrap(),
|
||||
Some(format!("{} {}", uidvalidity, uid)),
|
||||
)),
|
||||
Response::build()
|
||||
.tag(append_tag)
|
||||
.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,
|
||||
)),
|
||||
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn append_internal(
|
||||
self,
|
||||
mailbox: &MailboxCodec,
|
||||
flags: &[Flag],
|
||||
date: &Option<MyDateTime>,
|
||||
message: &NonZeroBytes,
|
||||
mailbox: &MailboxCodec<'a>,
|
||||
flags: &[Flag<'a>],
|
||||
date: &Option<DateTime>,
|
||||
message: &Literal<'a>,
|
||||
) -> 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 = match mb_opt {
|
||||
|
@ -389,8 +523,8 @@ impl<'a> AuthenticatedContext<'a> {
|
|||
bail!("Cannot set date when appending message");
|
||||
}
|
||||
|
||||
let msg = IMF::try_from(message.as_slice())
|
||||
.map_err(|_| anyhow!("Could not parse e-mail message"))?;
|
||||
let msg =
|
||||
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<_>>();
|
||||
// TODO: filter allowed flags? ping @Quentin
|
||||
|
||||
|
@ -422,7 +556,7 @@ fn matches_wildcard(wildcard: &str, name: &str) -> bool {
|
|||
&& j > 0
|
||||
&& matches[i - 1][j]
|
||||
&& (wildcard[j - 1] == '*'
|
||||
|| (wildcard[j - 1] == '%' && name[i - 1] != MAILBOX_HIERARCHY_DELIMITER)));
|
||||
|| (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,56 +1,60 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use boitalettres::proto::Request;
|
||||
use boitalettres::proto::Response;
|
||||
use imap_codec::types::command::{CommandBody, SearchKey};
|
||||
use imap_codec::types::core::{Charset, NonZeroBytes};
|
||||
use imap_codec::types::datetime::MyDateTime;
|
||||
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 imap_codec::imap_types::command::{Command, CommandBody};
|
||||
use imap_codec::imap_types::core::Charset;
|
||||
use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
|
||||
use imap_codec::imap_types::search::SearchKey;
|
||||
use imap_codec::imap_types::sequence::SequenceSet;
|
||||
|
||||
use crate::imap::command::authenticated;
|
||||
use crate::imap::command::{anystate, authenticated};
|
||||
use crate::imap::flow;
|
||||
use crate::imap::mailbox_view::MailboxView;
|
||||
use crate::imap::response::Response;
|
||||
use crate::mail::user::User;
|
||||
|
||||
pub struct ExaminedContext<'a> {
|
||||
pub req: &'a Request,
|
||||
pub req: &'a Command<'static>,
|
||||
pub user: &'a Arc<User>,
|
||||
pub mailbox: &'a mut MailboxView,
|
||||
}
|
||||
|
||||
pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Transition)> {
|
||||
match &ctx.req.command.body {
|
||||
// CLOSE in examined state is not the same as in selected state
|
||||
// (in selected state it also does an EXPUNGE, here it doesn't)
|
||||
pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> {
|
||||
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 the EXAMINE state (specialization of the SELECTED state)
|
||||
// ~3 commands -> close, fetch, search + NOOP
|
||||
CommandBody::Close => ctx.close().await,
|
||||
CommandBody::Fetch {
|
||||
sequence_set,
|
||||
attributes,
|
||||
macro_or_item_names,
|
||||
uid,
|
||||
} => ctx.fetch(sequence_set, attributes, uid).await,
|
||||
} => ctx.fetch(sequence_set, macro_or_item_names, uid).await,
|
||||
CommandBody::Search {
|
||||
charset,
|
||||
criteria,
|
||||
uid,
|
||||
} => ctx.search(charset, criteria, uid).await,
|
||||
CommandBody::Noop => ctx.noop().await,
|
||||
CommandBody::Append {
|
||||
mailbox,
|
||||
flags,
|
||||
date,
|
||||
message,
|
||||
} => ctx.append(mailbox, flags, date, message).await,
|
||||
CommandBody::Noop | CommandBody::Check => ctx.noop().await,
|
||||
CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok((
|
||||
Response::build()
|
||||
.to_req(ctx.req)
|
||||
.message("Forbidden command: can't write in read-only mode (EXAMINE)")
|
||||
.bad()?,
|
||||
flow::Transition::None,
|
||||
)),
|
||||
|
||||
// In examined mode, we fallback to authenticated when needed
|
||||
_ => {
|
||||
let ctx = authenticated::AuthenticatedContext {
|
||||
authenticated::dispatch(authenticated::AuthenticatedContext {
|
||||
req: ctx.req,
|
||||
user: ctx.user,
|
||||
};
|
||||
authenticated::dispatch(ctx).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,71 +62,69 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Trans
|
|||
// --- PRIVATE ---
|
||||
|
||||
impl<'a> ExaminedContext<'a> {
|
||||
async fn close(self) -> Result<(Response, flow::Transition)> {
|
||||
Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect))
|
||||
/// CLOSE in examined state is not the same as in selected state
|
||||
/// (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(
|
||||
self,
|
||||
sequence_set: &SequenceSet,
|
||||
attributes: &MacroOrFetchAttributes,
|
||||
attributes: &'a MacroOrMessageDataItemNames<'static>,
|
||||
uid: &bool,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
match self.mailbox.fetch(sequence_set, attributes, uid).await {
|
||||
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,
|
||||
)),
|
||||
Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
self,
|
||||
_charset: &Option<Charset>,
|
||||
_criteria: &SearchKey,
|
||||
_charset: &Option<Charset<'a>>,
|
||||
_criteria: &SearchKey<'a>,
|
||||
_uid: &bool,
|
||||
) -> Result<(Response, 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?;
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
Ok((
|
||||
Response::ok("NOOP completed.")?.with_body(updates),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Not implemented")
|
||||
.bad()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
|
||||
async fn append(
|
||||
self,
|
||||
mailbox: &MailboxCodec,
|
||||
flags: &[Flag],
|
||||
date: &Option<MyDateTime>,
|
||||
message: &NonZeroBytes,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
let ctx2 = authenticated::AuthenticatedContext {
|
||||
req: self.req,
|
||||
user: self.user,
|
||||
};
|
||||
pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> {
|
||||
self.mailbox.mailbox.force_sync().await?;
|
||||
|
||||
match ctx2.append_internal(mailbox, flags, date, message).await {
|
||||
Ok((mb, uidvalidity, uid)) => {
|
||||
let resp = Response::ok("APPEND completed")?.with_extra_code(Code::Other(
|
||||
"APPENDUID".try_into().unwrap(),
|
||||
Some(format!("{} {}", uidvalidity, uid)),
|
||||
));
|
||||
|
||||
if Arc::ptr_eq(&mb, &self.mailbox.mailbox) {
|
||||
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)),
|
||||
}
|
||||
let updates = self.mailbox.update().await?;
|
||||
Ok((
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("NOOP completed.")
|
||||
.set_body(updates)
|
||||
.ok()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,21 @@
|
|||
pub mod anonymous;
|
||||
pub mod anystate;
|
||||
pub mod authenticated;
|
||||
pub mod examined;
|
||||
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())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,50 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use boitalettres::proto::Request;
|
||||
use boitalettres::proto::Response;
|
||||
use imap_codec::types::command::CommandBody;
|
||||
use imap_codec::types::flag::{Flag, StoreResponse, StoreType};
|
||||
use imap_codec::types::mailbox::Mailbox as MailboxCodec;
|
||||
use imap_codec::types::response::Code;
|
||||
use imap_codec::types::sequence::SequenceSet;
|
||||
use imap_codec::imap_types::command::{Command, CommandBody};
|
||||
use imap_codec::imap_types::core::Charset;
|
||||
use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
|
||||
use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType};
|
||||
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
|
||||
use imap_codec::imap_types::response::{Code, CodeOther};
|
||||
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::mailbox_view::MailboxView;
|
||||
use crate::imap::response::Response;
|
||||
|
||||
use crate::mail::user::User;
|
||||
|
||||
pub struct SelectedContext<'a> {
|
||||
pub req: &'a Request,
|
||||
pub req: &'a Command<'static>,
|
||||
pub user: &'a Arc<User>,
|
||||
pub mailbox: &'a mut MailboxView,
|
||||
}
|
||||
|
||||
pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Transition)> {
|
||||
match &ctx.req.command.body {
|
||||
// Only write commands here, read commands are handled in
|
||||
// `examined.rs`
|
||||
pub async fn dispatch<'a>(
|
||||
ctx: SelectedContext<'a>,
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
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::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::Store {
|
||||
sequence_set,
|
||||
|
@ -39,13 +58,14 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans
|
|||
mailbox,
|
||||
uid,
|
||||
} => 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,
|
||||
user: ctx.user,
|
||||
mailbox: ctx.mailbox,
|
||||
};
|
||||
examined::dispatch(ctx).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,18 +73,81 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans
|
|||
// --- PRIVATE ---
|
||||
|
||||
impl<'a> SelectedContext<'a> {
|
||||
async fn close(self) -> Result<(Response, flow::Transition)> {
|
||||
async fn close(self) -> Result<(Response<'static>, flow::Transition)> {
|
||||
// We expunge messages,
|
||||
// but we don't send the untagged EXPUNGE responses
|
||||
let tag = self.req.tag.clone();
|
||||
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?;
|
||||
|
||||
Ok((
|
||||
Response::ok("EXPUNGE completed")?.with_body(data),
|
||||
Response::build()
|
||||
.tag(tag)
|
||||
.message("EXPUNGE completed")
|
||||
.set_body(data)
|
||||
.ok()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
|
@ -74,16 +157,20 @@ impl<'a> SelectedContext<'a> {
|
|||
sequence_set: &SequenceSet,
|
||||
kind: &StoreType,
|
||||
response: &StoreResponse,
|
||||
flags: &[Flag],
|
||||
flags: &[Flag<'a>],
|
||||
uid: &bool,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let data = self
|
||||
.mailbox
|
||||
.store(sequence_set, kind, response, flags, uid)
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
Response::ok("STORE completed")?.with_body(data),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("STORE completed")
|
||||
.set_body(data)
|
||||
.ok()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
|
@ -91,18 +178,21 @@ impl<'a> SelectedContext<'a> {
|
|||
async fn copy(
|
||||
self,
|
||||
sequence_set: &SequenceSet,
|
||||
mailbox: &MailboxCodec,
|
||||
mailbox: &MailboxCodec<'a>,
|
||||
uid: &bool,
|
||||
) -> Result<(Response, flow::Transition)> {
|
||||
let name = String::try_from(mailbox.clone())?;
|
||||
) -> Result<(Response<'static>, flow::Transition)> {
|
||||
let name: &str = MailboxName(mailbox).try_into()?;
|
||||
|
||||
let mb_opt = self.user.open_mailbox(&name).await?;
|
||||
let mb = match mb_opt {
|
||||
Some(mb) => mb,
|
||||
None => {
|
||||
return Ok((
|
||||
Response::no("Destination mailbox does not exist")?
|
||||
.with_extra_code(Code::TryCreate),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("Destination mailbox does not exist")
|
||||
.code(Code::TryCreate)
|
||||
.no()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
|
@ -126,10 +216,13 @@ impl<'a> SelectedContext<'a> {
|
|||
);
|
||||
|
||||
Ok((
|
||||
Response::ok("COPY completed")?.with_extra_code(Code::Other(
|
||||
"COPYUID".try_into().unwrap(),
|
||||
Some(copyuid_str),
|
||||
)),
|
||||
Response::build()
|
||||
.to_req(self.req)
|
||||
.message("COPY completed")
|
||||
.code(Code::Other(CodeOther::unvalidated(
|
||||
format!("COPYUID {}", copyuid_str).into_bytes(),
|
||||
)))
|
||||
.ok()?,
|
||||
flow::Transition::None,
|
||||
))
|
||||
}
|
||||
|
|
|
@ -37,23 +37,27 @@ pub enum Transition {
|
|||
// See RFC3501 section 3.
|
||||
// https://datatracker.ietf.org/doc/html/rfc3501#page-13
|
||||
impl State {
|
||||
pub fn apply(self, tr: Transition) -> Result<Self, Error> {
|
||||
match (self, tr) {
|
||||
(s, Transition::None) => Ok(s),
|
||||
(State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)),
|
||||
pub fn apply(&mut self, tr: Transition) -> Result<(), Error> {
|
||||
let new_state = match (&self, tr) {
|
||||
(_s, Transition::None) => return Ok(()),
|
||||
(State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u),
|
||||
(
|
||||
State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _),
|
||||
Transition::Select(m),
|
||||
) => Ok(State::Selected(u, m)),
|
||||
) => State::Selected(u.clone(), m),
|
||||
(
|
||||
State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _),
|
||||
Transition::Examine(m),
|
||||
) => Ok(State::Examined(u, m)),
|
||||
) => State::Examined(u.clone(), m),
|
||||
(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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,22 +4,20 @@ use std::num::NonZeroU32;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Error, Result};
|
||||
use boitalettres::proto::res::body::Data as Body;
|
||||
use chrono::{Offset, TimeZone, Utc};
|
||||
|
||||
use futures::stream::{FuturesOrdered, StreamExt};
|
||||
|
||||
use imap_codec::types::address::Address;
|
||||
use imap_codec::types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields};
|
||||
use imap_codec::types::core::{AString, Atom, IString, NString};
|
||||
use imap_codec::types::datetime::MyDateTime;
|
||||
use imap_codec::types::envelope::Envelope;
|
||||
use imap_codec::types::fetch_attributes::{
|
||||
FetchAttribute, MacroOrFetchAttributes, Section as FetchSection,
|
||||
use imap_codec::imap_types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields};
|
||||
use imap_codec::imap_types::core::{AString, Atom, IString, NString, NonEmptyVec};
|
||||
use imap_codec::imap_types::datetime::DateTime;
|
||||
use imap_codec::imap_types::envelope::{Address, Envelope};
|
||||
use imap_codec::imap_types::fetch::{
|
||||
MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName, Section as FetchSection,
|
||||
};
|
||||
use imap_codec::types::flag::{Flag, StoreResponse, StoreType};
|
||||
use imap_codec::types::response::{Code, Data, MessageAttribute, Status};
|
||||
use imap_codec::types::sequence::{self, SequenceSet};
|
||||
use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType};
|
||||
use imap_codec::imap_types::response::{Code, Data, Status};
|
||||
use imap_codec::imap_types::sequence::{self, SequenceSet};
|
||||
|
||||
use eml_codec::{
|
||||
header, imf, mime,
|
||||
|
@ -28,6 +26,7 @@ use eml_codec::{
|
|||
};
|
||||
|
||||
use crate::cryptoblob::Key;
|
||||
use crate::imap::response::Body;
|
||||
use crate::mail::mailbox::{MailMeta, Mailbox};
|
||||
use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex};
|
||||
use crate::mail::unique_ident::UniqueIdent;
|
||||
|
@ -77,19 +76,31 @@ impl<'a> FetchedMail<'a> {
|
|||
}
|
||||
|
||||
pub struct AttributesProxy {
|
||||
attrs: Vec<FetchAttribute>,
|
||||
attrs: Vec<MessageDataItemName<'static>>,
|
||||
}
|
||||
impl AttributesProxy {
|
||||
fn new(attrs: &MacroOrFetchAttributes, is_uid_fetch: bool) -> Self {
|
||||
fn new(attrs: &MacroOrMessageDataItemNames<'static>, is_uid_fetch: bool) -> Self {
|
||||
// Expand macros
|
||||
let mut fetch_attrs = match attrs {
|
||||
MacroOrFetchAttributes::Macro(m) => m.expand(),
|
||||
MacroOrFetchAttributes::FetchAttributes(a) => a.clone(),
|
||||
MacroOrMessageDataItemNames::Macro(m) => {
|
||||
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
|
||||
if is_uid_fetch && !fetch_attrs.contains(&FetchAttribute::Uid) {
|
||||
fetch_attrs.push(FetchAttribute::Uid);
|
||||
if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) {
|
||||
fetch_attrs.push(MessageDataItemName::Uid);
|
||||
}
|
||||
|
||||
Self { attrs: fetch_attrs }
|
||||
|
@ -99,11 +110,11 @@ impl AttributesProxy {
|
|||
self.attrs.iter().any(|x| {
|
||||
matches!(
|
||||
x,
|
||||
FetchAttribute::Body
|
||||
| FetchAttribute::BodyExt { .. }
|
||||
| FetchAttribute::Rfc822
|
||||
| FetchAttribute::Rfc822Text
|
||||
| FetchAttribute::BodyStructure
|
||||
MessageDataItemName::Body
|
||||
| MessageDataItemName::BodyExt { .. }
|
||||
| MessageDataItemName::Rfc822
|
||||
| MessageDataItemName::Rfc822Text
|
||||
| MessageDataItemName::BodyStructure
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -127,16 +138,20 @@ pub struct MailView<'a> {
|
|||
meta: &'a MailMeta,
|
||||
flags: &'a Vec<String>,
|
||||
content: FetchedMail<'a>,
|
||||
add_seen: bool,
|
||||
}
|
||||
|
||||
enum SeenFlag {
|
||||
DoNothing,
|
||||
MustAdd,
|
||||
}
|
||||
|
||||
impl<'a> MailView<'a> {
|
||||
fn uid(&self) -> MessageAttribute {
|
||||
MessageAttribute::Uid(self.ids.uid)
|
||||
fn uid(&self) -> MessageDataItem<'static> {
|
||||
MessageDataItem::Uid(self.ids.uid.clone())
|
||||
}
|
||||
|
||||
fn flags(&self) -> MessageAttribute {
|
||||
MessageAttribute::Flags(
|
||||
fn flags(&self) -> MessageDataItem<'static> {
|
||||
MessageDataItem::Flags(
|
||||
self.flags
|
||||
.iter()
|
||||
.filter_map(|f| string_to_flag(f))
|
||||
|
@ -144,12 +159,12 @@ impl<'a> MailView<'a> {
|
|||
)
|
||||
}
|
||||
|
||||
fn rfc_822_size(&self) -> MessageAttribute {
|
||||
MessageAttribute::Rfc822Size(self.meta.rfc822_size as u32)
|
||||
fn rfc_822_size(&self) -> MessageDataItem<'static> {
|
||||
MessageDataItem::Rfc822Size(self.meta.rfc822_size as u32)
|
||||
}
|
||||
|
||||
fn rfc_822_header(&self) -> MessageAttribute {
|
||||
MessageAttribute::Rfc822Header(NString(
|
||||
fn rfc_822_header(&self) -> MessageDataItem<'static> {
|
||||
MessageDataItem::Rfc822Header(NString(
|
||||
self.meta
|
||||
.headers
|
||||
.to_vec()
|
||||
|
@ -159,41 +174,42 @@ impl<'a> MailView<'a> {
|
|||
))
|
||||
}
|
||||
|
||||
fn rfc_822_text(&self) -> Result<MessageAttribute> {
|
||||
Ok(MessageAttribute::Rfc822Text(NString(
|
||||
fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> {
|
||||
Ok(MessageDataItem::Rfc822Text(NString(
|
||||
self.content
|
||||
.as_full()?
|
||||
.raw_body
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(IString::Literal),
|
||||
)))
|
||||
}
|
||||
|
||||
fn rfc822(&self) -> Result<MessageAttribute> {
|
||||
Ok(MessageAttribute::Rfc822(NString(
|
||||
fn rfc822(&self) -> Result<MessageDataItem<'static>> {
|
||||
Ok(MessageDataItem::Rfc822(NString(
|
||||
self.content
|
||||
.as_full()?
|
||||
.raw_body
|
||||
.clone()
|
||||
.raw_part
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(IString::Literal),
|
||||
)))
|
||||
}
|
||||
|
||||
fn envelope(&self) -> MessageAttribute {
|
||||
MessageAttribute::Envelope(message_envelope(self.content.imf()))
|
||||
fn envelope(&self) -> MessageDataItem<'static> {
|
||||
MessageDataItem::Envelope(message_envelope(self.content.imf().clone()))
|
||||
}
|
||||
|
||||
fn body(&self) -> Result<MessageAttribute> {
|
||||
Ok(MessageAttribute::Body(build_imap_email_struct(
|
||||
fn body(&self) -> Result<MessageDataItem<'static>> {
|
||||
Ok(MessageDataItem::Body(build_imap_email_struct(
|
||||
self.content.as_full()?.child.as_ref(),
|
||||
)?))
|
||||
}
|
||||
|
||||
fn body_structure(&self) -> Result<MessageAttribute> {
|
||||
Ok(MessageAttribute::Body(build_imap_email_struct(
|
||||
fn body_structure(&self) -> Result<MessageDataItem<'static>> {
|
||||
Ok(MessageDataItem::Body(build_imap_email_struct(
|
||||
self.content.as_full()?.child.as_ref(),
|
||||
)?))
|
||||
}
|
||||
|
@ -202,12 +218,14 @@ impl<'a> MailView<'a> {
|
|||
/// peek does not implicitly set the \Seen flag
|
||||
/// eg. BODY[HEADER.FIELDS (DATE FROM)]
|
||||
/// eg. BODY[]<0.2048>
|
||||
fn body_ext(
|
||||
&mut self,
|
||||
section: &Option<FetchSection>,
|
||||
fn body_ext<'b>(
|
||||
&self,
|
||||
section: &Option<FetchSection<'b>>,
|
||||
partial: &Option<(u32, NonZeroU32)>,
|
||||
peek: &bool,
|
||||
) -> Result<MessageAttribute> {
|
||||
) -> Result<(MessageDataItem<'b>, SeenFlag)> {
|
||||
let mut seen = SeenFlag::DoNothing;
|
||||
|
||||
// Extract message 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) {
|
||||
// Add \Seen flag
|
||||
//self.mailbox.add_flags(uuid, &[seen_flag]).await?;
|
||||
self.add_seen = true;
|
||||
seen = SeenFlag::MustAdd;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
return Ok(MessageAttribute::BodyExt {
|
||||
section: section.clone(),
|
||||
origin,
|
||||
data,
|
||||
});
|
||||
return Ok((
|
||||
MessageDataItem::BodyExt {
|
||||
section: section.as_ref().map(|fs| fs.clone()),
|
||||
origin,
|
||||
data,
|
||||
},
|
||||
seen,
|
||||
));
|
||||
}
|
||||
|
||||
fn internal_date(&self) -> Result<MessageAttribute> {
|
||||
fn internal_date(&self) -> Result<MessageDataItem<'static>> {
|
||||
let dt = Utc
|
||||
.fix()
|
||||
.timestamp_opt(i64::try_from(self.meta.internaldate / 1000)?, 0)
|
||||
.earliest()
|
||||
.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
|
||||
.attrs
|
||||
.iter()
|
||||
.map(|attr| match attr {
|
||||
FetchAttribute::Uid => Ok(self.uid()),
|
||||
FetchAttribute::Flags => Ok(self.flags()),
|
||||
FetchAttribute::Rfc822Size => Ok(self.rfc_822_size()),
|
||||
FetchAttribute::Rfc822Header => Ok(self.rfc_822_header()),
|
||||
FetchAttribute::Rfc822Text => self.rfc_822_text(),
|
||||
FetchAttribute::Rfc822 => self.rfc822(),
|
||||
FetchAttribute::Envelope => Ok(self.envelope()),
|
||||
FetchAttribute::Body => self.body(),
|
||||
FetchAttribute::BodyStructure => self.body_structure(),
|
||||
FetchAttribute::BodyExt {
|
||||
MessageDataItemName::Uid => Ok(self.uid()),
|
||||
MessageDataItemName::Flags => Ok(self.flags()),
|
||||
MessageDataItemName::Rfc822Size => Ok(self.rfc_822_size()),
|
||||
MessageDataItemName::Rfc822Header => Ok(self.rfc_822_header()),
|
||||
MessageDataItemName::Rfc822Text => self.rfc_822_text(),
|
||||
MessageDataItemName::Rfc822 => self.rfc822(),
|
||||
MessageDataItemName::Envelope => Ok(self.envelope()),
|
||||
MessageDataItemName::Body => self.body(),
|
||||
MessageDataItemName::BodyStructure => self.body_structure(),
|
||||
MessageDataItemName::BodyExt {
|
||||
section,
|
||||
partial,
|
||||
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<_>, _>>()?;
|
||||
|
||||
Ok(Body::Data(Data::Fetch {
|
||||
seq_or_uid: self.ids.i,
|
||||
attributes: res_attrs,
|
||||
}))
|
||||
Ok((
|
||||
Body::Data(Data::Fetch {
|
||||
seq: self.ids.i,
|
||||
items: res_attrs.try_into()?,
|
||||
}),
|
||||
seen,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -376,7 +405,6 @@ impl<'a> MailSelectionBuilder<'a> {
|
|||
meta,
|
||||
flags,
|
||||
content,
|
||||
add_seen: false,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
@ -396,35 +424,26 @@ pub struct MailboxView {
|
|||
|
||||
impl MailboxView {
|
||||
/// Creates a new IMAP view into a mailbox.
|
||||
/// 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 async fn new(mailbox: Arc<Mailbox>) -> Result<(Self, Vec<Body>)> {
|
||||
pub async fn new(mailbox: Arc<Mailbox>) -> Self {
|
||||
let state = mailbox.current_uid_index().await;
|
||||
|
||||
let new_view = Self {
|
||||
Self {
|
||||
mailbox,
|
||||
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
|
||||
/// what the client knows and what is actually in the mailbox.
|
||||
/// This does NOT trigger a sync, it bases itself on what is currently
|
||||
/// loaded in RAM by Bayou.
|
||||
pub async fn update(&mut self) -> Result<Vec<Body>> {
|
||||
let new_view = MailboxView {
|
||||
mailbox: self.mailbox.clone(),
|
||||
known_state: self.mailbox.current_uid_index().await,
|
||||
pub async fn update(&mut self) -> Result<Vec<Body<'static>>> {
|
||||
let old_view: &mut Self = self;
|
||||
let new_view = Self {
|
||||
mailbox: old_view.mailbox.clone(),
|
||||
known_state: old_view.mailbox.current_uid_index().await,
|
||||
};
|
||||
|
||||
let mut data = Vec::<Body>::new();
|
||||
|
@ -446,7 +465,7 @@ impl MailboxView {
|
|||
|
||||
// - notify client of expunged mails
|
||||
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) {
|
||||
data.push(Body::Data(Data::Expunge(
|
||||
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_view.known_state.table.len() != self.known_state.table.len() - n_expunge
|
||||
|| new_view.known_state.uidvalidity != self.known_state.uidvalidity
|
||||
if new_view.known_state.table.len() != old_view.known_state.table.len() - n_expunge
|
||||
|| new_view.known_state.uidvalidity != old_view.known_state.uidvalidity
|
||||
{
|
||||
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?
|
||||
data.push(new_view.uidvalidity_status()?);
|
||||
data.push(new_view.uidnext_status()?);
|
||||
} else {
|
||||
// - if flags changed for existing mails, tell client
|
||||
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);
|
||||
if old_mail.is_some() && old_mail != new_mail {
|
||||
if let Some((uid, flags)) = new_mail {
|
||||
data.push(Body::Data(Data::Fetch {
|
||||
seq_or_uid: NonZeroU32::try_from((i + 1) as u32).unwrap(),
|
||||
attributes: vec![
|
||||
MessageAttribute::Uid(*uid),
|
||||
MessageAttribute::Flags(
|
||||
seq: NonZeroU32::try_from((i + 1) as u32).unwrap(),
|
||||
items: vec![
|
||||
MessageDataItem::Uid(*uid),
|
||||
MessageDataItem::Flags(
|
||||
flags.iter().filter_map(|f| string_to_flag(f)).collect(),
|
||||
),
|
||||
],
|
||||
]
|
||||
.try_into()?,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*self = new_view;
|
||||
*old_view = new_view;
|
||||
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,
|
||||
sequence_set: &SequenceSet,
|
||||
kind: &StoreType,
|
||||
_response: &StoreResponse,
|
||||
flags: &[Flag],
|
||||
flags: &[Flag<'a>],
|
||||
is_uid_store: &bool,
|
||||
) -> Result<Vec<Body>> {
|
||||
) -> Result<Vec<Body<'static>>> {
|
||||
self.mailbox.opportunistic_sync().await?;
|
||||
|
||||
let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>();
|
||||
|
@ -522,7 +555,7 @@ impl MailboxView {
|
|||
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?;
|
||||
|
||||
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
|
||||
/// responses describing the new state.
|
||||
pub async fn fetch(
|
||||
pub async fn fetch<'b>(
|
||||
&self,
|
||||
sequence_set: &SequenceSet,
|
||||
attributes: &MacroOrFetchAttributes,
|
||||
attributes: &'b MacroOrMessageDataItemNames<'static>,
|
||||
is_uid_fetch: &bool,
|
||||
) -> Result<Vec<Body>> {
|
||||
) -> Result<Vec<Body<'static>>> {
|
||||
let ap = AttributesProxy::new(attributes, *is_uid_fetch);
|
||||
|
||||
// Prepare data
|
||||
|
@ -619,31 +652,37 @@ impl MailboxView {
|
|||
selection.with_bodies(bodies.as_slice());
|
||||
|
||||
// Build mail selection views
|
||||
let mut views = selection.build()?;
|
||||
let views = selection.build()?;
|
||||
|
||||
// Filter views to build the result
|
||||
let ret = views
|
||||
.iter_mut()
|
||||
.filter_map(|mv| mv.filter(&ap).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Register seen flags
|
||||
let future_flags = views
|
||||
// Also identify what must be put as seen
|
||||
let filtered_view = views
|
||||
.iter()
|
||||
.filter(|mv| mv.add_seen)
|
||||
.map(|mv| async move {
|
||||
.filter_map(|mv| mv.filter(&ap).ok().map(|(body, seen)| (mv, body, seen)))
|
||||
.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();
|
||||
self.mailbox.add_flags(mv.ids.uuid, &[seen_flag]).await?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
future_flags
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.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`
|
||||
fn uidvalidity_status(&self) -> Result<Body> {
|
||||
fn uidvalidity_status(&self) -> Result<Body<'static>> {
|
||||
let uid_validity = Status::ok(
|
||||
None,
|
||||
Some(Code::UidValidity(self.uidvalidity())),
|
||||
|
@ -732,7 +771,7 @@ impl MailboxView {
|
|||
}
|
||||
|
||||
/// 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(
|
||||
None,
|
||||
Some(Code::UidNext(self.uidnext())),
|
||||
|
@ -748,7 +787,7 @@ impl MailboxView {
|
|||
|
||||
/// Produce an EXISTS message corresponding to the number of mails
|
||||
/// in `known_state`
|
||||
fn exists_status(&self) -> Result<Body> {
|
||||
fn exists_status(&self) -> Result<Body<'static>> {
|
||||
Ok(Body::Data(Data::Exists(self.exists()?)))
|
||||
}
|
||||
|
||||
|
@ -758,7 +797,7 @@ impl MailboxView {
|
|||
|
||||
/// Produce a RECENT message corresponding to the number of
|
||||
/// 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()?)))
|
||||
}
|
||||
|
||||
|
@ -774,27 +813,48 @@ impl MailboxView {
|
|||
|
||||
/// Produce a FLAGS and a PERMANENTFLAGS message that indicates
|
||||
/// the flags that are in `known_state` + default flags
|
||||
fn flags_status(&self) -> Result<Vec<Body>> {
|
||||
let mut flags: Vec<Flag> = self
|
||||
fn flags_status(&self) -> Result<Vec<Body<'static>>> {
|
||||
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
|
||||
.idx_by_flag
|
||||
.flags()
|
||||
.filter_map(|f| string_to_flag(f))
|
||||
.filter_map(|f| match string_to_flag(f) {
|
||||
Some(FlagFetch::Flag(fl)) => Some(fl),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// 1.b Merge it with our default flags list
|
||||
for f in DEFAULT_FLAGS.iter() {
|
||||
if !flags.contains(f) {
|
||||
flags.push(f.clone());
|
||||
if !known_flags.contains(f) {
|
||||
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);
|
||||
let permanent_flags =
|
||||
Status::ok(None, Some(Code::PermanentFlags(flags)), "Flags permitted")
|
||||
.map_err(Error::msg)?;
|
||||
ret.push(Body::Status(permanent_flags));
|
||||
// 2. Returning flags that are persisted
|
||||
// 2.a Always advertise our default flags
|
||||
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)?;
|
||||
body.push(Body::Status(permanent_flags));
|
||||
|
||||
Ok(ret)
|
||||
// Done!
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
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() {
|
||||
Some('\\') => match f {
|
||||
"\\Seen" => Some(Flag::Seen),
|
||||
"\\Answered" => Some(Flag::Answered),
|
||||
"\\Flagged" => Some(Flag::Flagged),
|
||||
"\\Deleted" => Some(Flag::Deleted),
|
||||
"\\Draft" => Some(Flag::Draft),
|
||||
"\\Recent" => Some(Flag::Recent),
|
||||
"\\Seen" => Some(FlagFetch::Flag(Flag::Seen)),
|
||||
"\\Answered" => Some(FlagFetch::Flag(Flag::Answered)),
|
||||
"\\Flagged" => Some(FlagFetch::Flag(Flag::Flagged)),
|
||||
"\\Deleted" => Some(FlagFetch::Flag(Flag::Deleted)),
|
||||
"\\Draft" => Some(FlagFetch::Flag(Flag::Draft)),
|
||||
"\\Recent" => Some(FlagFetch::Recent),
|
||||
_ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) {
|
||||
Err(_) => {
|
||||
tracing::error!(flag=%f, "Unable to encode flag as IMAP atom");
|
||||
None
|
||||
}
|
||||
Ok(a) => Some(Flag::Extension(a)),
|
||||
Ok(a) => Some(FlagFetch::Flag(Flag::system(a))),
|
||||
},
|
||||
},
|
||||
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");
|
||||
None
|
||||
}
|
||||
Ok(a) => Some(Flag::Keyword(a)),
|
||||
Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))),
|
||||
},
|
||||
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 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<_>>();
|
||||
|
||||
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![];
|
||||
for item in addrlist {
|
||||
match item {
|
||||
|
@ -911,23 +971,23 @@ fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address> {
|
|||
return acc;
|
||||
}
|
||||
|
||||
fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address {
|
||||
Address::new(
|
||||
NString(
|
||||
fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address<'static> {
|
||||
Address {
|
||||
name: NString(
|
||||
addr.name
|
||||
.as_ref()
|
||||
.map(|x| IString::try_from(x.to_string()).unwrap()),
|
||||
),
|
||||
// SMTP at-domain-list (source route) seems obsolete since at least 1991
|
||||
// https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html
|
||||
NString(None),
|
||||
NString(Some(
|
||||
adl: NString(None),
|
||||
mailbox: NString(Some(
|
||||
IString::try_from(addr.addrspec.local_part.to_string()).unwrap(),
|
||||
)),
|
||||
NString(Some(
|
||||
host: NString(Some(
|
||||
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).
|
||||
*/
|
||||
|
||||
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 {
|
||||
AnyPart::Mult(x) => {
|
||||
let itype = &x.mime.interpreted_type;
|
||||
let subtype = IString::try_from(itype.subtype.to_string())
|
||||
.unwrap_or(unchecked_istring("alternative"));
|
||||
|
||||
let inner_bodies = x
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|inner| build_imap_email_struct(&inner).ok())
|
||||
.collect::<Vec<_>>();
|
||||
NonEmptyVec::validate(&inner_bodies)?;
|
||||
let bodies = NonEmptyVec::unvalidated(inner_bodies);
|
||||
|
||||
Ok(BodyStructure::Multi {
|
||||
bodies: x
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|inner| build_imap_email_struct(&inner).ok())
|
||||
.collect(),
|
||||
bodies,
|
||||
subtype,
|
||||
extension_data: None,
|
||||
/*Some(MultipartExtensionData {
|
||||
|
@ -996,7 +1060,7 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
|
|||
number_of_lines: nol(x.body),
|
||||
},
|
||||
},
|
||||
extension: None,
|
||||
extension_data: None,
|
||||
})
|
||||
}
|
||||
AnyPart::Bin(x) => {
|
||||
|
@ -1009,9 +1073,10 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
|
|||
};
|
||||
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(
|
||||
anyhow!("Unable to build IString from given Content-Type type given"),
|
||||
))?;
|
||||
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"),
|
||||
))?;
|
||||
|
||||
let subtype =
|
||||
IString::try_from(String::from_utf8_lossy(ct.sub).to_string()).or(Err(anyhow!(
|
||||
|
@ -1021,9 +1086,9 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
|
|||
Ok(BodyStructure::Single {
|
||||
body: FetchBody {
|
||||
basic,
|
||||
specific: SpecificFields::Basic { type_, subtype },
|
||||
specific: SpecificFields::Basic { r#type, subtype },
|
||||
},
|
||||
extension: None,
|
||||
extension_data: None,
|
||||
})
|
||||
}
|
||||
AnyPart::Msg(x) => {
|
||||
|
@ -1033,12 +1098,12 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
|
|||
body: FetchBody {
|
||||
basic,
|
||||
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())?),
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
.ctype
|
||||
.as_ref()
|
||||
|
@ -1136,20 +1201,18 @@ fn get_message_section<'a>(
|
|||
.ok_or(anyhow!("Part must be a message"))?;
|
||||
match section {
|
||||
Some(FetchSection::Text(None)) => Ok(msg.raw_body.into()),
|
||||
Some(FetchSection::Text(Some(part))) => {
|
||||
map_subpart(parsed, part.0.as_slice(), |part_msg| {
|
||||
Ok(part_msg
|
||||
.as_message()
|
||||
.ok_or(Error::msg(
|
||||
"Not a message/rfc822 part while expected by request (TEXT)",
|
||||
))?
|
||||
.raw_body
|
||||
.into())
|
||||
})
|
||||
}
|
||||
Some(FetchSection::Text(Some(part))) => map_subpart(parsed, part.0.as_ref(), |part_msg| {
|
||||
Ok(part_msg
|
||||
.as_message()
|
||||
.ok_or(Error::msg(
|
||||
"Not a message/rfc822 part while expected by request (TEXT)",
|
||||
))?
|
||||
.raw_body
|
||||
.into())
|
||||
}),
|
||||
Some(FetchSection::Header(part)) => map_subpart(
|
||||
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| {
|
||||
Ok(part_msg
|
||||
.as_message()
|
||||
|
@ -1165,17 +1228,18 @@ fn get_message_section<'a>(
|
|||
) => {
|
||||
let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _)));
|
||||
let fields = fields
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|x| match x {
|
||||
AString::Atom(a) => a.as_bytes(),
|
||||
AString::String(IString::Literal(l)) => l.as_slice(),
|
||||
AString::String(IString::Quoted(q)) => q.as_bytes(),
|
||||
AString::Atom(a) => a.inner().as_bytes(),
|
||||
AString::String(IString::Literal(l)) => l.as_ref(),
|
||||
AString::String(IString::Quoted(q)) => q.inner().as_bytes(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
map_subpart(
|
||||
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| {
|
||||
let mut ret = vec![];
|
||||
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 {
|
||||
AnyPart::Txt(p) => p.body,
|
||||
AnyPart::Bin(p) => p.body,
|
||||
|
@ -1204,7 +1268,7 @@ fn get_message_section<'a>(
|
|||
};
|
||||
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 {
|
||||
AnyPart::Txt(p) => p.mime.fields.raw,
|
||||
AnyPart::Bin(p) => p.mime.fields.raw,
|
||||
|
@ -1245,18 +1309,22 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::cryptoblob;
|
||||
use crate::mail::unique_ident;
|
||||
use imap_codec::codec::Encode;
|
||||
use imap_codec::types::fetch_attributes::Section;
|
||||
use imap_codec::encode::Encoder;
|
||||
use imap_codec::imap_types::fetch::Section;
|
||||
use imap_codec::imap_types::response::Response;
|
||||
use imap_codec::ResponseCodec;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn mailview_body_ext() -> Result<()> {
|
||||
let ap = AttributesProxy::new(
|
||||
&MacroOrFetchAttributes::FetchAttributes(vec![FetchAttribute::BodyExt {
|
||||
section: Some(Section::Header(None)),
|
||||
partial: None,
|
||||
peek: false,
|
||||
}]),
|
||||
&MacroOrMessageDataItemNames::MessageDataItemNames(vec![
|
||||
MessageDataItemName::BodyExt {
|
||||
section: Some(Section::Header(None)),
|
||||
partial: None,
|
||||
peek: 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 content = FetchedMail::new_from_message(eml_codec::parse_message(rfc822)?.1);
|
||||
|
||||
let mut mv = MailView {
|
||||
let mv = MailView {
|
||||
ids: &ids,
|
||||
content,
|
||||
meta: &meta,
|
||||
flags: &flags,
|
||||
add_seen: false,
|
||||
};
|
||||
let res_body = mv.filter(&ap)?;
|
||||
let (res_body, _seen) = mv.filter(&ap)?;
|
||||
|
||||
let fattr = match res_body {
|
||||
Body::Data(Data::Fetch {
|
||||
seq_or_uid: _seq,
|
||||
attributes: attr,
|
||||
seq: _seq,
|
||||
items: attr,
|
||||
}) => Ok(attr),
|
||||
_ => 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] {
|
||||
MessageAttribute::BodyExt {
|
||||
let (sec, _orig, _data) = match &fattr.as_ref()[0] {
|
||||
MessageDataItem::BodyExt {
|
||||
section,
|
||||
origin,
|
||||
data,
|
||||
|
@ -1345,22 +1412,24 @@ mod tests {
|
|||
for pref in prefixes.iter() {
|
||||
println!("{}", 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 mut resp = Vec::new();
|
||||
MessageAttribute::Body(build_imap_email_struct(&message.child)?)
|
||||
.encode(&mut resp)
|
||||
.unwrap();
|
||||
let test_repr = Response::Data(Data::Fetch {
|
||||
seq: NonZeroU32::new(1).unwrap(),
|
||||
items: NonEmptyVec::from(MessageDataItem::Body(build_imap_email_struct(
|
||||
&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];
|
||||
let exp_str = String::from_utf8_lossy(exp_no_parenthesis).to_lowercase();
|
||||
|
||||
println!("aerogramme: {}\n\ndovecot: {}\n\n", resp_str, exp_str);
|
||||
println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str);
|
||||
//println!("\n\n {} \n\n", String::from_utf8_lossy(&resp));
|
||||
assert_eq!(resp_str, exp_str);
|
||||
assert_eq!(test_str, oracle_str);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
227
src/imap/mod.rs
227
src/imap/mod.rs
|
@ -1,105 +1,186 @@
|
|||
mod command;
|
||||
mod flow;
|
||||
mod mailbox_view;
|
||||
mod response;
|
||||
mod session;
|
||||
|
||||
use std::task::{Context, Poll};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Result;
|
||||
use boitalettres::errors::Error as BalError;
|
||||
use boitalettres::proto::{Request, Response};
|
||||
use boitalettres::server::accept::addr::AddrIncoming;
|
||||
use boitalettres::server::accept::addr::AddrStream;
|
||||
use boitalettres::server::Server as ImapServer;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::future::FutureExt;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
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::login::ArcLoginProvider;
|
||||
|
||||
/// 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> {
|
||||
//@FIXME add a configuration parameter
|
||||
let incoming = AddrIncoming::new(config.bind_addr).await?;
|
||||
tracing::info!("IMAP activated, will listen on {:#}", incoming.local_addr);
|
||||
struct ClientContext {
|
||||
stream: AnyStream,
|
||||
addr: SocketAddr,
|
||||
login_provider: ArcLoginProvider,
|
||||
must_exit: watch::Receiver<bool>,
|
||||
}
|
||||
|
||||
let imap = ImapServer::new(incoming).serve(Instance::new(login.clone()));
|
||||
Ok(Server(imap))
|
||||
pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server {
|
||||
Server {
|
||||
bind_addr: config.bind_addr,
|
||||
login_provider: login,
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn run(self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
||||
tracing::info!("IMAP started!");
|
||||
tokio::select! {
|
||||
s = self.0 => s?,
|
||||
_ = must_exit.changed() => tracing::info!("Stopped IMAP server"),
|
||||
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
||||
let tcp = TcpListener::bind(self.bind_addr).await?;
|
||||
tracing::info!("IMAP server listening on {:#}", self.bind_addr);
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
//---
|
||||
|
||||
/// Instance is the main Tokio Tower service that we register in BàL.
|
||||
/// It receives new connection demands and spawn a dedicated service.
|
||||
struct Instance {
|
||||
login_provider: ArcLoginProvider,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
pub fn new(login_provider: ArcLoginProvider) -> Self {
|
||||
Self { login_provider }
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
async fn client_wrapper(ctx: ClientContext) {
|
||||
let addr = ctx.addr.clone();
|
||||
match client(ctx).await {
|
||||
Ok(()) => {
|
||||
tracing::info!("closing successful session for {:?}", addr);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("closing errored session for {:?}: {}", addr, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Service<Request> for Connection {
|
||||
type Response = Response;
|
||||
type Error = BalError;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
async fn client(mut ctx: ClientContext) -> Result<()> {
|
||||
// Send greeting
|
||||
let (mut server, _) = ServerFlow::send_greeting(
|
||||
ctx.stream,
|
||||
ServerFlowOptions::default(),
|
||||
Greeting::ok(None, "Aerogramme").unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
use crate::imap::response::{Body, Response as MyResponse};
|
||||
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 {
|
||||
tracing::debug!("Got request: {:#?}", req.command);
|
||||
self.session.process(req)
|
||||
}
|
||||
drop(cmd_tx);
|
||||
bckgrnd.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
112
src/imap/response.rs
Normal file
112
src/imap/response.rs
Normal 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![],
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,180 +1,86 @@
|
|||
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::flow;
|
||||
use crate::imap::response::Response;
|
||||
use crate::login::ArcLoginProvider;
|
||||
|
||||
/* 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>>,
|
||||
}
|
||||
use imap_codec::imap_types::command::Command;
|
||||
|
||||
//-----
|
||||
|
||||
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 {
|
||||
rx: mpsc::Receiver<Message>,
|
||||
|
||||
pub login_provider: ArcLoginProvider,
|
||||
pub state: flow::State,
|
||||
}
|
||||
impl Instance {
|
||||
fn new(login_provider: ArcLoginProvider, rx: mpsc::Receiver<Message>) -> Self {
|
||||
pub fn new(login_provider: ArcLoginProvider) -> Self {
|
||||
Self {
|
||||
login_provider,
|
||||
rx,
|
||||
state: flow::State::NotAuthenticated,
|
||||
}
|
||||
}
|
||||
|
||||
//@FIXME add a function that compute the runner's name from its local info
|
||||
// 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.
|
||||
// To prevent state error, we handle the same command in separate code paths.
|
||||
let ctrl = match &mut self.state {
|
||||
flow::State::NotAuthenticated => {
|
||||
let ctx = anonymous::AnonymousContext {
|
||||
req: &msg.req,
|
||||
login_provider: Some(&self.login_provider),
|
||||
};
|
||||
anonymous::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Authenticated(ref user) => {
|
||||
let ctx = authenticated::AuthenticatedContext {
|
||||
req: &msg.req,
|
||||
user,
|
||||
};
|
||||
authenticated::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Selected(ref user, ref mut mailbox) => {
|
||||
let ctx = selected::SelectedContext {
|
||||
req: &msg.req,
|
||||
user,
|
||||
mailbox,
|
||||
};
|
||||
selected::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Examined(ref user, ref mut mailbox) => {
|
||||
let ctx = examined::ExaminedContext {
|
||||
req: &msg.req,
|
||||
user,
|
||||
mailbox,
|
||||
};
|
||||
examined::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Logout => {
|
||||
Response::bad("No commands are allowed in the LOGOUT state.")
|
||||
.map(|r| (r, flow::Transition::None))
|
||||
.map_err(Error::msg)
|
||||
}
|
||||
};
|
||||
|
||||
// Process result
|
||||
let res = match ctrl {
|
||||
Ok((res, tr)) => {
|
||||
//@FIXME remove unwrap
|
||||
self.state = match self.state.apply(tr) {
|
||||
Ok(new_state) => new_state,
|
||||
Err(e) => {
|
||||
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 {
|
||||
break;
|
||||
pub async fn command(&mut self, cmd: Command<'static>) -> Response<'static> {
|
||||
// Command behavior is modulated by the state.
|
||||
// To prevent state error, we handle the same command in separate code paths.
|
||||
let (resp, tr) = match &mut self.state {
|
||||
flow::State::NotAuthenticated => {
|
||||
let ctx = anonymous::AnonymousContext {
|
||||
req: &cmd,
|
||||
login_provider: &self.login_provider,
|
||||
};
|
||||
anonymous::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Authenticated(ref user) => {
|
||||
let ctx = authenticated::AuthenticatedContext { req: &cmd, user };
|
||||
authenticated::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Selected(ref user, ref mut mailbox) => {
|
||||
let ctx = selected::SelectedContext {
|
||||
req: &cmd,
|
||||
user,
|
||||
mailbox,
|
||||
};
|
||||
selected::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Examined(ref user, ref mut mailbox) => {
|
||||
let ctx = examined::ExaminedContext {
|
||||
req: &cmd,
|
||||
user,
|
||||
mailbox,
|
||||
};
|
||||
examined::dispatch(ctx).await
|
||||
}
|
||||
flow::State::Logout => Response::build()
|
||||
.tag(cmd.tag.clone())
|
||||
.message("No commands are allowed in the LOGOUT state.")
|
||||
.bad()
|
||||
.map(|r| (r, flow::Transition::None)),
|
||||
}
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::error!("Command error {:?} occured while processing {:?}", err, cmd);
|
||||
(
|
||||
Response::build()
|
||||
.to_req(&cmd)
|
||||
.message("Internal error while processing command")
|
||||
.bad()
|
||||
.unwrap(),
|
||||
flow::Transition::None,
|
||||
)
|
||||
});
|
||||
|
||||
if let Err(e) = self.state.apply(tr) {
|
||||
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
|
||||
tracing::debug!("exiting runner");
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
|
51
src/login/demo_provider.rs
Normal file
51
src/login/demo_provider.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod demo_provider;
|
||||
pub mod ldap_provider;
|
||||
pub mod static_provider;
|
||||
|
||||
|
|
26
src/main.rs
26
src/main.rs
|
@ -29,7 +29,12 @@ struct Args {
|
|||
#[clap(subcommand)]
|
||||
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")]
|
||||
/// Path to the main Aerogramme configuration file
|
||||
config_file: PathBuf,
|
||||
}
|
||||
|
||||
|
@ -158,7 +163,22 @@ async fn main() -> Result<()> {
|
|||
tracing_subscriber::fmt::init();
|
||||
|
||||
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) {
|
||||
(Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand {
|
||||
|
@ -184,8 +204,8 @@ async fn main() -> Result<()> {
|
|||
ProviderCommand::Account(cmd) => {
|
||||
let user_file = match config.users {
|
||||
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)?;
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::config::*;
|
|||
use crate::imap;
|
||||
use crate::lmtp::*;
|
||||
use crate::login::ArcLoginProvider;
|
||||
use crate::login::{ldap_provider::*, static_provider::*};
|
||||
use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*};
|
||||
|
||||
pub struct Server {
|
||||
lmtp_server: Option<Arc<LmtpServer>>,
|
||||
|
@ -25,7 +25,7 @@ impl Server {
|
|||
let login = Arc::new(StaticLoginProvider::new(config.users).await?);
|
||||
|
||||
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 {
|
||||
lmtp_server,
|
||||
imap_server,
|
||||
|
@ -36,12 +36,13 @@ impl Server {
|
|||
pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> {
|
||||
tracing::info!("Init as provider");
|
||||
let login: ArcLoginProvider = match config.users {
|
||||
UserManagement::Demo => Arc::new(DemoLoginProvider::new()),
|
||||
UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?),
|
||||
UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?),
|
||||
};
|
||||
|
||||
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 {
|
||||
lmtp_server,
|
||||
|
|
394
tests/imap_features.rs
Normal file
394
tests/imap_features.rs
Normal 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])
|
||||
}
|
Loading…
Reference in a new issue