From d53cf1d220ef08c0b9368cfe91bee7660b7f5a3b Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 31 May 2022 15:30:32 +0200 Subject: [PATCH 1/5] Implement public_login --- src/config.rs | 2 + src/login/ldap_provider.rs | 120 ++++++++++++++++++++++--------- src/login/mod.rs | 12 +++- src/login/static_provider.rs | 136 ++++++++++++++++++++++++----------- 4 files changed, 195 insertions(+), 75 deletions(-) diff --git a/src/config.rs b/src/config.rs index b77288b..a1de5ba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,6 +23,8 @@ pub struct LoginStaticConfig { #[derive(Deserialize, Debug, Clone)] pub struct LoginStaticUser { + #[serde(default)] + pub email_addresses: Vec, pub password: String, pub aws_access_key_id: String, diff --git a/src/login/ldap_provider.rs b/src/login/ldap_provider.rs index c9d23a0..9310e55 100644 --- a/src/login/ldap_provider.rs +++ b/src/login/ldap_provider.rs @@ -84,11 +84,30 @@ impl LdapLoginProvider { bucket_source, }) } + + fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result { + let aws_access_key_id = get_attr(user, &self.aws_access_key_id_attr)?; + let aws_secret_access_key = get_attr(user, &self.aws_secret_access_key_attr)?; + let bucket = match &self.bucket_source { + BucketSource::Constant(b) => b.clone(), + BucketSource::Attr(a) => get_attr(user, a)?, + }; + + Ok(StorageCredentials { + k2v_region: self.k2v_region.clone(), + s3_region: self.s3_region.clone(), + aws_access_key_id, + aws_secret_access_key, + bucket, + }) + } } #[async_trait] impl LoginProvider for LdapLoginProvider { async fn login(&self, username: &str, password: &str) -> Result { + check_identifier(username)?; + let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?; ldap3::drive!(conn); @@ -97,13 +116,6 @@ impl LoginProvider for LdapLoginProvider { ldap.simple_bind(dn, pw).await?.success()?; } - let username_is_ok = username - .chars() - .all(|c| c.is_alphanumeric() || "-+_.@".contains(c)); - if !username_is_ok { - bail!("Invalid username, must contain only a-z A-Z 0-9 - + _ . @"); - } - let (matches, _res) = ldap .search( &self.search_base, @@ -137,32 +149,9 @@ impl LoginProvider for LdapLoginProvider { .context("Invalid password")?; debug!("Ldap login with user name {} successfull", username); - let get_attr = |attr: &str| -> Result { - Ok(user - .attrs - .get(attr) - .ok_or(anyhow!("Missing attr: {}", attr))? - .iter() - .next() - .ok_or(anyhow!("No value for attr: {}", attr))? - .clone()) - }; - let aws_access_key_id = get_attr(&self.aws_access_key_id_attr)?; - let aws_secret_access_key = get_attr(&self.aws_secret_access_key_attr)?; - let bucket = match &self.bucket_source { - BucketSource::Constant(b) => b.clone(), - BucketSource::Attr(a) => get_attr(a)?, - }; + let storage = self.storage_creds_from_ldap_user(&user)?; - let storage = StorageCredentials { - k2v_region: self.k2v_region.clone(), - s3_region: self.s3_region.clone(), - aws_access_key_id, - aws_secret_access_key, - bucket, - }; - - let user_secret = get_attr(&self.user_secret_attr)?; + let user_secret = get_attr(&user, &self.user_secret_attr)?; let alternate_user_secrets = match &self.alternate_user_secrets_attr { None => vec![], Some(a) => user.attrs.get(a).cloned().unwrap_or_default(), @@ -178,4 +167,71 @@ impl LoginProvider for LdapLoginProvider { Ok(Credentials { storage, keys }) } + + async fn public_login(&self, email: &str) -> Result { + check_identifier(email)?; + + let (dn, pw) = match self.bind_dn_and_pw.as_ref() { + Some(x) => x, + None => bail!("Missing bind_dn and bind_password in LDAP login provider config"), + }; + + let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?; + ldap3::drive!(conn); + ldap.simple_bind(dn, pw).await?.success()?; + + let (matches, _res) = ldap + .search( + &self.search_base, + Scope::Subtree, + &format!( + "(&(objectClass=inetOrgPerson)({}={}))", + self.mail_attr, email + ), + &self.attrs_to_retrieve, + ) + .await? + .success()?; + + if matches.is_empty() { + bail!("No such user account"); + } + if matches.len() > 1 { + bail!("Multiple matching user accounts"); + } + let user = SearchEntry::construct(matches.into_iter().next().unwrap()); + debug!("Found matching LDAP user for email {}: {}", email, user.dn); + + let storage = self.storage_creds_from_ldap_user(&user)?; + drop(ldap); + + let k2v_client = storage.k2v_client()?; + let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?; + + Ok(PublicCredentials { + storage, + public_key, + }) + } +} + +fn get_attr(user: &SearchEntry, attr: &str) -> Result { + Ok(user + .attrs + .get(attr) + .ok_or(anyhow!("Missing attr: {}", attr))? + .iter() + .next() + .ok_or(anyhow!("No value for attr: {}", attr))? + .clone()) +} + +fn check_identifier(id: &str) -> Result<()> { + let is_ok = id + .chars() + .all(|c| c.is_alphanumeric() || "-+_.@".contains(c)); + if !is_ok { + bail!("Invalid username/email address, must contain only a-z A-Z 0-9 - + _ . @"); + } + Ok(()) } diff --git a/src/login/mod.rs b/src/login/mod.rs index 2640a58..c0e9032 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -24,6 +24,9 @@ pub trait LoginProvider { /// The login method takes an account's password as an input to decypher /// decryption keys and obtain full access to the user's account. async fn login(&self, username: &str, password: &str) -> Result; + /// The public_login method takes an account's email address and returns + /// public credentials for adding mails to the user's inbox. + async fn public_login(&self, email: &str) -> Result; } /// The struct Credentials represent all of the necessary information to interact @@ -36,6 +39,13 @@ pub struct Credentials { pub keys: CryptoKeys, } +#[derive(Clone, Debug)] +pub struct PublicCredentials { + /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V) + pub storage: StorageCredentials, + pub public_key: PublicKey, +} + /// The struct StorageCredentials contains access key to an S3 and K2V bucket #[derive(Clone, Debug)] pub struct StorageCredentials { @@ -396,7 +406,7 @@ impl CryptoKeys { Ok((salt_ct, public_ct)) } - async fn load_salt_and_public(k2v: &K2vClient) -> Result<([u8; 32], PublicKey)> { + pub async fn load_salt_and_public(k2v: &K2vClient) -> Result<([u8; 32], PublicKey)> { let mut params = k2v .read_batch(&[ k2v_read_single_key("keys", "salt", false), diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs index cc6ffb6..aa5e499 100644 --- a/src/login/static_provider.rs +++ b/src/login/static_provider.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; @@ -10,16 +11,34 @@ use crate::login::*; pub struct StaticLoginProvider { default_bucket: Option, - users: HashMap, + users: HashMap>, + users_by_email: HashMap>, + k2v_region: Region, s3_region: Region, } impl StaticLoginProvider { pub fn new(config: LoginStaticConfig, k2v_region: Region, s3_region: Region) -> Result { + let users = config + .users + .into_iter() + .map(|(k, v)| (k, Arc::new(v))) + .collect::>(); + let mut users_by_email = HashMap::new(); + for (_, u) in users.iter() { + for m in u.email_addresses.iter() { + if users_by_email.contains_key(m) { + bail!("Several users have same email address: {}", m); + } + users_by_email.insert(m.clone(), u.clone()); + } + } + Ok(Self { default_bucket: config.default_bucket, - users: config.users, + users, + users_by_email, k2v_region, s3_region, }) @@ -29,49 +48,82 @@ impl StaticLoginProvider { #[async_trait] impl LoginProvider for StaticLoginProvider { async fn login(&self, username: &str, password: &str) -> Result { - match self.users.get(username) { + let user = match self.users.get(username) { None => bail!("User {} does not exist", username), - Some(u) => { - if !verify_password(password, &u.password)? { - bail!("Wrong password"); - } - let bucket = u - .bucket - .clone() - .or_else(|| self.default_bucket.clone()) - .ok_or(anyhow!( - "No bucket configured and no default bucket specieid" - ))?; + Some(u) => u, + }; - let storage = StorageCredentials { - k2v_region: self.k2v_region.clone(), - s3_region: self.s3_region.clone(), - aws_access_key_id: u.aws_access_key_id.clone(), - aws_secret_access_key: u.aws_secret_access_key.clone(), - bucket, - }; - - let keys = match (&u.master_key, &u.secret_key) { - (Some(m), Some(s)) => { - let master_key = Key::from_slice(&base64::decode(m)?) - .ok_or(anyhow!("Invalid master key"))?; - let secret_key = SecretKey::from_slice(&base64::decode(s)?) - .ok_or(anyhow!("Invalid secret key"))?; - CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await? - } - (None, None) => { - let user_secrets = UserSecrets { - user_secret: u.user_secret.clone(), - alternate_user_secrets: u.alternate_user_secrets.clone(), - }; - CryptoKeys::open(&storage, &user_secrets, password).await? - } - _ => bail!("Either both master and secret key or none of them must be specified for user"), - }; - - Ok(Credentials { storage, keys }) - } + if !verify_password(password, &user.password)? { + bail!("Wrong password"); } + let bucket = user + .bucket + .clone() + .or_else(|| self.default_bucket.clone()) + .ok_or(anyhow!( + "No bucket configured and no default bucket specieid" + ))?; + + let storage = StorageCredentials { + k2v_region: self.k2v_region.clone(), + s3_region: self.s3_region.clone(), + aws_access_key_id: user.aws_access_key_id.clone(), + aws_secret_access_key: user.aws_secret_access_key.clone(), + bucket, + }; + + let keys = match (&user.master_key, &user.secret_key) { + (Some(m), Some(s)) => { + let master_key = + Key::from_slice(&base64::decode(m)?).ok_or(anyhow!("Invalid master key"))?; + let secret_key = SecretKey::from_slice(&base64::decode(s)?) + .ok_or(anyhow!("Invalid secret key"))?; + CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await? + } + (None, None) => { + let user_secrets = UserSecrets { + user_secret: user.user_secret.clone(), + alternate_user_secrets: user.alternate_user_secrets.clone(), + }; + CryptoKeys::open(&storage, &user_secrets, password).await? + } + _ => bail!( + "Either both master and secret key or none of them must be specified for user" + ), + }; + + Ok(Credentials { storage, keys }) + } + + async fn public_login(&self, email: &str) -> Result { + let user = match self.users_by_email.get(email) { + None => bail!("No user for email address {}", email), + Some(u) => u, + }; + + let bucket = user + .bucket + .clone() + .or_else(|| self.default_bucket.clone()) + .ok_or(anyhow!( + "No bucket configured and no default bucket specieid" + ))?; + + let storage = StorageCredentials { + k2v_region: self.k2v_region.clone(), + s3_region: self.s3_region.clone(), + aws_access_key_id: user.aws_access_key_id.clone(), + aws_secret_access_key: user.aws_secret_access_key.clone(), + bucket, + }; + + let k2v_client = storage.k2v_client()?; + let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?; + + Ok(PublicCredentials { + storage, + public_key, + }) } } From 01d82c14ca61e7c4de1e72c5f94456610464c064 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 31 May 2022 15:49:10 +0200 Subject: [PATCH 2/5] UUID generator; import kannader smtp server --- Cargo.lock | 636 ++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/config.rs | 8 + src/mail_uuid.rs | 76 ++++++ src/mailbox.rs | 6 +- src/main.rs | 1 + src/uidindex.rs | 36 +-- 7 files changed, 718 insertions(+), 48 deletions(-) create mode 100644 src/mail_uuid.rs diff --git a/Cargo.lock b/Cargo.lock index bbc8288..d9d4f69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,152 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-global-executor" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-net" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +dependencies = [ + "async-io", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi", +] + +[[package]] +name = "async-std" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite 0.2.9", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + [[package]] name = "async-trait" version = "0.1.53" @@ -39,6 +185,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + [[package]] name = "atty" version = "0.2.14" @@ -50,6 +202,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "auto_enums" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0dfe45d75158751e195799f47ea02e81f570aa24bc5ef999cdd9e888c4b5c3" +dependencies = [ + "auto_enums_core", + "auto_enums_derive", +] + +[[package]] +name = "auto_enums_core" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da47c46001293a2c4b744d731958be22cff408a2ab76e2279328f9713b1267b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "auto_enums_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41aed1da83ecdc799503b7cb94da1b45a34d72b49caf40a61d9cf5b88ec07cfd" +dependencies = [ + "autocfg", + "derive_utils", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -83,6 +269,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.4" @@ -110,6 +308,26 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + [[package]] name = "byteorder" version = "1.4.3" @@ -122,6 +340,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "cc" version = "1.0.73" @@ -147,6 +371,7 @@ dependencies = [ "num-integer", "num-traits", "serde", + "time", "winapi", ] @@ -189,6 +414,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -223,6 +457,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -243,6 +487,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "ctor" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "derive_utils" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.9.0" @@ -284,6 +549,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "duplexify" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cc346cd6db38ceab2d33f59b26024c3ddb8e75f047c6cafbcbc016ea8065d5" +dependencies = [ + "async-std", + "pin-project-lite 0.1.12", +] + [[package]] name = "ed25519" version = "1.5.2" @@ -312,6 +587,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + [[package]] name = "fastrand" version = "1.7.0" @@ -352,6 +633,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futures" version = "0.3.21" @@ -400,6 +687,21 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.9", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.21" @@ -436,7 +738,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.9", "pin-utils", "slab", ] @@ -462,6 +764,18 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gloo-timers" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "h2" version = "0.3.13" @@ -537,7 +851,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", - "pin-project-lite", + "pin-project-lite 0.2.9", ] [[package]] @@ -577,7 +891,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite", + "pin-project-lite 0.2.9", "socket2", "tokio", "tower-service", @@ -666,6 +980,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "k2v-client" version = "0.1.0" @@ -683,6 +1006,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -697,7 +1029,7 @@ checksum = "8a99b520993b21a6faab32643cf4726573dc18ca4cf2d48cbeb24d248c86c930" dependencies = [ "byteorder", "bytes", - "nom", + "nom 2.2.1", ] [[package]] @@ -714,7 +1046,7 @@ dependencies = [ "lber", "log", "native-tls", - "nom", + "nom 2.2.1", "percent-encoding", "thiserror", "tokio", @@ -724,6 +1056,19 @@ dependencies = [ "url", ] +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.126" @@ -749,6 +1094,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", + "value-bag", ] [[package]] @@ -764,6 +1110,7 @@ dependencies = [ "im", "itertools", "k2v-client", + "lazy_static", "ldap3", "log", "pretty_env_logger", @@ -775,6 +1122,7 @@ dependencies = [ "rusoto_s3", "rusoto_signature", "serde", + "smtp-server", "sodiumoxide", "tokio", "toml", @@ -840,6 +1188,19 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -932,6 +1293,12 @@ version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "password-hash" version = "0.3.2" @@ -955,6 +1322,32 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -973,6 +1366,19 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1037,6 +1443,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.8.5" @@ -1107,6 +1519,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.26" @@ -1230,7 +1651,7 @@ dependencies = [ "log", "md-5", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.9", "rusoto_credential", "rustc_version", "serde", @@ -1351,6 +1772,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1382,6 +1813,62 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +[[package]] +name = "smol" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "smtp-message" +version = "0.1.0" +source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#8c01360230f21c20d4c2da462dcf62e8a801ce0f" +dependencies = [ + "auto_enums", + "futures", + "idna", + "lazy_static", + "nom 6.1.2", + "pin-project", + "regex-automata", + "serde", +] + +[[package]] +name = "smtp-server" +version = "0.1.0" +source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#8c01360230f21c20d4c2da462dcf62e8a801ce0f" +dependencies = [ + "async-trait", + "chrono", + "duplexify", + "futures", + "smol", + "smtp-message", + "smtp-server-types", +] + +[[package]] +name = "smtp-server-types" +version = "0.1.0" +source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#8c01360230f21c20d4c2da462dcf62e8a801ce0f" +dependencies = [ + "serde", + "smtp-message", +] + [[package]] name = "socket2" version = "0.4.4" @@ -1404,6 +1891,12 @@ dependencies = [ "serde", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -1427,6 +1920,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.3.0" @@ -1476,6 +1975,16 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1503,7 +2012,7 @@ dependencies = [ "mio", "num_cpus", "once_cell", - "pin-project-lite", + "pin-project-lite 0.2.9", "signal-hook-registry", "socket2", "tokio-macros", @@ -1538,7 +2047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", - "pin-project-lite", + "pin-project-lite 0.2.9", "tokio", ] @@ -1551,7 +2060,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "pin-project-lite", + "pin-project-lite 0.2.9", "tokio", "tracing", ] @@ -1578,7 +2087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if", - "pin-project-lite", + "pin-project-lite 0.2.9", "tracing-attributes", "tracing-core", ] @@ -1648,6 +2157,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1660,6 +2179,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -1693,6 +2218,91 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1767,6 +2377,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "xml-rs" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index 0fabd23..16d0b52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ clap = { version = "3.1.18", features = ["derive", "env"] } hex = "0.4" im = "15" itertools = "0.10" +lazy_static = "1.4" ldap3 = { version = "0.10", default-features = false, features = ["tls"] } log = "0.4" pretty_env_logger = "0.4" @@ -31,5 +32,7 @@ tokio = "1.17.0" toml = "0.5" zstd = { version = "0.9", default-features = false } +smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } + k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", branch = "improve-k2v-client" } #k2v-client = { path = "../garage/src/k2v-client" } diff --git a/src/config.rs b/src/config.rs index a1de5ba..3fd0bd4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::io::Read; +use std::net::SocketAddr; use std::path::PathBuf; use anyhow::Result; @@ -13,6 +14,8 @@ pub struct Config { pub login_static: Option, pub login_ldap: Option, + + pub lmtp: Option, } #[derive(Deserialize, Debug, Clone)] @@ -62,6 +65,11 @@ pub struct LoginLdapConfig { pub bucket_attr: Option, } +#[derive(Deserialize, Debug, Clone)] +pub struct LmtpConfig { + pub bind_addr: SocketAddr, +} + pub fn read_config(config_file: PathBuf) -> Result { let mut file = std::fs::OpenOptions::new() .read(true) diff --git a/src/mail_uuid.rs b/src/mail_uuid.rs new file mode 100644 index 0000000..d0d582f --- /dev/null +++ b/src/mail_uuid.rs @@ -0,0 +1,76 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +use lazy_static::lazy_static; +use rand::prelude::*; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::time::now_msec; + +/// A Mail UUID is composed of two components: +/// - a process identifier, 128 bits, itself composed of: +/// - the timestamp of when the process started, 64 bits +/// - a 64-bit random number +/// - a sequence number, 64 bits +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug)] +pub struct MailUuid(pub [u8; 24]); + +struct UuidGenerator { + pid: u128, + sn: AtomicU64, +} + +impl UuidGenerator { + fn new() -> Self { + let time = now_msec() as u128; + let rand = thread_rng().gen::() as u128; + Self { + pid: (time << 64) | rand, + sn: AtomicU64::new(0), + } + } + + fn gen(&self) -> MailUuid { + let sn = self.sn.fetch_add(1, Ordering::Relaxed); + let mut res = [0u8; 24]; + res[0..16].copy_from_slice(&u128::to_be_bytes(self.pid)); + res[16..24].copy_from_slice(&u64::to_be_bytes(sn)); + MailUuid(res) + } +} + +lazy_static! { + static ref GENERATOR: UuidGenerator = UuidGenerator::new(); +} + +pub fn gen_uuid() -> MailUuid { + GENERATOR.gen() +} + +// -- serde -- + +impl<'de> Deserialize<'de> for MailUuid { + fn deserialize(d: D) -> Result + where + D: Deserializer<'de>, + { + let v = String::deserialize(d)?; + let bytes = hex::decode(v).map_err(|_| D::Error::custom("invalid hex"))?; + + if bytes.len() != 24 { + return Err(D::Error::custom("bad length")); + } + + let mut tmp = [0u8; 24]; + tmp[..].copy_from_slice(&bytes); + Ok(Self(tmp)) + } +} + +impl Serialize for MailUuid { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&hex::encode(self.0)) + } +} diff --git a/src/mailbox.rs b/src/mailbox.rs index a20ca15..49d8e56 100644 --- a/src/mailbox.rs +++ b/src/mailbox.rs @@ -1,11 +1,11 @@ use anyhow::Result; use k2v_client::K2vClient; -use rand::prelude::*; use rusoto_s3::S3Client; use crate::bayou::Bayou; use crate::cryptoblob::Key; use crate::login::Credentials; +use crate::mail_uuid::*; use crate::uidindex::*; pub struct Mailbox { @@ -38,12 +38,10 @@ impl Mailbox { dump(&self.uid_index); - let mut rand_id = [0u8; 24]; - rand_id[..16].copy_from_slice(&u128::to_be_bytes(thread_rng().gen())); let add_mail_op = self .uid_index .state() - .op_mail_add(MailUuid(rand_id), vec!["\\Unseen".into()]); + .op_mail_add(gen_uuid(), vec!["\\Unseen".into()]); self.uid_index.push(add_mail_op).await?; dump(&self.uid_index); diff --git a/src/main.rs b/src/main.rs index ada94fc..b8231f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod bayou; mod config; mod cryptoblob; mod login; +mod mail_uuid; mod mailbox; mod server; mod time; diff --git a/src/uidindex.rs b/src/uidindex.rs index 1e30190..ecd52ff 100644 --- a/src/uidindex.rs +++ b/src/uidindex.rs @@ -1,17 +1,12 @@ use im::OrdMap; -use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::bayou::*; +use crate::mail_uuid::MailUuid; type ImapUid = u32; type ImapUidvalidity = u32; -/// A Mail UUID is composed of two components: -/// - a process identifier, 128 bits -/// - a sequence number, 64 bits -#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug)] -pub struct MailUuid(pub [u8; 24]); - #[derive(Clone)] pub struct UidIndex { pub mail_uid: OrdMap, @@ -176,30 +171,3 @@ impl Serialize for UidIndex { val.serialize(serializer) } } - -impl<'de> Deserialize<'de> for MailUuid { - fn deserialize(d: D) -> Result - where - D: Deserializer<'de>, - { - let v = String::deserialize(d)?; - let bytes = hex::decode(v).map_err(|_| D::Error::custom("invalid hex"))?; - - if bytes.len() != 24 { - return Err(D::Error::custom("bad length")); - } - - let mut tmp = [0u8; 24]; - tmp[..].copy_from_slice(&bytes); - Ok(Self(tmp)) - } -} - -impl Serialize for MailUuid { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&hex::encode(self.0)) - } -} From 553a15a82a700792986b23cb89e2a8ec070cc27d Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 31 May 2022 17:07:34 +0200 Subject: [PATCH 3/5] Implementn basic LMTP server --- Cargo.lock | 5 + Cargo.toml | 6 +- src/config.rs | 1 + src/lmtp.rs | 263 +++++++++++++++++++++++++++++++++++++++++++++++ src/mail_uuid.rs | 8 +- src/main.rs | 12 +++ src/server.rs | 69 ++++++++++--- 7 files changed, 350 insertions(+), 14 deletions(-) create mode 100644 src/lmtp.rs diff --git a/Cargo.lock b/Cargo.lock index d9d4f69..2e5416c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1106,6 +1106,8 @@ dependencies = [ "async-trait", "base64", "clap", + "duplexify", + "futures", "hex", "im", "itertools", @@ -1122,9 +1124,11 @@ dependencies = [ "rusoto_s3", "rusoto_signature", "serde", + "smtp-message", "smtp-server", "sodiumoxide", "tokio", + "tokio-util", "toml", "zstd", ] @@ -2059,6 +2063,7 @@ checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite 0.2.9", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 16d0b52..00acdca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,9 @@ argon2 = "0.3" async-trait = "0.1" base64 = "0.13" clap = { version = "3.1.18", features = ["derive", "env"] } +duplexify = "1.1.0" hex = "0.4" +futures = "0.3" im = "15" itertools = "0.10" lazy_static = "1.4" @@ -28,10 +30,12 @@ rand = "0.8.5" rmp-serde = "0.15" rpassword = "6.0" sodiumoxide = "0.2" -tokio = "1.17.0" +tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } +tokio-util = { version = "0.7", features = [ "compat" ] } toml = "0.5" zstd = { version = "0.9", default-features = false } +smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", branch = "improve-k2v-client" } diff --git a/src/config.rs b/src/config.rs index 3fd0bd4..9ec0ea1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -68,6 +68,7 @@ pub struct LoginLdapConfig { #[derive(Deserialize, Debug, Clone)] pub struct LmtpConfig { pub bind_addr: SocketAddr, + pub hostname: String, } pub fn read_config(config_file: PathBuf) -> Result { diff --git a/src/lmtp.rs b/src/lmtp.rs new file mode 100644 index 0000000..4186d69 --- /dev/null +++ b/src/lmtp.rs @@ -0,0 +1,263 @@ +use std::collections::HashMap; +use std::net::SocketAddr; +use std::{pin::Pin, sync::Arc}; + +use anyhow::{bail, Result}; +use async_trait::async_trait; +use duplexify::Duplex; +use futures::{io, AsyncRead, AsyncReadExt, AsyncWrite}; +use futures::{stream, stream::FuturesUnordered, StreamExt}; +use log::*; +use rusoto_s3::{PutObjectRequest, S3Client, S3}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::select; +use tokio::sync::watch; +use tokio_util::compat::*; + +use smtp_message::{Email, EscapedDataReader, Reply, ReplyCode}; +use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata, Protocol}; + +use crate::config::*; +use crate::cryptoblob::*; +use crate::login::*; +use crate::mail_uuid::*; + +pub struct LmtpServer { + bind_addr: SocketAddr, + hostname: String, + login_provider: Arc, +} + +impl LmtpServer { + pub fn new( + config: LmtpConfig, + login_provider: Arc, + ) -> Arc { + Arc::new(Self { + bind_addr: config.bind_addr, + hostname: config.hostname, + login_provider, + }) + } + + pub async fn run(self: &Arc, mut must_exit: watch::Receiver) -> Result<()> { + let tcp = TcpListener::bind(self.bind_addr).await?; + 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) = select! { + a = tcp.accept() => a?, + _ = wait_conn_finished => continue, + _ = must_exit.changed() => continue, + }; + + let conn = tokio::spawn(smtp_server::interact( + socket.compat(), + smtp_server::IsAlreadyTls::No, + Conn { remote_addr }, + self.clone(), + )); + + connections.push(conn); + } + drop(tcp); + + info!("LMTP server shutting down, draining remaining connections..."); + while connections.next().await.is_some() {} + + Ok(()) + } +} + +// ---- + +pub struct Conn { + remote_addr: SocketAddr, +} + +pub struct Message { + to: Vec, +} + +#[async_trait] +impl Config for LmtpServer { + const PROTOCOL: Protocol = Protocol::Lmtp; + + type ConnectionUserMeta = Conn; + type MailUserMeta = Message; + + fn hostname(&self, _conn_meta: &ConnectionMetadata) -> &str { + &self.hostname + } + + async fn new_mail(&self, _conn_meta: &mut ConnectionMetadata) -> Message { + Message { to: vec![] } + } + + async fn tls_accept( + &self, + _io: IO, + _conn_meta: &mut ConnectionMetadata, + ) -> io::Result>, Pin>>> + where + IO: Send + AsyncRead + AsyncWrite, + { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "TLS not implemented for LMTP server", + )) + } + + async fn filter_from( + &self, + from: Option, + meta: &mut MailMetadata, + _conn_meta: &mut ConnectionMetadata, + ) -> Decision> { + Decision::Accept { + reply: reply::okay_from().convert(), + res: from, + } + } + + async fn filter_to( + &self, + to: Email, + meta: &mut MailMetadata, + _conn_meta: &mut ConnectionMetadata, + ) -> Decision { + let to_str = match to.hostname.as_ref() { + Some(h) => format!("{}@{}", to.localpart, h), + None => to.localpart.to_string(), + }; + match self.login_provider.public_login(&to_str).await { + Ok(creds) => { + meta.user.to.push(creds); + Decision::Accept { + reply: reply::okay_to().convert(), + res: to, + } + } + Err(e) => Decision::Reject { + reply: Reply { + code: ReplyCode::POLICY_REASON, + ecode: None, + text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], + }, + }, + } + } + + async fn handle_mail<'a, R>( + &self, + reader: &mut EscapedDataReader<'a, R>, + _mail: MailMetadata, + _conn_meta: &mut ConnectionMetadata, + ) -> Decision<()> + where + R: Send + Unpin + AsyncRead, + { + unreachable!(); + } + + async fn handle_mail_multi<'a, 'slife0, 'slife1, 'stream, R>( + &'slife0 self, + reader: &mut EscapedDataReader<'a, R>, + meta: MailMetadata, + conn_meta: &'slife1 mut ConnectionMetadata, + ) -> Pin> + Send + 'stream>> + where + R: Send + Unpin + AsyncRead, + 'slife0: 'stream, + 'slife1: 'stream, + Self: 'stream, + { + let err_response_stream = |meta: MailMetadata, msg: String| { + Box::pin( + stream::iter(meta.user.to.into_iter()).map(move |_| Decision::Reject { + reply: Reply { + code: ReplyCode::POLICY_REASON, + ecode: None, + text: vec![smtp_message::MaybeUtf8::Utf8(msg.clone())], + }, + }), + ) + }; + + let mut text = Vec::new(); + if reader.read_to_end(&mut text).await.is_err() { + return err_response_stream(meta, "io error".into()); + } + reader.complete(); + + let encrypted_message = match EncryptedMessage::new(text) { + Ok(x) => Arc::new(x), + Err(e) => return err_response_stream(meta, e.to_string()), + }; + + Box::pin(stream::iter(meta.user.to.into_iter()).then(move |creds| { + let encrypted_message = encrypted_message.clone(); + async move { + match encrypted_message.deliver_to(creds).await { + Ok(()) => Decision::Accept { + reply: reply::okay_mail().convert(), + res: (), + }, + Err(e) => Decision::Reject { + reply: Reply { + code: ReplyCode::POLICY_REASON, + ecode: None, + text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], + }, + }, + } + } + })) + } +} + +// ---- + +struct EncryptedMessage { + key: Key, + encrypted_body: Vec, +} + +impl EncryptedMessage { + fn new(body: Vec) -> Result { + let key = gen_key(); + let encrypted_body = seal(&body, &key)?; + Ok(Self { + key, + encrypted_body, + }) + } + + async fn deliver_to(self: Arc, creds: PublicCredentials) -> Result<()> { + let s3_client = creds.storage.s3_client()?; + + let encrypted_key = + sodiumoxide::crypto::sealedbox::seal(self.key.as_ref(), &creds.public_key); + let key_header = base64::encode(&encrypted_key); + + let mut por = PutObjectRequest::default(); + por.bucket = creds.storage.bucket.clone(); + por.key = format!("incoming/{}", gen_uuid().to_string()); + por.metadata = Some( + [("Message-Key".to_string(), key_header)] + .into_iter() + .collect::>(), + ); + por.body = Some(self.encrypted_body.clone().into()); + s3_client.put_object(por).await?; + + Ok(()) + } +} diff --git a/src/mail_uuid.rs b/src/mail_uuid.rs index d0d582f..b784e78 100644 --- a/src/mail_uuid.rs +++ b/src/mail_uuid.rs @@ -71,6 +71,12 @@ impl Serialize for MailUuid { where S: Serializer, { - serializer.serialize_str(&hex::encode(self.0)) + serializer.serialize_str(&self.to_string()) + } +} + +impl ToString for MailUuid { + fn to_string(&self) -> String { + hex::encode(self.0) } } diff --git a/src/main.rs b/src/main.rs index b8231f1..33d3188 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod bayou; mod config; mod cryptoblob; +mod lmtp; mod login; mod mail_uuid; mod mailbox; @@ -35,6 +36,11 @@ enum Command { #[clap(short, long, env = "CONFIG_FILE", default_value = "mailrage.toml")] config_file: PathBuf, }, + /// TEST TEST TEST + Test { + #[clap(short, long, env = "CONFIG_FILE", default_value = "mailrage.toml")] + config_file: PathBuf, + }, /// Initializes key pairs for a user and adds a key decryption password FirstLogin { #[clap(flatten)] @@ -125,6 +131,12 @@ async fn main() -> Result<()> { let server = Server::new(config)?; server.run().await?; } + Command::Test { config_file } => { + let config = read_config(config_file)?; + + let server = Server::new(config)?; + server.test().await?; + } Command::FirstLogin { creds, user_secrets, diff --git a/src/server.rs b/src/server.rs index e1ab599..1fd21b4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,18 +1,23 @@ -use anyhow::{bail, Result}; use std::sync::Arc; +use anyhow::{bail, Result}; +use futures::{try_join, StreamExt}; +use log::*; use rusoto_signature::Region; +use tokio::sync::watch; use crate::config::*; +use crate::lmtp::*; use crate::login::{ldap_provider::*, static_provider::*, *}; use crate::mailbox::Mailbox; pub struct Server { - pub login_provider: Box, + pub login_provider: Arc, + pub lmtp_server: Option>, } impl Server { - pub fn new(config: Config) -> Result> { + pub fn new(config: Config) -> Result { let s3_region = Region::Custom { name: config.aws_region.clone(), endpoint: config.s3_endpoint, @@ -21,17 +26,43 @@ impl Server { name: config.aws_region, endpoint: config.k2v_endpoint, }; - let login_provider: Box = match (config.login_static, config.login_ldap) - { - (Some(st), None) => Box::new(StaticLoginProvider::new(st, k2v_region, s3_region)?), - (None, Some(ld)) => Box::new(LdapLoginProvider::new(ld, k2v_region, s3_region)?), - (Some(_), Some(_)) => bail!("A single login provider must be set up in config file"), - (None, None) => bail!("No login provider is set up in config file"), - }; - Ok(Arc::new(Self { login_provider })) + let login_provider: Arc = + match (config.login_static, config.login_ldap) { + (Some(st), None) => Arc::new(StaticLoginProvider::new(st, k2v_region, s3_region)?), + (None, Some(ld)) => Arc::new(LdapLoginProvider::new(ld, k2v_region, s3_region)?), + (Some(_), Some(_)) => { + bail!("A single login provider must be set up in config file") + } + (None, None) => bail!("No login provider is set up in config file"), + }; + + let lmtp_server = config + .lmtp + .map(|cfg| LmtpServer::new(cfg, login_provider.clone())); + + Ok(Self { + login_provider, + lmtp_server, + }) } - pub async fn run(self: &Arc) -> Result<()> { + pub async fn run(&self) -> Result<()> { + let (exit_signal, provoke_exit) = watch_ctrl_c(); + let exit_on_err = move |err: anyhow::Error| { + error!("Error: {}", err); + let _ = provoke_exit.send(true); + }; + + try_join!(async { + match self.lmtp_server.as_ref() { + None => Ok(()), + Some(s) => s.run(exit_signal.clone()).await, + } + })?; + Ok(()) + } + + pub async fn test(&self) -> Result<()> { let creds = self.login_provider.login("lx", "plop").await?; let mut mailbox = Mailbox::new(&creds, "TestMailbox".to_string()).await?; @@ -41,3 +72,17 @@ impl Server { Ok(()) } } + +pub fn watch_ctrl_c() -> (watch::Receiver, Arc>) { + let (send_cancel, watch_cancel) = watch::channel(false); + let send_cancel = Arc::new(send_cancel); + let send_cancel_2 = send_cancel.clone(); + tokio::spawn(async move { + tokio::signal::ctrl_c() + .await + .expect("failed to install CTRL+C signal handler"); + info!("Received CTRL+C, shutting down."); + send_cancel.send(true).unwrap(); + }); + (watch_cancel, send_cancel_2) +} From dd62efa24c66eaec87b0cbb99fc48d6bf8441801 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 1 Jun 2022 00:02:27 +0200 Subject: [PATCH 4/5] Implement FromStr for MailUuid --- src/mail_uuid.rs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/mail_uuid.rs b/src/mail_uuid.rs index b784e78..647238f 100644 --- a/src/mail_uuid.rs +++ b/src/mail_uuid.rs @@ -1,4 +1,5 @@ use std::sync::atomic::{AtomicU64, Ordering}; +use std::str::FromStr; use lazy_static::lazy_static; use rand::prelude::*; @@ -54,15 +55,7 @@ impl<'de> Deserialize<'de> for MailUuid { D: Deserializer<'de>, { let v = String::deserialize(d)?; - let bytes = hex::decode(v).map_err(|_| D::Error::custom("invalid hex"))?; - - if bytes.len() != 24 { - return Err(D::Error::custom("bad length")); - } - - let mut tmp = [0u8; 24]; - tmp[..].copy_from_slice(&bytes); - Ok(Self(tmp)) + MailUuid::from_str(&v).map_err(D::Error::custom) } } @@ -80,3 +73,19 @@ impl ToString for MailUuid { hex::encode(self.0) } } + +impl FromStr for MailUuid { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let bytes = hex::decode(s).map_err(|_| "invalid hex")?; + + if bytes.len() != 24 { + return Err("bad length"); + } + + let mut tmp = [0u8; 24]; + tmp[..].copy_from_slice(&bytes); + Ok(MailUuid(tmp)) + } +} From 0700e27127e4644dbd323b9a22d994209143fa2a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 1 Jun 2022 00:06:26 +0200 Subject: [PATCH 5/5] Implement ToString and FromStr for bayou timestamp --- src/bayou.rs | 38 +++++++++++++++++++++++--------------- src/mail_uuid.rs | 2 +- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/bayou.rs b/src/bayou.rs index c9ae67f..7a76222 100644 --- a/src/bayou.rs +++ b/src/bayou.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use std::time::{Duration, Instant}; use anyhow::{anyhow, bail, Result}; @@ -123,7 +124,7 @@ impl Bayou { .collect(); // 3. List all operations starting from checkpoint - let ts_ser = self.checkpoint.0.serialize(); + let ts_ser = self.checkpoint.0.to_string(); debug!("(sync) looking up operations starting at {}", ts_ser); let ops_map = self .k2v @@ -148,8 +149,9 @@ impl Bayou { let mut ops = vec![]; for (tsstr, val) in ops_map { - let ts = Timestamp::parse(&tsstr) - .ok_or(anyhow!("Invalid operation timestamp: {}", tsstr))?; + let ts = tsstr + .parse::() + .map_err(|_| anyhow!("Invalid operation timestamp: {}", tsstr))?; if val.value.len() != 1 { bail!("Invalid operation, has {} values", val.value.len()); } @@ -251,7 +253,7 @@ impl Bayou { self.k2v .insert_item( &self.path, - &ts.serialize(), + &ts.to_string(), seal_serialize(&op, &self.key)?, None, ) @@ -316,7 +318,7 @@ impl Bayou { let ts_cp = self.history[i_cp].0; debug!( "(cp) we could checkpoint at time {} (index {} in history)", - ts_cp.serialize(), + ts_cp.to_string(), i_cp ); @@ -330,13 +332,13 @@ impl Bayou { { debug!( "(cp) last checkpoint is too recent: {}, not checkpointing", - last_cp.0.serialize() + last_cp.0.to_string() ); return Ok(()); } } - debug!("(cp) saving checkpoint at {}", ts_cp.serialize()); + debug!("(cp) saving checkpoint at {}", ts_cp.to_string()); // Calculate state at time of checkpoint let mut last_known_state = (0, &self.checkpoint.1); @@ -356,7 +358,7 @@ impl Bayou { let mut por = PutObjectRequest::default(); por.bucket = self.bucket.clone(); - por.key = format!("{}/checkpoint/{}", self.path, ts_cp.serialize()); + por.key = format!("{}/checkpoint/{}", self.path, ts_cp.to_string()); por.body = Some(cryptoblob.into()); self.s3.put_object(por).await?; @@ -375,7 +377,7 @@ impl Bayou { } // Delete corresponding range of operations - let ts_ser = existing_checkpoints[last_to_keep].0.serialize(); + let ts_ser = existing_checkpoints[last_to_keep].0.to_string(); self.k2v .delete_batch(&[BatchDeleteOp { partition_key: &self.path, @@ -414,7 +416,7 @@ impl Bayou { for object in checkpoints_res.contents.unwrap_or_default() { if let Some(key) = object.key { if let Some(ckid) = key.strip_prefix(&prefix) { - if let Some(ts) = Timestamp::parse(ckid) { + if let Ok(ts) = ckid.parse::() { checkpoints.push((ts, key)); } } @@ -451,20 +453,26 @@ impl Timestamp { pub fn zero() -> Self { Self { msec: 0, rand: 0 } } +} - pub fn serialize(&self) -> String { +impl ToString for Timestamp { + fn to_string(&self) -> String { let mut bytes = [0u8; 16]; bytes[0..8].copy_from_slice(&u64::to_be_bytes(self.msec)); bytes[8..16].copy_from_slice(&u64::to_be_bytes(self.rand)); hex::encode(&bytes) } +} - pub fn parse(v: &str) -> Option { - let bytes = hex::decode(v).ok()?; +impl FromStr for Timestamp { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let bytes = hex::decode(s).map_err(|_| "invalid hex")?; if bytes.len() != 16 { - return None; + return Err("bad length"); } - Some(Self { + Ok(Self { msec: u64::from_be_bytes(bytes[0..8].try_into().unwrap()), rand: u64::from_be_bytes(bytes[8..16].try_into().unwrap()), }) diff --git a/src/mail_uuid.rs b/src/mail_uuid.rs index 647238f..ab76bce 100644 --- a/src/mail_uuid.rs +++ b/src/mail_uuid.rs @@ -1,5 +1,5 @@ -use std::sync::atomic::{AtomicU64, Ordering}; use std::str::FromStr; +use std::sync::atomic::{AtomicU64, Ordering}; use lazy_static::lazy_static; use rand::prelude::*;