aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorQuentin <quentin@dufour.io>2024-01-02 22:44:29 +0000
committerQuentin <quentin@dufour.io>2024-01-02 22:44:29 +0000
commitb9a0c1e6eced036eb71e8221a4f236f72832fec2 (patch)
treec498a7a2a5833f2c6f27d4ba97894747f9d454c0 /src
parent6ff3c6f71efd802da422a371e6168ae528fb2ddc (diff)
parentc9a33c080d39d4a2b269e3c8f166a708b6606da5 (diff)
downloadaerogramme-b9a0c1e6eced036eb71e8221a4f236f72832fec2.tar.gz
aerogramme-b9a0c1e6eced036eb71e8221a4f236f72832fec2.zip
Merge pull request 'Implement imap-flow' (#34) from refactor/imap-flow into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/pulls/34
Diffstat (limited to 'src')
-rw-r--r--src/bayou.rs5
-rw-r--r--src/config.rs1
-rw-r--r--src/future_rest_admin_api.txt174
-rw-r--r--src/imap/command/anonymous.rs91
-rw-r--r--src/imap/command/anystate.rs52
-rw-r--r--src/imap/command/authenticated.rs400
-rw-r--r--src/imap/command/examined.rs142
-rw-r--r--src/imap/command/mod.rs17
-rw-r--r--src/imap/command/selected.rs161
-rw-r--r--src/imap/flow.rs24
-rw-r--r--src/imap/mailbox_view.rs493
-rw-r--r--src/imap/mod.rs223
-rw-r--r--src/imap/response.rs112
-rw-r--r--src/imap/session.rs224
-rw-r--r--src/login/demo_provider.rs51
-rw-r--r--src/login/mod.rs1
-rw-r--r--src/main.rs26
-rw-r--r--src/server.rs7
18 files changed, 1278 insertions, 926 deletions
diff --git a/src/bayou.rs b/src/bayou.rs
index 7253a30..c6a7ac0 100644
--- a/src/bayou.rs
+++ b/src/bayou.rs
@@ -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) {
diff --git a/src/config.rs b/src/config.rs
index 1438910..b9c1f09 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -26,6 +26,7 @@ pub struct ProviderConfig {
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "user_driver")]
pub enum UserManagement {
+ Demo,
Static(LoginStaticConfig),
Ldap(LoginLdapConfig),
}
diff --git a/src/future_rest_admin_api.txt b/src/future_rest_admin_api.txt
deleted file mode 100644
index 19ece27..0000000
--- a/src/future_rest_admin_api.txt
+++ /dev/null
@@ -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));
-}
diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs
index d258bd3..fbd10e9 100644
--- a/src/imap/command/anonymous.rs
+++ b/src/imap/command/anonymous.rs
@@ -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,
- ))
- }
}
diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs
new file mode 100644
index 0000000..42fe645
--- /dev/null
+++ b/src/imap/command/anystate.rs
@@ -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,
+ ))
+}
diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs
index 2deb723..1bb4c6d 100644
--- a/src/imap/command/authenticated.rs
+++ b/src/imap/command/authenticated.rs
@@ -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 crate::imap::command::anonymous;
+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::{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)));
}
}
diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs
index 1740b39..7de94f4 100644
--- a/src/imap/command/examined.rs
+++ b/src/imap/command/examined.rs
@@ -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))
+ ) -> 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, flow::Transition)> {
+ pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> {
self.mailbox.mailbox.force_sync().await?;
let updates = self.mailbox.update().await?;
Ok((
- Response::ok("NOOP completed.")?.with_body(updates),
+ Response::build()
+ .to_req(self.req)
+ .message("NOOP completed.")
+ .set_body(updates)
+ .ok()?,
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,
- };
-
- 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)),
- }
- }
}
diff --git a/src/imap/command/mod.rs b/src/imap/command/mod.rs
index 0b7e576..dc95746 100644
--- a/src/imap/command/mod.rs
+++ b/src/imap/command/mod.rs
@@ -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())?),
+ }
+ }
+}
diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs
index 90a00ee..220a952 100644
--- a/src/imap/command/selected.rs
+++ b/src/imap/command/selected.rs
@@ -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 crate::imap::command::examined;
+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::{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,
+ ))
+ }
+
+ 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,
+ ))
}
- async fn expunge(self) -> Result<(Response, flow::Transition)> {
+ 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,
))
}
diff --git a/src/imap/flow.rs b/src/imap/flow.rs
index eb94bb5..95810c1 100644
--- a/src/imap/flow.rs
+++ b/src/imap/flow.rs
@@ -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(())
}
}
diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs
index 99069e2..7434512 100644
--- a/src/imap/mailbox_view.rs
+++ b/src/imap/mailbox_view.rs
@@ -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()?,
}));
}
}
}
}
+ *old_view = new_view;
+ Ok(data)
+ }
+
+ /// 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()?);
- *self = new_view;
Ok(data)
}
- pub async fn store(
+ 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())
+ // Also identify what must be put as seen
+ let filtered_view = views
+ .iter()
+ .filter_map(|mv| mv.filter(&ap).ok().map(|(body, seen)| (mv, body, seen)))
.collect::<Vec<_>>();
-
// Register seen flags
- let future_flags = views
+ let future_flags = filtered_view
.iter()
- .filter(|mv| mv.add_seen)
- .map(|mv| async move {
+ .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 resp_str = String::from_utf8_lossy(&resp).to_lowercase();
+ 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 exp_no_parenthesis = &exp[1..exp.len() - 1];
- let exp_str = String::from_utf8_lossy(exp_no_parenthesis).to_lowercase();
+ let oracle_str =
+ format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).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(())
diff --git a/src/imap/mod.rs b/src/imap/mod.rs
index f85bcc6..31eeaa8 100644
--- a/src/imap/mod.rs
+++ b/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 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);
-
- let imap = ImapServer::new(incoming).serve(Instance::new(login.clone()));
- Ok(Server(imap))
-}
-
-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"),
- }
-
- Ok(())
- }
+pub struct Server {
+ bind_addr: SocketAddr,
+ login_provider: ArcLoginProvider,
}
-//---
-
-/// 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 {
+struct ClientContext {
+ stream: AnyStream,
+ addr: SocketAddr,
login_provider: ArcLoginProvider,
+ must_exit: watch::Receiver<bool>,
}
-impl Instance {
- pub fn new(login_provider: ArcLoginProvider) -> Self {
- Self { login_provider }
+pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server {
+ Server {
+ bind_addr: config.bind_addr,
+ login_provider: login,
}
}
-impl<'a> Service<&'a AddrStream> for Instance {
- type Response = Connection;
- type Error = anyhow::Error;
- type Future = BoxFuture<'static, Result<Self::Response>>;
+impl 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);
- fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
- Poll::Ready(Ok(()))
- }
+ tracing::info!("IMAP server shutting down, draining remaining connections...");
+ while connections.next().await.is_some() {}
- 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()
+ Ok(())
}
}
-//---
-
-/// 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?;
+
+ 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");
+ });
- fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
- Poll::Ready(Ok(()))
+ // 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(())
}
diff --git a/src/imap/response.rs b/src/imap/response.rs
new file mode 100644
index 0000000..d20e58e
--- /dev/null
+++ b/src/imap/response.rs
@@ -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![],
+ })
+ }
+}
diff --git a/src/imap/session.rs b/src/imap/session.rs
index 15141d3..5c67f8e 100644
--- a/src/imap/session.rs
+++ b/src/imap/session.rs
@@ -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>>,
-}
-
-//-----
-
-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()
- }
-}
+use imap_codec::imap_types::command::Command;
//-----
-
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
}
}
diff --git a/src/login/demo_provider.rs b/src/login/demo_provider.rs
new file mode 100644
index 0000000..11c7d54
--- /dev/null
+++ b/src/login/demo_provider.rs
@@ -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,
+ })
+ }
+}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 2926738..4a1dee1 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -1,3 +1,4 @@
+pub mod demo_provider;
pub mod ldap_provider;
pub mod static_provider;
diff --git a/src/main.rs b/src/main.rs
index 3221c2e..3baa8e2 100644
--- a/src/main.rs
+++ b/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)?;
diff --git a/src/server.rs b/src/server.rs
index 28e0b27..bd2fd5d 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -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,