diff --git a/crates/directory/src/cache/lookup.rs b/crates/directory/src/cache/lookup.rs index 2e4a7c92..f18548c7 100644 --- a/crates/directory/src/cache/lookup.rs +++ b/crates/directory/src/cache/lookup.rs @@ -13,24 +13,16 @@ impl Directory for CachedDirectory { self.inner.authenticate(credentials).await } - async fn principal_by_name(&self, name: &str) -> crate::Result> { - self.inner.principal_by_name(name).await + async fn principal(&self, name: &str) -> crate::Result> { + self.inner.principal(name).await } - async fn principal_by_id(&self, id: u32) -> crate::Result> { - self.inner.principal_by_id(id).await + async fn emails_by_name(&self, name: &str) -> crate::Result> { + self.inner.emails_by_name(name).await } - async fn member_of(&self, _principal: &Principal) -> crate::Result> { - self.inner.member_of(_principal).await - } - - async fn emails_by_id(&self, id: u32) -> crate::Result> { - self.inner.emails_by_id(id).await - } - - async fn ids_by_email(&self, address: &str) -> crate::Result> { - self.inner.ids_by_email(address).await + async fn names_by_email(&self, address: &str) -> crate::Result> { + self.inner.names_by_email(address).await } async fn rcpt(&self, address: &str) -> crate::Result { diff --git a/crates/directory/src/imap/lookup.rs b/crates/directory/src/imap/lookup.rs index e70d393d..18e9bc1d 100644 --- a/crates/directory/src/imap/lookup.rs +++ b/crates/directory/src/imap/lookup.rs @@ -51,24 +51,16 @@ impl Directory for ImapDirectory { } } - async fn principal_by_name(&self, _name: &str) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "principal_by_name")) + async fn principal(&self, _name: &str) -> crate::Result> { + Err(DirectoryError::unsupported("imap", "principal")) } - async fn principal_by_id(&self, _id: u32) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "principal_by_id")) + async fn emails_by_name(&self, _: &str) -> crate::Result> { + Err(DirectoryError::unsupported("imap", "emails_by_name")) } - async fn member_of(&self, _principal: &Principal) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "member_of")) - } - - async fn emails_by_id(&self, _id: u32) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "emails_by_id")) - } - - async fn ids_by_email(&self, _address: &str) -> crate::Result> { - Err(DirectoryError::unsupported("imap", "ids_by_email")) + async fn names_by_email(&self, _address: &str) -> crate::Result> { + Err(DirectoryError::unsupported("imap", "names_by_email")) } async fn rcpt(&self, _address: &str) -> crate::Result { diff --git a/crates/directory/src/ldap/config.rs b/crates/directory/src/ldap/config.rs index 07da01be..b82c2e09 100644 --- a/crates/directory/src/ldap/config.rs +++ b/crates/directory/src/ldap/config.rs @@ -36,10 +36,8 @@ impl LdapDirectory { let mut mappings = LdapMappings { base_dn: config.value_require((&prefix, "base-dn"))?.to_string(), - filter_login: LdapFilter::from_config(config, (&prefix, "filter.login"))?, filter_name: LdapFilter::from_config(config, (&prefix, "filter.name"))?, filter_email: LdapFilter::from_config(config, (&prefix, "filter.email"))?, - filter_id: LdapFilter::from_config(config, (&prefix, "filter.id"))?, filter_verify: LdapFilter::from_config(config, (&prefix, "filter.verify"))?, filter_expand: LdapFilter::from_config(config, (&prefix, "filter.expand"))?, filter_domains: LdapFilter::from_config(config, (&prefix, "filter.domains"))?, @@ -65,10 +63,6 @@ impl LdapDirectory { .values((&prefix, "attributes.groups")) .map(|(_, v)| v.to_string()) .collect(), - attr_id: config - .values((&prefix, "attributes.id")) - .map(|(_, v)| v.to_string()) - .collect(), attr_email_address: config .values((&prefix, "attributes.email")) .map(|(_, v)| v.to_string()) @@ -85,7 +79,6 @@ impl LdapDirectory { }; for attr in [ - &mappings.attr_id, &mappings.attr_name, &mappings.attr_description, &mappings.attr_secret, diff --git a/crates/directory/src/ldap/lookup.rs b/crates/directory/src/ldap/lookup.rs index 998cd244..ccec0e34 100644 --- a/crates/directory/src/ldap/lookup.rs +++ b/crates/directory/src/ldap/lookup.rs @@ -17,7 +17,7 @@ impl Directory for LdapDirectory { Credentials::XOauth2 { username, secret } => (username, secret), }; match self - .find_principal(&self.mappings.filter_login.build(username)) + .find_principal(&self.mappings.filter_name.build(username)) .await { Ok(Some(principal)) => { @@ -31,54 +31,12 @@ impl Directory for LdapDirectory { } } - async fn principal_by_name(&self, name: &str) -> crate::Result> { + async fn principal(&self, name: &str) -> crate::Result> { self.find_principal(&self.mappings.filter_name.build(name)) .await } - async fn principal_by_id(&self, id: u32) -> crate::Result> { - self.find_principal(&self.mappings.filter_id.build(&id.to_string())) - .await - } - - async fn member_of(&self, principal: &Principal) -> crate::Result> { - if principal.member_of.is_empty() { - return Ok(Vec::new()); - } - let mut conn = self.pool.get().await?; - let mut ids = Vec::with_capacity(principal.member_of.len()); - for group in &principal.member_of { - let (rs, _res) = if group.contains('=') { - conn.search(group, Scope::Base, "objectClass=*", &self.mappings.attr_id) - .await? - .success()? - } else { - conn.search( - &self.mappings.base_dn, - Scope::Subtree, - &self.mappings.filter_name.build(group), - &self.mappings.attr_id, - ) - .await? - .success()? - }; - for entry in rs { - for (attr, value) in SearchEntry::construct(entry).attrs { - if self.mappings.attr_id.contains(&attr) { - if let Some(id) = value.first() { - if let Ok(id) = id.parse() { - ids.push(id); - } - } - } - } - } - } - - Ok(ids) - } - - async fn emails_by_id(&self, id: u32) -> crate::Result> { + async fn emails_by_name(&self, name: &str) -> crate::Result> { let (rs, _res) = self .pool .get() @@ -86,7 +44,7 @@ impl Directory for LdapDirectory { .search( &self.mappings.base_dn, Scope::Subtree, - &self.mappings.filter_id.build(&id.to_string()), + &self.mappings.filter_name.build(name), &self.mappings.attrs_email, ) .await? @@ -109,8 +67,8 @@ impl Directory for LdapDirectory { Ok(emails) } - async fn ids_by_email(&self, address: &str) -> crate::Result> { - let ids = self + async fn names_by_email(&self, address: &str) -> crate::Result> { + let names = self .pool .get() .await? @@ -121,13 +79,13 @@ impl Directory for LdapDirectory { .mappings .filter_email .build(unwrap_subaddress(address, self.opt.subaddressing).as_ref()), - &self.mappings.attr_id, + &self.mappings.attr_name, ) .await? .success() - .map(|(rs, _res)| self.extract_ids(rs))?; + .map(|(rs, _res)| self.extract_names(rs))?; - if ids.is_empty() && self.opt.catch_all { + if names.is_empty() && self.opt.catch_all { self.pool .get() .await? @@ -138,14 +96,14 @@ impl Directory for LdapDirectory { .mappings .filter_email .build(&to_catch_all_address(address)), - &self.mappings.attr_id, + &self.mappings.attr_name, ) .await? .success() - .map(|(rs, _res)| self.extract_ids(rs)) + .map(|(rs, _res)| self.extract_names(rs)) .map_err(|e| e.into()) } else { - Ok(ids) + Ok(names) } } @@ -323,45 +281,72 @@ impl LdapDirectory { ) .await? .success()?; - Ok(rs.into_iter().next().map(|entry| { + + if let Some(mut principal) = rs.into_iter().next().map(|entry| { self.mappings .entry_to_principal(SearchEntry::construct(entry)) - })) + }) { + // Map groups + if !principal.member_of.is_empty() { + let mut conn = self.pool.get().await?; + let mut names = Vec::with_capacity(principal.member_of.len()); + for group in principal.member_of { + if group.contains('=') { + let (rs, _res) = conn + .search( + &group, + Scope::Base, + "objectClass=*", + &self.mappings.attr_name, + ) + .await? + .success()?; + for entry in rs { + 'outer: for (attr, value) in SearchEntry::construct(entry).attrs { + if self.mappings.attr_name.contains(&attr) { + if let Some(name) = value.first() { + if !name.is_empty() { + names.push(name.to_string()); + break 'outer; + } + } + } + } + } + } else { + names.push(group); + } + } + principal.member_of = names; + } + Ok(Some(principal)) + } else { + Ok(None) + } } - fn extract_ids(&self, rs: Vec) -> Vec { - let mut ids = Vec::with_capacity(rs.len()); + fn extract_names(&self, rs: Vec) -> Vec { + let mut names = Vec::with_capacity(rs.len()); for entry in rs { let entry = SearchEntry::construct(entry); - 'outer: for attr in &self.mappings.attr_id { - if let Some(values) = entry.attrs.get(attr) { - for id in values { - if let Ok(id) = id.parse() { - ids.push(id); - break 'outer; - } + 'outer: for attr in &self.mappings.attr_name { + if let Some(value) = entry.attrs.get(attr).and_then(|v| v.first()) { + if !value.is_empty() { + names.push(value.to_string()); + break 'outer; } } } } - ids + names } } impl LdapMappings { pub fn entry_to_principal(&self, entry: SearchEntry) -> Principal { - let mut principal = Principal { - id: u32::MAX, - ..Default::default() - }; + let mut principal = Principal::default(); for (attr, value) in entry.attrs { - if let Some(idx) = self.attr_id.iter().position(|a| a == &attr) { - if principal.id == u32::MAX || idx == 0 { - if let Ok(id) = value.into_iter().next().unwrap_or_default().parse() { - principal.id = id; - } - } - } else if self.attr_name.contains(&attr) { + if self.attr_name.contains(&attr) { principal.name = value.into_iter().next().unwrap_or_default(); } else if self.attr_secret.contains(&attr) { principal.secrets.extend(value); diff --git a/crates/directory/src/ldap/mod.rs b/crates/directory/src/ldap/mod.rs index 0a09ed8d..a71c454d 100644 --- a/crates/directory/src/ldap/mod.rs +++ b/crates/directory/src/ldap/mod.rs @@ -16,10 +16,8 @@ pub struct LdapDirectory { #[derive(Debug, Default)] pub struct LdapMappings { base_dn: String, - filter_login: LdapFilter, filter_name: LdapFilter, filter_email: LdapFilter, - filter_id: LdapFilter, filter_verify: LdapFilter, filter_expand: LdapFilter, filter_domains: LdapFilter, @@ -29,7 +27,6 @@ pub struct LdapMappings { attr_description: Vec, attr_secret: Vec, attr_groups: Vec, - attr_id: Vec, attr_email_address: Vec, attr_quota: Vec, attrs_principal: Vec, diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 0a821558..6f17eb4e 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -17,7 +17,6 @@ pub mod sql; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Principal { - pub id: u32, pub name: String, pub secrets: Vec, pub typ: Type, @@ -49,11 +48,9 @@ pub enum DirectoryError { #[async_trait::async_trait] pub trait Directory: Sync + Send { async fn authenticate(&self, credentials: &Credentials) -> Result>; - async fn principal_by_name(&self, name: &str) -> Result>; - async fn principal_by_id(&self, id: u32) -> Result>; - async fn member_of(&self, principal: &Principal) -> Result>; - async fn emails_by_id(&self, id: u32) -> Result>; - async fn ids_by_email(&self, email: &str) -> Result>; + async fn principal(&self, name: &str) -> Result>; + async fn emails_by_name(&self, name: &str) -> Result>; + async fn names_by_email(&self, email: &str) -> Result>; async fn is_local_domain(&self, domain: &str) -> crate::Result; async fn rcpt(&self, address: &str) -> crate::Result; async fn vrfy(&self, address: &str) -> Result>; @@ -103,14 +100,6 @@ impl PartialEq for Lookup { impl Eq for Lookup {} impl Principal { - pub fn id(&self) -> u32 { - self.id - } - - pub fn has_id(&self) -> bool { - self.id != u32::MAX - } - pub fn name(&self) -> &str { &self.name } diff --git a/crates/directory/src/memory/config.rs b/crates/directory/src/memory/config.rs index 25312043..8d6c16c2 100644 --- a/crates/directory/src/memory/config.rs +++ b/crates/directory/src/memory/config.rs @@ -15,43 +15,43 @@ impl MemoryDirectory { let mut directory = MemoryDirectory::default(); for lookup_id in config.sub_keys((prefix.as_str(), "users")) { - let id = directory.principals.len() as u32; let name = config .value_require((prefix.as_str(), "users", lookup_id, "name"))? .to_string(); - directory.names.insert(name.clone(), id); - directory.principals.push(Principal { - id, - name, - secrets: config - .values((prefix.as_str(), "users", lookup_id, "secret")) - .map(|(_, v)| v.to_string()) - .collect(), - typ: Type::Individual, - description: config - .value((prefix.as_str(), "users", lookup_id, "description")) - .map(|v| v.to_string()), - quota: config - .property((prefix.as_str(), "users", lookup_id, "quota"))? - .unwrap_or(0), - member_of: config - .values((prefix.as_str(), "users", lookup_id, "member-of")) - .map(|(_, v)| v.to_string()) - .collect(), - }); + directory.principals.insert( + name.clone(), + Principal { + name: name.clone(), + secrets: config + .values((prefix.as_str(), "users", lookup_id, "secret")) + .map(|(_, v)| v.to_string()) + .collect(), + typ: Type::Individual, + description: config + .value((prefix.as_str(), "users", lookup_id, "description")) + .map(|v| v.to_string()), + quota: config + .property((prefix.as_str(), "users", lookup_id, "quota"))? + .unwrap_or(0), + member_of: config + .values((prefix.as_str(), "users", lookup_id, "member-of")) + .map(|(_, v)| v.to_string()) + .collect(), + }, + ); let mut emails = Vec::new(); for (pos, (_, email)) in config .values((prefix.as_str(), "users", lookup_id, "email")) .enumerate() { directory - .emails_to_ids + .emails_to_names .entry(email.to_string()) .or_default() .push(if pos > 0 { - EmailType::Alias(id) + EmailType::Alias(name.clone()) } else { - EmailType::Primary(id) + EmailType::Primary(name.clone()) }); if let Some((_, domain)) = email.rsplit_once('@') { @@ -66,40 +66,40 @@ impl MemoryDirectory { } for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) { directory - .emails_to_ids + .emails_to_names .entry(email.to_lowercase()) .or_default() - .push(EmailType::List(id)); + .push(EmailType::List(name.clone())); if let Some((_, domain)) = email.rsplit_once('@') { directory.domains.insert(domain.to_lowercase()); } emails.push(EmailType::List(email.to_lowercase())); } - directory.ids_to_email.insert(id, emails); + directory.names_to_email.insert(name, emails); } for lookup_id in config.sub_keys((prefix.as_str(), "groups")) { - let id = directory.principals.len() as u32; let name = config .value_require((prefix.as_str(), "groups", lookup_id, "name"))? .to_string(); - directory.names.insert(name.clone(), id); - directory.principals.push(Principal { - id, - name, - secrets: vec![], - typ: Type::Group, - description: config - .value((prefix.as_str(), "groups", lookup_id, "description")) - .map(|v| v.to_string()), - quota: config - .property((prefix.as_str(), "groups", lookup_id, "quota"))? - .unwrap_or(0), - member_of: config - .values((prefix.as_str(), "groups", lookup_id, "member-of")) - .map(|(_, v)| v.to_string()) - .collect(), - }); + directory.principals.insert( + name.clone(), + Principal { + name, + secrets: vec![], + typ: Type::Group, + description: config + .value((prefix.as_str(), "groups", lookup_id, "description")) + .map(|v| v.to_string()), + quota: config + .property((prefix.as_str(), "groups", lookup_id, "quota"))? + .unwrap_or(0), + member_of: config + .values((prefix.as_str(), "groups", lookup_id, "member-of")) + .map(|(_, v)| v.to_string()) + .collect(), + }, + ); } directory diff --git a/crates/directory/src/memory/lookup.rs b/crates/directory/src/memory/lookup.rs index 8be00063..79ba585d 100644 --- a/crates/directory/src/memory/lookup.rs +++ b/crates/directory/src/memory/lookup.rs @@ -15,41 +15,19 @@ impl Directory for MemoryDirectory { Credentials::OAuthBearer { token } => (token, token), Credentials::XOauth2 { username, secret } => (username, secret), }; - match self - .names - .get(username) - .and_then(|id| self.principals.get(*id as usize)) - { + match self.principals.get(username) { Some(principal) if principal.verify_secret(secret).await => Ok(Some(principal.clone())), _ => Ok(None), } } - async fn principal_by_name(&self, name: &str) -> crate::Result> { - Ok(self - .names - .get(name) - .and_then(|id| self.principals.get(*id as usize)) - .cloned()) + async fn principal(&self, name: &str) -> crate::Result> { + Ok(self.principals.get(name).cloned()) } - async fn principal_by_id(&self, id: u32) -> crate::Result> { - Ok(self.principals.get(id as usize).cloned()) - } - - async fn member_of(&self, principal: &Principal) -> crate::Result> { - let mut result = Vec::with_capacity(principal.member_of.len()); - for member in &principal.member_of { - if let Some(id) = self.names.get(member) { - result.push(*id); - } - } - Ok(result) - } - - async fn emails_by_id(&self, id: u32) -> crate::Result> { + async fn emails_by_name(&self, name: &str) -> crate::Result> { let mut result = Vec::new(); - if let Some(emails) = self.ids_to_email.get(&id) { + if let Some(emails) = self.names_to_email.get(name) { for email in emails { match email { EmailType::Primary(email) | EmailType::Alias(email) => { @@ -63,21 +41,24 @@ impl Directory for MemoryDirectory { Ok(result) } - async fn ids_by_email(&self, address: &str) -> crate::Result> { + async fn names_by_email(&self, address: &str) -> crate::Result> { Ok(self - .emails_to_ids + .emails_to_names .get(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) .or_else(|| { if self.opt.catch_all { - self.emails_to_ids.get(&to_catch_all_address(address)) + self.emails_to_names.get(&to_catch_all_address(address)) } else { None } }) - .map(|ids| { - ids.iter() + .map(|names| { + names + .iter() .map(|t| match t { - EmailType::Primary(id) | EmailType::Alias(id) | EmailType::List(id) => *id, + EmailType::Primary(name) + | EmailType::Alias(name) + | EmailType::List(name) => name.to_string(), }) .collect::>() }) @@ -86,7 +67,7 @@ impl Directory for MemoryDirectory { async fn rcpt(&self, address: &str) -> crate::Result { Ok(self - .emails_to_ids + .emails_to_names .contains_key(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) || (self.opt.catch_all && self.domains.contains(&to_catch_all_address(address)))) } @@ -94,7 +75,7 @@ impl Directory for MemoryDirectory { async fn vrfy(&self, address: &str) -> crate::Result> { let mut result = Vec::new(); let address = unwrap_subaddress(address, self.opt.subaddressing); - for (key, value) in &self.emails_to_ids { + for (key, value) in &self.emails_to_names { if key.contains(address.as_ref()) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) { @@ -107,11 +88,11 @@ impl Directory for MemoryDirectory { async fn expn(&self, address: &str) -> crate::Result> { let mut result = Vec::new(); let address = unwrap_subaddress(address, self.opt.subaddressing); - for (key, value) in &self.emails_to_ids { + for (key, value) in &self.emails_to_names { if key == address.as_ref() { for item in value { - if let EmailType::List(id) = item { - for addr in self.ids_to_email.get(id).unwrap() { + if let EmailType::List(name) = item { + for addr in self.names_to_email.get(name).unwrap() { if let EmailType::Primary(addr) = addr { result.push(addr.clone()) } diff --git a/crates/directory/src/memory/mod.rs b/crates/directory/src/memory/mod.rs index 15641e53..926743c6 100644 --- a/crates/directory/src/memory/mod.rs +++ b/crates/directory/src/memory/mod.rs @@ -7,16 +7,15 @@ pub mod lookup; #[derive(Default)] pub struct MemoryDirectory { - principals: Vec, - names: AHashMap, - emails_to_ids: AHashMap>>, - ids_to_email: AHashMap>>, + principals: AHashMap, + emails_to_names: AHashMap>, + names_to_email: AHashMap>, domains: AHashSet, opt: DirectoryOptions, } -enum EmailType { - Primary(T), - Alias(T), - List(T), +enum EmailType { + Primary(String), + Alias(String), + List(String), } diff --git a/crates/directory/src/smtp/lookup.rs b/crates/directory/src/smtp/lookup.rs index 6004bd14..fd69edb0 100644 --- a/crates/directory/src/smtp/lookup.rs +++ b/crates/directory/src/smtp/lookup.rs @@ -14,24 +14,16 @@ impl Directory for SmtpDirectory { self.pool.get().await?.authenticate(credentials).await } - async fn principal_by_name(&self, _name: &str) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "principal_by_name")) + async fn principal(&self, _name: &str) -> crate::Result> { + Err(DirectoryError::unsupported("smtp", "principal")) } - async fn principal_by_id(&self, _id: u32) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "principal_by_id")) + async fn emails_by_name(&self, _: &str) -> crate::Result> { + Err(DirectoryError::unsupported("smtp", "emails_by_name")) } - async fn member_of(&self, _principal: &Principal) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "member_of")) - } - - async fn emails_by_id(&self, _id: u32) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "emails_by_id")) - } - - async fn ids_by_email(&self, _address: &str) -> crate::Result> { - Err(DirectoryError::unsupported("smtp", "ids_by_email")) + async fn names_by_email(&self, _address: &str) -> crate::Result> { + Err(DirectoryError::unsupported("smtp", "names_by_email")) } async fn rcpt(&self, address: &str) -> crate::Result { diff --git a/crates/directory/src/sql/config.rs b/crates/directory/src/sql/config.rs index 9b699a39..924afb05 100644 --- a/crates/directory/src/sql/config.rs +++ b/crates/directory/src/sql/config.rs @@ -31,18 +31,10 @@ impl SqlDirectory { .map_err(|err| format!("Failed to create connection pool for {address:?}: {err}"))?; let mappings = SqlMappings { - query_login: config - .value((&prefix, "query.login")) - .unwrap_or_default() - .to_string(), query_name: config .value((&prefix, "query.name")) .unwrap_or_default() .to_string(), - query_id: config - .value((&prefix, "query.id")) - .unwrap_or_default() - .to_string(), query_members: config .value((&prefix, "query.members")) .unwrap_or_default() @@ -79,10 +71,6 @@ impl SqlDirectory { .value((&prefix, "columns.secret")) .unwrap_or_default() .to_string(), - column_id: config - .value((&prefix, "columns.id")) - .unwrap_or_default() - .to_string(), column_quota: config .value((&prefix, "columns.quota")) .unwrap_or_default() diff --git a/crates/directory/src/sql/lookup.rs b/crates/directory/src/sql/lookup.rs index dfbde816..5c21fd6d 100644 --- a/crates/directory/src/sql/lookup.rs +++ b/crates/directory/src/sql/lookup.rs @@ -17,75 +17,54 @@ impl Directory for SqlDirectory { Credentials::XOauth2 { username, secret } => (username, secret), }; - if let Some(row) = sqlx::query(&self.mappings.query_login) - .bind(username) - .fetch_optional(&self.pool) - .await? - { - match self.mappings.row_to_principal(row) { - Ok(principal) if principal.verify_secret(secret).await => Ok(Some(principal)), - Ok(_) => Ok(None), - Err(err) => Err(err), - } - } else { - Ok(None) + match self.principal(&username).await { + Ok(Some(principal)) if principal.verify_secret(secret).await => Ok(Some(principal)), + Ok(_) => Ok(None), + Err(err) => Err(err), } } - async fn principal_by_name(&self, name: &str) -> crate::Result> { + async fn principal(&self, name: &str) -> crate::Result> { if let Some(row) = sqlx::query(&self.mappings.query_name) .bind(name) .fetch_optional(&self.pool) .await? { - self.mappings.row_to_principal(row).map(Some) + // Map row to principal + let mut principal = self.mappings.row_to_principal(row)?; + + // Obtain members + principal.member_of = sqlx::query_scalar::<_, String>(&self.mappings.query_members) + .bind(name) + .fetch_all(&self.pool) + .await?; + + Ok(Some(principal)) } else { Ok(None) } } - async fn principal_by_id(&self, id: u32) -> crate::Result> { - if let Some(row) = sqlx::query(&self.mappings.query_id) - .bind(id as i64) - .fetch_optional(&self.pool) - .await? - { - self.mappings.row_to_principal(row).map(Some) - } else { - Ok(None) - } - } - - async fn member_of(&self, principal: &Principal) -> crate::Result> { - sqlx::query_scalar::<_, i64>(&self.mappings.query_members) - .bind(principal.id as i64) - .fetch_all(&self.pool) - .await - .map(|ids| ids.into_iter().map(|id| id as u32).collect()) - .map_err(Into::into) - } - - async fn emails_by_id(&self, id: u32) -> crate::Result> { + async fn emails_by_name(&self, name: &str) -> crate::Result> { sqlx::query_scalar::<_, String>(&self.mappings.query_emails) - .bind(id as i64) + .bind(name) .fetch_all(&self.pool) .await .map_err(Into::into) } - async fn ids_by_email(&self, address: &str) -> crate::Result> { - match sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients) + async fn names_by_email(&self, address: &str) -> crate::Result> { + match sqlx::query_scalar::<_, String>(&self.mappings.query_recipients) .bind(unwrap_subaddress(address, self.opt.subaddressing).as_ref()) .fetch_all(&self.pool) .await { - Ok(ids) if !ids.is_empty() => Ok(ids.into_iter().map(|id| id as u32).collect()), + Ok(ids) if !ids.is_empty() => Ok(ids), Ok(_) if self.opt.catch_all => { - sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients) + sqlx::query_scalar::<_, String>(&self.mappings.query_recipients) .bind(to_catch_all_address(address)) .fetch_all(&self.pool) .await - .map(|ids| ids.into_iter().map(|id| id as u32).collect()) .map_err(Into::into) } Ok(_) => Ok(vec![]), @@ -151,17 +130,12 @@ impl Directory for SqlDirectory { impl SqlMappings { pub fn row_to_principal(&self, row: AnyRow) -> crate::Result { - let mut principal = Principal { - id: u32::MAX, - ..Default::default() - }; + let mut principal = Principal::default(); for col in row.columns() { let idx = col.ordinal(); let name = col.name(); - if name.eq_ignore_ascii_case(&self.column_id) { - principal.id = row.try_get::(idx)? as u32; - } else if name.eq_ignore_ascii_case(&self.column_name) { + if name.eq_ignore_ascii_case(&self.column_name) { principal.name = row.try_get::(idx)?; } else if name.eq_ignore_ascii_case(&self.column_secret) { if let Ok(secret) = row.try_get::(idx) { diff --git a/crates/directory/src/sql/mod.rs b/crates/directory/src/sql/mod.rs index 1700539b..914e8a27 100644 --- a/crates/directory/src/sql/mod.rs +++ b/crates/directory/src/sql/mod.rs @@ -13,9 +13,7 @@ pub struct SqlDirectory { #[derive(Debug)] pub(crate) struct SqlMappings { - query_login: String, query_name: String, - query_id: String, query_members: String, query_recipients: String, query_emails: String, @@ -25,7 +23,6 @@ pub(crate) struct SqlMappings { column_name: String, column_description: String, column_secret: String, - column_id: String, column_quota: String, column_type: String, } diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index b577c6af..d7e3b089 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -9,7 +9,7 @@ use jmap::{ }; use jmap_proto::{ object::Object, - types::{acl::Acl, collection::Collection, property::Property, value::Value}, + types::{acl::Acl, collection::Collection, id::Id, property::Property, value::Value}, }; use parking_lot::Mutex; use store::query::log::{Change, Query}; @@ -50,7 +50,12 @@ impl SessionData { format!( "{}/{}", session.imap.name_shared, - session.jmap.get_account_name(account_id).await + session + .jmap + .get_account_name(account_id) + .await + .unwrap_or_default() + .unwrap_or_else(|| Id::from(account_id).to_string()) ) .into(), access_token, @@ -318,7 +323,11 @@ impl SessionData { let prefix = format!( "{}/{}", self.imap.name_shared, - self.jmap.get_account_name(account_id).await + self.jmap + .get_account_name(account_id) + .await + .unwrap_or_default() + .unwrap_or_else(|| Id::from(account_id).to_string()) ); match self .fetch_account_mailboxes(account_id, prefix.into(), &access_token) @@ -399,7 +408,11 @@ impl SessionData { format!( "{}/{}", self.imap.name_shared, - self.jmap.get_account_name(account_id).await + self.jmap + .get_account_name(account_id) + .await + .unwrap_or_default() + .unwrap_or_else(|| Id::from(account_id).to_string()) ) .into() } else { diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index f3a09adc..bf95bd40 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -74,17 +74,9 @@ impl Session { { if let Some(account_name) = data .jmap - .directory - .principal_by_id(id.document_id()) + .get_account_name(id.document_id()) .await .unwrap_or_default() - .and_then(|p| { - if p.has_name() { - Some(p.name().to_string()) - } else { - None - } - }) { let mut rights = Vec::new(); @@ -254,11 +246,22 @@ impl Session { let (acl_account_id, id) = match data .jmap .directory - .principal_by_name(arguments.identifier.as_ref().unwrap()) + .principal(arguments.identifier.as_ref().unwrap()) .await { - Ok(Some(principal)) if principal.has_id() => { - (principal.id(), Value::Id(Id::from(principal.id()))) + Ok(Some(principal)) => { + match data.jmap.get_account_id(&principal.name()).await { + Ok(account_id) => (account_id, Value::Id(Id::from(account_id))), + Err(_) => { + data.write_bytes( + StatusResponse::database_failure() + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + } + } } Ok(None) => { data.write_bytes( diff --git a/crates/jmap-proto/src/object/index.rs b/crates/jmap-proto/src/object/index.rs index 35c0660b..2c43e55d 100644 --- a/crates/jmap-proto/src/object/index.rs +++ b/crates/jmap-proto/src/object/index.rs @@ -147,8 +147,7 @@ impl IntoOperations for ObjectIndexBuilder { // Insertion build_batch(batch, self.index, &changes, true); batch.ops.push(Operation::Value { - field: Property::Value.into(), - family: 0, + class: Property::Value.into(), set: changes.serialize().into(), }); } @@ -162,8 +161,7 @@ impl IntoOperations for ObjectIndexBuilder { batch.assert_value(Property::Value, ¤t); build_batch(batch, self.index, ¤t.inner, false); batch.ops.push(Operation::Value { - field: Property::Value.into(), - family: 0, + class: Property::Value.into(), set: None, }); } @@ -370,10 +368,7 @@ fn merge_batch( for item in current_value.chunks_exact(2) { if let Some(Value::Id(id)) = item.first() { if !value.contains(&Value::Id(*id)) { - batch.ops.push(Operation::Acl { - grant_account_id: id.document_id(), - set: None, - }); + batch.ops.push(Operation::acl(id.document_id(), None)); } } } @@ -399,10 +394,10 @@ fn merge_batch( } } if add_item { - batch.ops.push(Operation::Acl { - grant_account_id: id.document_id(), - set: acl.serialize().into(), - }); + batch.ops.push(Operation::acl( + id.document_id(), + acl.serialize().into(), + )); } } } @@ -413,10 +408,10 @@ fn merge_batch( if let (Some(Value::Id(id)), Some(Value::UnsignedInt(acl))) = (item.first(), item.last()) { - batch.ops.push(Operation::Acl { - grant_account_id: id.document_id(), - set: acl.serialize().into(), - }); + batch.ops.push(Operation::acl( + id.document_id(), + acl.serialize().into(), + )); } } } @@ -424,10 +419,7 @@ fn merge_batch( // Remove all ACLs for item in current_values.chunks_exact(2) { if let Some(Value::Id(id)) = item.first() { - batch.ops.push(Operation::Acl { - grant_account_id: id.document_id(), - set: None, - }); + batch.ops.push(Operation::acl(id.document_id(), None)); } } } @@ -455,8 +447,7 @@ fn merge_batch( if has_changes { batch.ops.push(Operation::Value { - field: Property::Value.into(), - family: 0, + class: Property::Value.into(), set: current.serialize().into(), }); } @@ -561,10 +552,10 @@ fn build_batch( if let (Some(Value::Id(id)), Some(Value::UnsignedInt(acl))) = (item.first(), item.last()) { - batch.ops.push(Operation::Acl { - grant_account_id: id.document_id(), - set: if set { acl.serialize().into() } else { None }, - }); + batch.ops.push(Operation::acl( + id.document_id(), + if set { acl.serialize().into() } else { None }, + )); } } } diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index be3b4cf8..b1dacec1 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -30,7 +30,7 @@ pub mod sieve; use std::slice::Iter; use store::{ - write::{DeserializeFrom, SerializeInto, ToBitmaps}, + write::{DeserializeFrom, SerializeInto, ToBitmaps, ValueClass}, Deserialize, Serialize, }; use utils::{ @@ -261,3 +261,12 @@ impl DeserializeFrom for Value { } } } + +impl From for ValueClass { + fn from(value: Property) -> Self { + ValueClass::Property { + field: value.into(), + family: 0, + } + } +} diff --git a/crates/jmap-proto/src/types/collection.rs b/crates/jmap-proto/src/types/collection.rs index 0d1b4a90..99dcd6e1 100644 --- a/crates/jmap-proto/src/types/collection.rs +++ b/crates/jmap-proto/src/types/collection.rs @@ -37,7 +37,8 @@ pub enum Collection { EmailSubmission = 4, SieveScript = 5, PushSubscription = 6, - None = 7, + Principal = 7, + None = 8, } impl From for Collection { @@ -50,6 +51,7 @@ impl From for Collection { 4 => Collection::EmailSubmission, 5 => Collection::SieveScript, 6 => Collection::PushSubscription, + 7 => Collection::Principal, _ => Collection::None, } } @@ -65,6 +67,7 @@ impl From for Collection { 4 => Collection::EmailSubmission, 5 => Collection::SieveScript, 6 => Collection::PushSubscription, + 7 => Collection::Principal, _ => Collection::None, } } @@ -107,6 +110,7 @@ impl Display for Collection { Collection::Identity => write!(f, "identity"), Collection::EmailSubmission => write!(f, "emailSubmission"), Collection::SieveScript => write!(f, "sieveScript"), + Collection::Principal => write!(f, "principal"), Collection::None => write!(f, ""), } } diff --git a/crates/jmap/src/api/config.rs b/crates/jmap/src/api/config.rs index 20a8e08e..970dde37 100644 --- a/crates/jmap/src/api/config.rs +++ b/crates/jmap/src/api/config.rs @@ -137,6 +137,10 @@ impl crate::Config { web_socket_timeout: settings.property_or_static("jmap.web-socket.timeout", "10m")?, web_socket_heartbeat: settings.property_or_static("jmap.web-socket.heartbeat", "1m")?, push_max_total: settings.property_or_static("jmap.push.max-total", "100")?, + superusers_group_name: settings + .value("jmap.superusers-group") + .unwrap_or("superusers") + .to_string(), }; config.add_capabilites(settings); Ok(config) diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index f1fa5f67..07513ca2 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -434,14 +434,14 @@ impl HtmlResponse { impl ToHttpResponse for Response { fn into_http_response(self) -> HttpResponse { - //let _ = println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); + //let c = println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); JsonResponse::new(self).into_http_response() } } impl ToHttpResponse for Session { fn into_http_response(self) -> HttpResponse { - //let _ = println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); + //let c = println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); JsonResponse::new(self).into_http_response() } } diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 53d61edc..573d43b2 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -211,7 +211,7 @@ impl JMAP { set::RequestArguments::Identity => { access_token.assert_is_member(req.account_id)?; - self.identity_set(req).await?.into() + self.identity_set(req, access_token).await?.into() } set::RequestArguments::EmailSubmission(arguments) => { access_token.assert_is_member(req.account_id)?; diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index e266a5cf..92d344cd 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -192,7 +192,10 @@ impl JMAP { session.add_account( (*id).into(), - self.get_account_name(*id).await, + self.get_account_name(*id) + .await + .unwrap_or_default() + .unwrap_or_else(|| Id::from(*id).to_string()), is_personal, is_readonly, Some(&[Capability::Core, Capability::Mail, Capability::WebSocket]), @@ -201,15 +204,6 @@ impl JMAP { Ok(session) } - - pub async fn get_account_name(&self, account_id: u32) -> String { - self.directory - .principal_by_id(account_id) - .await - .unwrap_or_default() - .map(|p| p.name) - .unwrap_or_else(|| Id::from(account_id).to_string()) - } } impl crate::Config { diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 9057c240..7ef3bba8 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -420,17 +420,9 @@ impl JMAP { (item.first(), item.last()) { if let Some(account_name) = self - .directory - .principal_by_id(id.document_id()) + .get_account_name(id.document_id()) .await .unwrap_or_default() - .and_then(|p| { - if p.has_name() { - Some(p.name().to_string()) - } else { - None - } - }) { acl_obj.append( Property::_T(account_name), @@ -501,9 +493,18 @@ impl JMAP { async fn map_acl_accounts(&self, mut acl_set: Vec) -> Result, SetError> { for item in &mut acl_set { if let Value::Text(account_name) = item { - match self.directory.principal_by_name(account_name).await { - Ok(Some(principal)) if principal.has_id() => { - *item = Value::Id(principal.id().into()); + match self.directory.principal(account_name).await { + Ok(Some(_)) => { + *item = Value::Id( + self.get_account_id(account_name) + .await + .map_err(|_| { + SetError::forbidden() + .with_property(Property::Acl) + .with_description("Temporary server failure during lookup") + })? + .into(), + ); } Ok(None) => { return Err(SetError::invalid_properties() diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 31ad6dd1..25c76fb4 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -28,12 +28,19 @@ use std::{ }; use hyper::header; -use jmap_proto::error::request::RequestError; +use jmap_proto::{ + error::{method::MethodError, request::RequestError}, + types::collection::Collection, +}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; +use store::{ + write::{key::KeySerializer, BatchBuilder, Operation, ValueClass}, + CustomValueKey, Serialize, +}; use utils::{listener::limiter::InFlight, map::ttl_dashmap::TtlMap}; -use crate::JMAP; +use crate::{JMAP, SUPERUSER_ID}; use super::{rate_limit::RemoteAddress, AccessToken}; @@ -146,6 +153,119 @@ impl JMAP { } } + pub async fn get_account_id(&self, name: &str) -> Result { + let mut try_count = 0; + + loop { + // Try to obtain ID + match self + .store + .get_value::(CustomValueKey { + value: KeySerializer::new(name.len() + std::mem::size_of::() + 1) + .write(u32::MAX) + .write(0u8) + .write(name) + .finalize(), + }) + .await + { + Ok(Some(id)) => return Ok(id), + Ok(None) => {} + Err(err) => { + tracing::error!(event = "error", + context = "store", + account_name = name, + error = ?err, + "Failed to retrieve account id"); + return Err(MethodError::ServerPartialFail); + } + } + + // Assign new ID + let account_id = self + .assign_document_id(u32::MAX, Collection::Principal) + .await?; + + // Serialize key + let key = KeySerializer::new(name.len() + std::mem::size_of::() + 1) + .write(u32::MAX) + .write(0u8) + .write(name) + .finalize(); + + // Write account ID + let mut batch = BatchBuilder::new(); + batch + .with_account_id(u32::MAX) + .with_collection(Collection::Principal) + .create_document(account_id) + .assert_value(ValueClass::Custom { bytes: key.clone() }, ()) + .op(Operation::Value { + class: ValueClass::Custom { bytes: key }, + set: account_id.serialize().into(), + }) + .op(Operation::Value { + class: ValueClass::Custom { + bytes: KeySerializer::new(std::mem::size_of::() * 2 + 1) + .write(u32::MAX) + .write(1u8) + .write(account_id) + .finalize(), + }, + set: name.serialize().into(), + }); + + match self.store.write(batch.build()).await { + Ok(_) => { + return Ok(account_id); + } + Err(store::Error::AssertValueFailed) if try_count < 3 => { + try_count += 1; + continue; + } + Err(err) => { + tracing::error!(event = "error", + context = "store", + error = ?err, + "Failed to generate account id"); + return Err(MethodError::ServerPartialFail); + } + } + } + } + + pub async fn map_member_of(&self, names: Vec) -> Result, MethodError> { + let mut ids = Vec::with_capacity(names.len()); + for name in names { + if !name.eq_ignore_ascii_case(&self.config.superusers_group_name) { + ids.push(self.get_account_id(&name).await?); + } else { + ids.push(SUPERUSER_ID); + } + } + Ok(ids) + } + + pub async fn get_account_name(&self, account_id: u32) -> Result, MethodError> { + self.store + .get_value::(CustomValueKey { + value: KeySerializer::new(std::mem::size_of::() * 2 + 1) + .write(u32::MAX) + .write(1u8) + .write(account_id) + .finalize(), + }) + .await + .map_err(|err| { + tracing::error!(event = "error", + context = "store", + account_id = account_id, + error = ?err, + "Failed to retrieve account name"); + MethodError::ServerPartialFail + }) + } + pub fn build_remote_addr( &self, req: &hyper::Request, @@ -174,42 +294,42 @@ impl JMAP { }) .await .ok()??; - if !principal.has_id() { - tracing::warn!( - context = "authenticate_plain", - username = username, - "Principal has no ID." - ); - return None; - } else if !principal.has_name() { + if !principal.has_name() { principal.name = username.to_string(); } // Obtain groups - let member_of = self - .directory - .member_of(&principal) - .await - .unwrap_or_default(); - - // Create access token - self.update_access_token(AccessToken::new(principal).with_member_of(member_of)) + if let (Ok(account_id), Ok(member_of)) = ( + self.get_account_id(&principal.name).await, + self.map_member_of(std::mem::take(&mut principal.member_of)) + .await, + ) { + // Create access token + self.update_access_token( + AccessToken::new(principal, account_id).with_member_of(member_of), + ) .await + } else { + None + } } - pub async fn get_access_token(&self, id: u32) -> Option { - let mut principal = self.directory.principal_by_id(id).await.ok()??; - if !principal.has_id() { - principal.id = id; - } - // Obtain groups - let member_of = self - .directory - .member_of(&principal) - .await - .unwrap_or_default(); + pub async fn get_access_token(&self, account_id: u32) -> Option { + let name = self.get_account_name(account_id).await.ok()??; + let mut principal = self.directory.principal(&name).await.ok()??; - // Create access token - self.update_access_token(AccessToken::new(principal).with_member_of(member_of)) + // Obtain groups + if let (Ok(account_id), Ok(member_of)) = ( + self.get_account_id(&principal.name).await, + self.map_member_of(std::mem::take(&mut principal.member_of)) + .await, + ) { + // Create access token + self.update_access_token( + AccessToken::new(principal, account_id).with_member_of(member_of), + ) .await + } else { + None + } } } diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs index c88a1ae3..ee5f8de0 100644 --- a/crates/jmap/src/auth/mod.rs +++ b/crates/jmap/src/auth/mod.rs @@ -57,9 +57,9 @@ pub struct AccessToken { } impl AccessToken { - pub fn new(principal: Principal) -> Self { + pub fn new(principal: Principal, primary_id: u32) -> Self { Self { - primary_id: principal.id, + primary_id, member_of: Vec::new(), access_to: Vec::new(), name: principal.name, @@ -97,7 +97,6 @@ impl AccessToken { pub fn is_member(&self, account_id: u32) -> bool { self.primary_id == account_id || self.member_of.contains(&account_id) - || self.primary_id == SUPERUSER_ID || self.member_of.contains(&SUPERUSER_ID) } @@ -106,7 +105,7 @@ impl AccessToken { } pub fn is_super_user(&self) -> bool { - self.primary_id == SUPERUSER_ID || self.member_of.contains(&SUPERUSER_ID) + self.member_of.contains(&SUPERUSER_ID) } pub fn is_shared(&self, account_id: u32) -> bool { diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 121c9257..457a5227 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -182,9 +182,14 @@ impl JMAP { client_id: &str, with_refresh_token: bool, ) -> Result { + let account_name = self + .get_account_name(account_id) + .await + .map_err(|_| "Temporary lookup error")? + .ok_or("Account no longer exists")?; let password_hash = self .directory - .principal_by_id(account_id) + .principal(&account_name) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? @@ -300,9 +305,14 @@ impl JMAP { } // Optain password hash + let account_name = self + .get_account_name(account_id) + .await + .map_err(|_| "Temporary lookup error")? + .ok_or("Account no longer exists")?; let password_hash = self .directory - .principal_by_id(account_id) + .principal(&account_name) .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index f9d6b6fb..837bb7d6 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -56,7 +56,7 @@ use store::{ fts::term_index::TokenIndex, write::{ assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, DeserializeFrom, SerializeInto, - ToBitmaps, F_BITMAP, F_CLEAR, F_VALUE, + ToBitmaps, ValueClass, F_BITMAP, F_CLEAR, F_VALUE, }, BlobKind, Serialize, ValueKey, }; @@ -1362,7 +1362,13 @@ impl< let property = u8::from(property); batch - .assert_value(property, &self.current) + .assert_value( + ValueClass::Property { + field: property, + family: 0, + }, + &self.current, + ) .value(property, self.current.inner, F_VALUE); for added in self.added { batch.value(property, added, F_BITMAP); diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index d966c140..65f99546 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -34,12 +34,13 @@ use jmap_proto::{ }; use store::write::{log::ChangeLogBuilder, BatchBuilder, F_CLEAR, F_VALUE}; -use crate::JMAP; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn identity_set( &self, mut request: SetRequest, + access_token: &AccessToken, ) -> Result { let account_id = request.account_id.document_id(); let mut identity_ids = self @@ -72,9 +73,15 @@ impl JMAP { // Validate email address if let Value::Text(email) = identity.get(&Property::Email) { + let account_name = if access_token.primary_id == account_id { + access_token.name.clone() + } else { + self.get_account_name(account_id).await?.unwrap_or_default() + }; + if !self .directory - .emails_by_id(account_id) + .emails_by_name(&account_name) .await .unwrap_or_default() .contains(email) diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 399daadd..b64bf230 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -150,6 +150,7 @@ pub struct Config { pub oauth_expiry_refresh_token_renew: u64, pub oauth_max_auth_attempts: u32, + pub superusers_group_name: String, pub capabilities: BaseCapabilities, } @@ -557,7 +558,7 @@ impl JMAP { access_token.quota as i64 } else { self.directory - .principal_by_id(account_id) + .principal(&access_token.name) .await .map_err(|err| { tracing::error!( diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index e187b711..c6263ad1 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -750,7 +750,12 @@ impl JMAP { .get_document_ids(account_id, Collection::Mailbox) .await? .unwrap_or_default(); - if !mailbox_ids.is_empty() || account_id == SUPERUSER_ID { + if !mailbox_ids.is_empty() { + return Ok(mailbox_ids); + } + + #[cfg(feature = "test_mode")] + if mailbox_ids.is_empty() && account_id == SUPERUSER_ID { return Ok(mailbox_ids); } diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 0017b2f0..2508135c 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -44,31 +44,47 @@ impl JMAP { // Obtain the UIDs for each recipient let mut recipients = Vec::with_capacity(message.recipients.len()); - let mut deliver_uids = AHashMap::with_capacity(message.recipients.len()); + let mut deliver_names = AHashMap::with_capacity(message.recipients.len()); for rcpt in &message.recipients { - let uids = self.directory.ids_by_email(rcpt).await.unwrap_or_default(); - for uid in &uids { - deliver_uids.insert(*uid, (DeliveryResult::Success, rcpt)); + let names = self + .directory + .names_by_email(rcpt) + .await + .unwrap_or_default(); + for name in &names { + deliver_names.insert(name.clone(), (DeliveryResult::Success, rcpt)); } - recipients.push(uids); + recipients.push(names); } // Deliver to each recipient - for (uid, (status, rcpt)) in &mut deliver_uids { + for (name, (status, rcpt)) in &mut deliver_names { + // Obtain account id + let uid = match self.get_account_id(&name).await { + Ok(uid) => uid, + Err(_) => { + *status = DeliveryResult::TemporaryFailure { + reason: "Transient server failure.".into(), + }; + continue; + } + }; + // Check if there is an active sieve script - let result = match self.sieve_script_get_active(*uid).await { + let result = match self.sieve_script_get_active(uid).await { Ok(Some(active_script)) => { self.sieve_script_ingest( &raw_message, &message.sender_address, rcpt, - *uid, + uid, + name, active_script, ) .await } Ok(None) => { - let account_quota = match self.directory.principal_by_id(*uid).await { + let account_quota = match self.directory.principal(&name).await { Ok(Some(p)) => p.quota as i64, Ok(None) => 0, Err(_) => { @@ -82,7 +98,7 @@ impl JMAP { self.email_ingest(IngestEmail { raw_message: &raw_message, message: Message::parse(&raw_message), - account_id: *uid, + account_id: uid, account_quota, mailbox_ids: vec![INBOX_ID], keywords: vec![], @@ -104,7 +120,7 @@ impl JMAP { // Notify state change if ingested_message.change_id != u64::MAX { self.broadcast_state_change( - StateChange::new(*uid) + StateChange::new(uid) .with_change(TypeState::EmailDelivery, ingested_message.change_id) .with_change(TypeState::Email, ingested_message.change_id) .with_change(TypeState::Mailbox, ingested_message.change_id) @@ -137,11 +153,11 @@ impl JMAP { // Build result recipients .into_iter() - .map(|uids| { - match uids.len() { + .map(|names| { + match names.len() { 1 => { // Delivery to single recipient - deliver_uids.get(&uids[0]).unwrap().0.clone() + deliver_names.get(&names[0]).unwrap().0.clone() } 0 => { // Something went wrong @@ -153,8 +169,8 @@ impl JMAP { // Delivery to list, count number of successes and failures let mut success = 0; let mut temp_failures = 0; - for uid in uids { - match deliver_uids.get(&uid).unwrap().0 { + for uid in names { + match deliver_names.get(&uid).unwrap().0 { DeliveryResult::Success => success += 1, DeliveryResult::TemporaryFailure { .. } => temp_failures += 1, DeliveryResult::PermanentFailure { .. } => {} diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index 511ef236..f4d8c1e0 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -55,6 +55,7 @@ impl JMAP { envelope_from: &str, envelope_to: &str, account_id: u32, + account_name: &str, mut active_script: ActiveScript, ) -> Result { // Parse message @@ -79,7 +80,7 @@ impl JMAP { // Obtain mail from address let mail_from = if let Some(email) = self .directory - .emails_by_id(account_id) + .emails_by_name(account_name) .await .unwrap_or_default() .into_iter() @@ -93,10 +94,17 @@ impl JMAP { // Set account address instance.set_user_address(&mail_from); - // Set account name - if let Ok(Some(p)) = self.directory.principal_by_id(account_id).await { - instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); - } + // Set account name and obtain quota + let account_quota = match self.directory.principal(account_name).await { + Ok(Some(p)) => { + instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); + p.quota as i64 + } + Ok(None) => 0, + Err(_) => { + return Err(IngestError::Temporary); + } + }; // Set envelope instance.set_envelope(Envelope::From, envelope_from); @@ -412,13 +420,6 @@ impl JMAP { } // Deliver messages - let account_quota = match self.directory.principal_by_id(account_id).await { - Ok(Some(p)) => p.quota as i64, - Ok(None) => 0, - Err(_) => { - return Err(IngestError::Temporary); - } - }; let mut last_temp_error = None; let mut has_delivered = false; for (message_id, sieve_message) in messages.into_iter().enumerate() { diff --git a/crates/store/src/backend/foundationdb/read.rs b/crates/store/src/backend/foundationdb/read.rs index 7b5f9f3b..223974ba 100644 --- a/crates/store/src/backend/foundationdb/read.rs +++ b/crates/store/src/backend/foundationdb/read.rs @@ -434,11 +434,11 @@ impl Store { ); } SUBSPACE_VALUES => { - // Ignore lastId counter - if key.len() == 4 + // Ignore lastId counter and ID mappings + if (key.len() == 4 && value.len() == 8 && u32::deserialize(key).is_ok() - && u64::deserialize(value).is_ok() + && u64::deserialize(value).is_ok()) || &key[0..4] == u32::MAX.to_be_bytes() { { continue; } @@ -446,15 +446,17 @@ impl Store { panic!("Table values is not empty: {key:?} {value:?}"); } SUBSPACE_BITMAPS => { - panic!( - "Table bitmaps is not empty, account {}, collection {}, family {}, field {}, key {:?}: {:?}", - u32::from_be_bytes(key[0..4].try_into().unwrap()), - key[4], - key[5], - key[6], - key, - value - ); + if &key[0..4] != u32::MAX.to_be_bytes() { + panic!( + "Table bitmaps is not empty, account {}, collection {}, family {}, field {}, key {:?}: {:?}", + u32::from_be_bytes(key[0..4].try_into().unwrap()), + key[4], + key[5], + key[6], + key, + value + ); + } } SUBSPACE_QUOTAS => { let v = i64::from_le_bytes(value[..].try_into().unwrap()); diff --git a/crates/store/src/backend/foundationdb/write.rs b/crates/store/src/backend/foundationdb/write.rs index bdce010a..9502b81b 100644 --- a/crates/store/src/backend/foundationdb/write.rs +++ b/crates/store/src/backend/foundationdb/write.rs @@ -194,12 +194,18 @@ impl Store { field: *field, } .serialize(); - if trx - .get(&key, false) - .await - .unwrap_or_default() - .map_or(true, |bytes| !assert_value.matches(bytes.as_ref())) - { + + let matches = if let Ok(bytes) = trx.get(&key, false).await { + if let Some(bytes) = bytes { + assert_value.matches(bytes.as_ref()) + } else { + assert_value.is_none(); + } + } else { + false + }; + + if !matches { trx.cancel(); return Err(crate::Error::AssertValueFailed); } diff --git a/crates/store/src/backend/sqlite/read.rs b/crates/store/src/backend/sqlite/read.rs index 6a3fda47..8e187b63 100644 --- a/crates/store/src/backend/sqlite/read.rs +++ b/crates/store/src/backend/sqlite/read.rs @@ -373,7 +373,9 @@ impl Store { let key = row.get_ref(0).unwrap().as_bytes().unwrap(); let value = row.get_ref(1).unwrap().as_bytes().unwrap(); - panic!("Table values is not empty: {key:?} {value:?}"); + if &key[0..4] != u32::MAX.to_be_bytes() { + panic!("Table values is not empty: {key:?} {value:?}"); + } } // Indexes @@ -384,14 +386,14 @@ impl Store { let key = row.get_ref(0).unwrap().as_bytes().unwrap(); panic!( - "Table index is not empty, account {}, collection {}, document {}, property {}, value {:?}: {:?}", - u32::from_be_bytes(key[0..4].try_into().unwrap()), - key[4], - u32::from_be_bytes(key[key.len()-4..].try_into().unwrap()), - key[5], - String::from_utf8_lossy(&key[6..key.len()-4]), - key - ); + "Table index is not empty, account {}, collection {}, document {}, property {}, value {:?}: {:?}", + u32::from_be_bytes(key[0..4].try_into().unwrap()), + key[4], + u32::from_be_bytes(key[key.len()-4..].try_into().unwrap()), + key[5], + String::from_utf8_lossy(&key[6..key.len()-4]), + key + ); } // Bitmaps @@ -404,13 +406,15 @@ impl Store { while let Some(row) = rows.next().unwrap() { let key = row.get_ref(0).unwrap().as_bytes().unwrap(); - for bit_pos in 1..=16 { - let bit_value = row.get::<_, i64>(bit_pos).unwrap() as u64; - if bit_value != 0 { - panic!("Table bitmaps is not empty: {key:?} {bit_pos} {bit_value}"); + if &key[0..4] != u32::MAX.to_be_bytes() { + for bit_pos in 1..=16 { + let bit_value = row.get::<_, i64>(bit_pos).unwrap() as u64; + if bit_value != 0 { + panic!("Table bitmaps is not empty: {key:?} {bit_pos} {bit_value}"); + } } + panic!("Table bitmaps failed to purge, found key: {key:?}"); } - panic!("Table bitmaps failed to purge, found key: {key:?}"); } // Quotas diff --git a/crates/store/src/backend/sqlite/write.rs b/crates/store/src/backend/sqlite/write.rs index 72aaf903..4e180f5b 100644 --- a/crates/store/src/backend/sqlite/write.rs +++ b/crates/store/src/backend/sqlite/write.rs @@ -24,7 +24,7 @@ use rusqlite::{params, OptionalExtension, TransactionBehavior}; use crate::{ - write::{Batch, Operation}, + write::{Batch, Operation, ValueClass}, AclKey, BitmapKey, IndexKey, Key, LogKey, Serialize, Store, ValueKey, }; @@ -120,15 +120,25 @@ impl Store { bitmap_value_set = (1u64 << (index as u64 & 63)) as i64; bitmap_value_clear = (!(1u64 << (index as u64 & 63))) as i64; } - Operation::Value { family, field, set } => { - let key = ValueKey { - account_id, - collection, - document_id, - family: *family, - field: *field, - } - .serialize(); + Operation::Value { class, set } => { + let key = match class { + ValueClass::Property { field, family } => ValueKey { + account_id, + collection, + document_id, + family: *family, + field: *field, + } + .serialize(), + ValueClass::Acl { grant_account_id } => AclKey { + grant_account_id: *grant_account_id, + to_account_id: account_id, + to_collection: collection, + to_document_id: document_id, + } + .serialize(), + ValueClass::Custom { bytes } => bytes.to_vec(), + }; if let Some(value) = set { trx.prepare_cached("INSERT OR REPLACE INTO v (k, v) VALUES (?, ?)")? @@ -186,26 +196,7 @@ impl Store { .execute(params![bitmap_value_clear, &key])?; }; } - Operation::Acl { - grant_account_id, - set, - } => { - let key = AclKey { - grant_account_id: *grant_account_id, - to_account_id: account_id, - to_collection: collection, - to_document_id: document_id, - } - .serialize(); - if let Some(value) = set { - trx.prepare_cached("INSERT OR REPLACE INTO v (k, v) VALUES (?, ?)")? - .execute([&key, value])?; - } else { - trx.prepare_cached("DELETE FROM v WHERE k = ?")? - .execute([&key])?; - } - } Operation::Log { collection, change_id, @@ -222,25 +213,34 @@ impl Store { .execute([&key, set])?; } Operation::AssertValue { - field, - family, + class, assert_value, } => { - let key = ValueKey { - account_id, - collection, - document_id, - family: *family, - field: *field, - } - .serialize(); + let key = match class { + ValueClass::Property { field, family } => ValueKey { + account_id, + collection, + document_id, + family: *family, + field: *field, + } + .serialize(), + ValueClass::Acl { grant_account_id } => AclKey { + grant_account_id: *grant_account_id, + to_account_id: account_id, + to_collection: collection, + to_document_id: document_id, + } + .serialize(), + ValueClass::Custom { bytes } => bytes.to_vec(), + }; let matches = trx .prepare_cached("SELECT v FROM v WHERE k = ?")? .query_row([&key], |row| { Ok(assert_value.matches(row.get_ref(0)?.as_bytes()?)) }) .optional()? - .unwrap_or(false); + .unwrap_or_else(|| assert_value.is_none()); if !matches { return Err(crate::Error::AssertValueFailed); } diff --git a/crates/store/src/fts/builder.rs b/crates/store/src/fts/builder.rs index 25ac235e..3ddf538f 100644 --- a/crates/store/src/fts/builder.rs +++ b/crates/store/src/fts/builder.rs @@ -28,7 +28,7 @@ use utils::map::vec_map::VecMap; use crate::{ query::RawValue, - write::{BatchBuilder, IntoOperations, Operation}, + write::{BatchBuilder, IntoOperations, Operation, ValueClass}, Serialize, HASH_EXACT, HASH_STEMMED, }; @@ -150,8 +150,10 @@ impl<'x> IntoOperations for FtsIndexBuilder<'x> { } batch.ops.push(Operation::Value { - field: u8::MAX, - family: u8::MAX, + class: ValueClass::Property { + field: u8::MAX, + family: u8::MAX, + }, set: term_index.serialize().into(), }); } @@ -184,8 +186,10 @@ impl IntoOperations for TokenIndex { fn build(self, batch: &mut BatchBuilder) { self.build_index(batch, false); batch.ops.push(Operation::Value { - field: u8::MAX, - family: u8::MAX, + class: ValueClass::Property { + field: u8::MAX, + family: u8::MAX, + }, set: None, }); } @@ -195,8 +199,10 @@ impl IntoOperations for RawValue { fn build(self, batch: &mut BatchBuilder) { self.inner.build_index(batch, true); batch.ops.push(Operation::Value { - field: u8::MAX, - family: u8::MAX, + class: ValueClass::Property { + field: u8::MAX, + family: u8::MAX, + }, set: self.raw.into(), }); } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index cffa92a4..882e43f3 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -136,6 +136,11 @@ pub struct ValueKey { pub field: u8, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CustomValueKey { + pub value: Vec, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct AclKey { pub grant_account_id: u32, diff --git a/crates/store/src/write/assert.rs b/crates/store/src/write/assert.rs index e499a882..a4f2755e 100644 --- a/crates/store/src/write/assert.rs +++ b/crates/store/src/write/assert.rs @@ -34,6 +34,7 @@ pub enum AssertValue { U32(u32), U64(u64), Hash(u64), + None, } impl HashedValue { @@ -46,6 +47,12 @@ pub trait ToAssertValue { fn to_assert_value(&self) -> AssertValue; } +impl ToAssertValue for () { + fn to_assert_value(&self) -> AssertValue { + AssertValue::None + } +} + impl ToAssertValue for u64 { fn to_assert_value(&self) -> AssertValue { AssertValue::U64(*self) @@ -80,8 +87,13 @@ impl AssertValue { bytes.len() == std::mem::size_of::() && u64::deserialize(bytes).unwrap() == *v } AssertValue::Hash(v) => xxhash_rust::xxh3::xxh3_64(bytes) == *v, + AssertValue::None => false, } } + + pub fn is_none(&self) -> bool { + matches!(self, AssertValue::None) + } } impl Deserialize for HashedValue { diff --git a/crates/store/src/write/batch.rs b/crates/store/src/write/batch.rs index eec7fc46..4388ded7 100644 --- a/crates/store/src/write/batch.rs +++ b/crates/store/src/write/batch.rs @@ -25,7 +25,7 @@ use crate::BM_DOCUMENT_IDS; use super::{ assert::ToAssertValue, Batch, BatchBuilder, BitmapFamily, HasFlag, IntoOperations, Operation, - Serialize, ToBitmaps, F_BITMAP, F_CLEAR, F_INDEX, F_VALUE, + Serialize, ToBitmaps, ValueClass, F_BITMAP, F_CLEAR, F_INDEX, F_VALUE, }; impl BatchBuilder { @@ -81,10 +81,13 @@ impl BatchBuilder { self } - pub fn assert_value(&mut self, field: impl Into, value: impl ToAssertValue) -> &mut Self { + pub fn assert_value( + &mut self, + class: impl Into, + value: impl ToAssertValue, + ) -> &mut Self { self.ops.push(Operation::AssertValue { - field: field.into(), - family: 0, + class: class.into(), assert_value: value.to_assert_value(), }); self @@ -115,8 +118,7 @@ impl BatchBuilder { if options.has_flag(F_VALUE) { self.ops.push(Operation::Value { - field, - family: 0, + class: ValueClass::Property { field, family: 0 }, set: if is_set { Some(value) } else { None }, }); } @@ -144,6 +146,11 @@ impl BatchBuilder { self } + pub fn op(&mut self, op: Operation) -> &mut Self { + self.ops.push(op); + self + } + pub fn custom(&mut self, value: impl IntoOperations) -> &mut Self { value.build(self); self diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 47a492a8..b43bd12d 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -25,8 +25,8 @@ use std::convert::TryInto; use utils::codec::leb128::Leb128_; use crate::{ - AclKey, BitmapKey, Deserialize, Error, IndexKey, IndexKeyPrefix, Key, LogKey, Serialize, - ValueKey, SUBSPACE_BITMAPS, SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, + AclKey, BitmapKey, CustomValueKey, Deserialize, Error, IndexKey, IndexKeyPrefix, Key, LogKey, + Serialize, ValueKey, SUBSPACE_BITMAPS, SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, }; pub struct KeySerializer { @@ -247,6 +247,24 @@ impl Serialize for &ValueKey { } } +impl Serialize for &CustomValueKey { + fn serialize(self) -> Vec { + { + #[cfg(feature = "key_subspace")] + { + KeySerializer::new(std::mem::size_of::() + 2) + .write(crate::SUBSPACE_VALUES) + } + #[cfg(not(feature = "key_subspace"))] + { + KeySerializer::new(std::mem::size_of::() + 1) + } + } + .write(&self.value[..]) + .finalize() + } +} + impl> Serialize for &BitmapKey { fn serialize(self) -> Vec { let key = self.key.as_ref(); @@ -342,6 +360,12 @@ impl Key for ValueKey { } } +impl Key for CustomValueKey { + fn subspace(&self) -> u8 { + SUBSPACE_VALUES + } +} + impl Key for AclKey { fn subspace(&self) -> u8 { SUBSPACE_VALUES @@ -366,6 +390,12 @@ impl Serialize for ValueKey { } } +impl Serialize for CustomValueKey { + fn serialize(self) -> Vec { + (&self).serialize() + } +} + impl Serialize for AclKey { fn serialize(self) -> Vec { (&self).serialize() diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index ae753a5b..48d8027a 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -63,17 +63,11 @@ pub enum Operation { document_id: u32, }, AssertValue { - field: u8, - family: u8, + class: ValueClass, assert_value: AssertValue, }, Value { - field: u8, - family: u8, - set: Option>, - }, - Acl { - grant_account_id: u32, + class: ValueClass, set: Option>, }, Index { @@ -97,6 +91,13 @@ pub enum Operation { }, } +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum ValueClass { + Property { field: u8, family: u8 }, + Acl { grant_account_id: u32 }, + Custom { bytes: Vec }, +} + impl Serialize for u32 { fn serialize(self) -> Vec { self.to_be_bytes().to_vec() @@ -364,6 +365,15 @@ pub trait IntoOperations { fn build(self, batch: &mut BatchBuilder); } +impl Operation { + pub fn acl(grant_account_id: u32, set: Option>) -> Self { + Operation::Value { + class: ValueClass::Acl { grant_account_id }, + set, + } + } +} + #[inline(always)] pub fn now() -> u64 { SystemTime::now() diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 344693ab..b8690d50 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -29,12 +29,11 @@ async fn ldap_directory() { .unwrap() .unwrap(), Principal { - id: 2, name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], typ: Type::Individual, - member_of: vec!["ou=sales,ou=groups,dc=example,dc=org".to_string()], + member_of: vec!["sales".to_string()], ..Default::default() } ); @@ -48,7 +47,6 @@ async fn ldap_directory() { .unwrap() .unwrap(), Principal { - id: 4, name: "bill".to_string(), description: "Bill Foobar".to_string().into(), secrets: vec![ @@ -68,44 +66,25 @@ async fn ldap_directory() { .unwrap() .is_none()); - // Get by id - assert_eq!( - handle.principal_by_id(2).await.unwrap().unwrap(), - Principal { - id: 2, - name: "john".to_string(), - description: "John Doe".to_string().into(), - typ: Type::Individual, - secrets: vec!["12345".to_string()], - member_of: vec!["ou=sales,ou=groups,dc=example,dc=org".to_string()], - ..Default::default() - } - ); - // Get user by name - let mut principal = handle.principal_by_name("jane").await.unwrap().unwrap(); + let mut principal = handle.principal("jane").await.unwrap().unwrap(); principal.member_of.sort_unstable(); assert_eq!( principal, Principal { - id: 3, name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, secrets: vec!["abcde".to_string()], - member_of: vec![ - "ou=sales,ou=groups,dc=example,dc=org".to_string(), - "support".to_string() - ], + member_of: vec!["sales".to_string(), "support".to_string()], ..Default::default() } ); // Get group by name assert_eq!( - handle.principal_by_name("sales").await.unwrap().unwrap(), + handle.principal("sales").await.unwrap().unwrap(), Principal { - id: 5, name: "sales".to_string(), description: "sales".to_string().into(), typ: Type::Group, @@ -113,55 +92,52 @@ async fn ldap_directory() { } ); - // Member of - compare_sorted( - handle - .member_of(&handle.principal_by_name("john").await.unwrap().unwrap()) - .await - .unwrap(), - vec![5], - ); - compare_sorted( - handle - .member_of(&handle.principal_by_name("jane").await.unwrap().unwrap()) - .await - .unwrap(), - vec![5, 6], - ); - // Emails by id compare_sorted( - handle.emails_by_id(2).await.unwrap(), + handle.emails_by_name("john").await.unwrap(), vec![ "john@example.org".to_string(), "john.doe@example.org".to_string(), ], ); compare_sorted( - handle.emails_by_id(4).await.unwrap(), + handle.emails_by_name("bill").await.unwrap(), vec!["bill@example.org".to_string()], ); // Ids by email compare_sorted( - handle.ids_by_email("jane@example.org").await.unwrap(), - vec![3], + handle.names_by_email("jane@example.org").await.unwrap(), + vec!["jane".to_string()], ); compare_sorted( - handle.ids_by_email("jane+alias@example.org").await.unwrap(), - vec![3], + handle + .names_by_email("jane+alias@example.org") + .await + .unwrap(), + vec!["jane".to_string()], ); compare_sorted( - handle.ids_by_email("info@example.org").await.unwrap(), - vec![2, 3, 4], + handle.names_by_email("info@example.org").await.unwrap(), + vec!["john".to_string(), "jane".to_string(), "bill".to_string()], ); compare_sorted( - handle.ids_by_email("info+alias@example.org").await.unwrap(), - vec![2, 3, 4], + handle + .names_by_email("info+alias@example.org") + .await + .unwrap(), + vec!["john".to_string(), "jane".to_string(), "bill".to_string()], ); compare_sorted( - handle.ids_by_email("unknown@example.org").await.unwrap(), - Vec::::new(), + handle.names_by_email("unknown@example.org").await.unwrap(), + Vec::::new(), + ); + assert_eq!( + handle + .names_by_email("anything@catchall.org") + .await + .unwrap(), + vec!["robert".to_string()] ); // Domain validation diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index 2220fc8c..7e3b4ccd 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -23,21 +23,18 @@ subaddressing = true max-connections = 1 [directory."sql".query] -login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'" -name = "SELECT id, name, type, description, quota FROM accounts WHERE name = ?" -id = "SELECT id, name, type, description, quota FROM accounts WHERE id = ?" -members = "SELECT gid FROM group_members WHERE uid = ?" -recipients = "SELECT id FROM emails WHERE address = ?" -emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC" +name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true" +members = "SELECT member_of FROM group_members WHERE name = ?" +recipients = "SELECT name FROM emails WHERE address = ?" +emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC" verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" -expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" +expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" [directory."sql".columns] name = "name" description = "description" secret = "secret" -id = "id" email = "address" quota = "quota" type = "type" @@ -59,10 +56,8 @@ catch-all = true subaddressing = true [directory."ldap".filter] -login = "(&(objectClass=posixAccount)(accountStatus=active)(cn=?))" -name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))" +name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))" email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(givenName=?)(sn=?)))" -id = "(|(&(objectClass=posixAccount)(uidNumber=?))(&(objectClass=posixGroup)(gidNumber=?)))" verify = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*?*)(givenName=*?*)))" expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(sn=?))" domains = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*@?)(givenName=*@?)(sn=*@?)))" @@ -75,11 +70,10 @@ group = "posixGroup" # 'sn' and 'givenName' are used to search for aliases/lists. [directory."ldap".attributes] -name = "cn" +name = "uid" description = ["principalName", "description"] secret = "userPassword" groups = ["memberOf", "otherGroups"] -id = ["uidNumber", "gidNumber"] email = "mail" email-alias = "givenName" quota = "diskQuota" diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index 457f516f..bcca7ee9 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -1,5 +1,4 @@ use directory::{Directory, Principal, Type}; -use jmap_proto::types::id::Id; use mail_send::Credentials; use crate::directory::parse_config; @@ -69,11 +68,11 @@ async fn sql_directory() { .unwrap() .unwrap(), Principal { - id: 2, name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], typ: Type::Individual, + member_of: vec!["sales".to_string()], ..Default::default() } ); @@ -87,7 +86,6 @@ async fn sql_directory() { .unwrap() .unwrap(), Principal { - id: 4, name: "bill".to_string(), description: "Bill Foobar".to_string().into(), secrets: vec![ @@ -107,35 +105,23 @@ async fn sql_directory() { .unwrap() .is_none()); - // Get by id - assert_eq!( - handle.principal_by_id(2).await.unwrap().unwrap(), - Principal { - id: 2, - name: "john".to_string(), - description: "John Doe".to_string().into(), - typ: Type::Individual, - ..Default::default() - } - ); - // Get user by name assert_eq!( - handle.principal_by_name("jane").await.unwrap().unwrap(), + handle.principal("jane").await.unwrap().unwrap(), Principal { - id: 3, name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, + secrets: vec!["abcde".to_string()], + member_of: vec!["sales".to_string(), "support".to_string()], ..Default::default() } ); // Get group by name assert_eq!( - handle.principal_by_name("sales").await.unwrap().unwrap(), + handle.principal("sales").await.unwrap().unwrap(), Principal { - id: 5, name: "sales".to_string(), description: "Sales Team".to_string().into(), typ: Type::Group, @@ -143,25 +129,9 @@ async fn sql_directory() { } ); - // Member of - assert_eq!( - handle - .member_of(&handle.principal_by_name("john").await.unwrap().unwrap()) - .await - .unwrap(), - vec![5] - ); - assert_eq!( - handle - .member_of(&handle.principal_by_name("jane").await.unwrap().unwrap()) - .await - .unwrap(), - vec![5, 6] - ); - // Emails by id assert_eq!( - handle.emails_by_id(2).await.unwrap(), + handle.emails_by_name("john").await.unwrap(), vec![ "john@example.org".to_string(), "jdoe@example.org".to_string(), @@ -169,34 +139,43 @@ async fn sql_directory() { ] ); assert_eq!( - handle.emails_by_id(4).await.unwrap(), + handle.emails_by_name("bill").await.unwrap(), vec!["bill@example.org".to_string(),] ); // Ids by email assert_eq!( - handle.ids_by_email("jane@example.org").await.unwrap(), - vec![3] + handle.names_by_email("jane@example.org").await.unwrap(), + vec!["jane".to_string()] ); assert_eq!( - handle.ids_by_email("info@example.org").await.unwrap(), - vec![2, 3, 4] + handle.names_by_email("info@example.org").await.unwrap(), + vec!["bill".to_string(), "jane".to_string(), "john".to_string()] ); assert_eq!( - handle.ids_by_email("jane+alias@example.org").await.unwrap(), - vec![3] + handle + .names_by_email("jane+alias@example.org") + .await + .unwrap(), + vec!["jane".to_string()] ); assert_eq!( - handle.ids_by_email("info+alias@example.org").await.unwrap(), - vec![2, 3, 4] + handle + .names_by_email("info+alias@example.org") + .await + .unwrap(), + vec!["bill".to_string(), "jane".to_string(), "john".to_string()] ); assert_eq!( - handle.ids_by_email("unknown@example.org").await.unwrap(), - Vec::::new() + handle.names_by_email("unknown@example.org").await.unwrap(), + Vec::::new() ); assert_eq!( - handle.ids_by_email("anything@catchall.org").await.unwrap(), - vec![7] + handle + .names_by_email("anything@catchall.org") + .await + .unwrap(), + vec!["robert".to_string()] ); // Domain validation @@ -245,16 +224,16 @@ async fn sql_directory() { pub async fn create_test_directory(handle: &dyn Directory) { // Create tables for query in [ - "CREATE TABLE accounts (name TEXT, id INTEGER PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)", - "CREATE TABLE group_members (uid INTEGER, gid INTEGER, PRIMARY KEY (uid, gid))", - "CREATE TABLE emails (id INTEGER NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (id, address))", + "CREATE TABLE accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)", + "CREATE TABLE group_members (name TEXT NOT NULL, member_of TEXT NOT NULL, PRIMARY KEY (name, member_of))", + "CREATE TABLE emails (name TEXT NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (name, address))", "INSERT INTO accounts (name, secret, type) VALUES ('admin', 'secret', 'individual')", ] { handle.query(query, &[]).await.unwrap_or_else(|_| panic!("failed for {query}")); } } -pub async fn create_test_user(handle: &dyn Directory, login: &str, secret: &str, name: &str) -> Id { +pub async fn create_test_user(handle: &dyn Directory, login: &str, secret: &str, name: &str) { handle .query( "INSERT OR IGNORE INTO accounts (name, secret, description, type, active) VALUES (?, ?, ?, 'individual', true)", @@ -262,8 +241,6 @@ pub async fn create_test_user(handle: &dyn Directory, login: &str, secret: &str, ) .await .unwrap(); - - Id::from(get_principal_id(handle, login).await) } pub async fn create_test_user_with_email( @@ -271,13 +248,12 @@ pub async fn create_test_user_with_email( login: &str, secret: &str, name: &str, -) -> Id { - let id = create_test_user(handle, login, secret, name).await; +) { + create_test_user(handle, login, secret, name).await; link_test_address(handle, login, login, "primary").await; - id } -pub async fn create_test_group(handle: &dyn Directory, login: &str, name: &str) -> Id { +pub async fn create_test_group(handle: &dyn Directory, login: &str, name: &str) { handle .query( "INSERT OR IGNORE INTO accounts (name, description, type, active) VALUES (?, ?, 'group', true)", @@ -285,96 +261,59 @@ pub async fn create_test_group(handle: &dyn Directory, login: &str, name: &str) ) .await .unwrap(); - - Id::from(get_principal_id(handle, login).await) } -pub async fn create_test_group_with_email(handle: &dyn Directory, login: &str, name: &str) -> Id { - let id = create_test_group(handle, login, name).await; +pub async fn create_test_group_with_email(handle: &dyn Directory, login: &str, name: &str) { + create_test_group(handle, login, name).await; link_test_address(handle, login, login, "primary").await; - id } pub async fn link_test_address(handle: &dyn Directory, login: &str, address: &str, typ: &str) { - let id = get_principal_id(handle, login).await; handle .query( - &format!( - "INSERT OR IGNORE INTO emails (id, address, type) VALUES ({}, ?, ?)", - id, - ), - &[address, typ], + &format!("INSERT OR IGNORE INTO emails (name, address, type) VALUES (?, ?, ?)",), + &[login, address, typ], ) .await .unwrap(); } pub async fn set_test_quota(handle: &dyn Directory, login: &str, quota: u32) { - let id = get_principal_id(handle, login).await; handle .query( - &format!("UPDATE accounts SET quota = {} where id = {}", quota, id,), - &[], + &format!("UPDATE accounts SET quota = {} where name = ?", quota,), + &[login], ) .await .unwrap(); } pub async fn add_to_group(handle: &dyn Directory, login: &str, group: &str) { - let group = handle.principal_by_name(group).await.unwrap().unwrap(); - let gid = group.id; - assert_ne!(gid, u32::MAX, "{group:?}"); - - add_to_group_id(handle, login, gid).await; -} - -pub async fn add_to_group_id(handle: &dyn Directory, login: &str, gid: u32) { - let user = handle.principal_by_name(login).await.unwrap().unwrap(); - let uid = user.id; - assert_ne!(uid, u32::MAX, "{user:?}"); - assert_ne!(uid, gid, "{user:?}"); - add_user_id_to_group_id(handle, uid, gid).await; -} - -pub async fn add_user_id_to_group_id(handle: &dyn Directory, uid: u32, gid: u32) { handle .query( - &format!( - "INSERT INTO group_members (uid, gid) VALUES ({}, {})", - uid, gid - ), - &[], + "INSERT INTO group_members (name, member_of) VALUES (?, ?)", + &[login, group], ) .await .unwrap(); } -pub async fn remove_from_group(handle: &dyn Directory, uid: u32, gid: u32) { +pub async fn remove_from_group(handle: &dyn Directory, login: &str, group: &str) { handle .query( - &format!( - "DELETE FROM group_members WHERE uid = {} AND gid = {}", - uid, gid - ), - &[], + "DELETE FROM group_members WHERE name = ? AND member_of = ?", + &[login, group], ) .await .unwrap(); } pub async fn remove_test_alias(handle: &dyn Directory, login: &str, alias: &str) { - let id = get_principal_id(handle, login).await; handle .query( - &format!("DELETE FROM emails WHERE id = {} AND address = ?", id), - &[alias], + "DELETE FROM emails WHERE name = ? AND address = ?", + &[login, alias], ) .await .unwrap(); } - -async fn get_principal_id(handle: &dyn Directory, name: &str) -> u32 { - let p = handle.principal_by_name(name).await.unwrap().unwrap(); - assert_ne!(p.id, u32::MAX, "{name} {p:#?}"); - p.id -} diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 50287fce..669fbe42 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -53,7 +53,7 @@ use utils::{config::ServerProtocol, UnwrapFailure}; use crate::{ add_test_certs, directory::sql::{ - add_to_group_id, create_test_directory, create_test_user, create_test_user_with_email, + add_to_group, create_test_directory, create_test_user, create_test_user_with_email, }, store::TempDir, }; @@ -182,21 +182,18 @@ address = "sqlite::memory:" max-connections = 1 [directory."sql".query] -login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'" -name = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ?" -id = "SELECT id, name, type, secret, description, quota FROM accounts WHERE id = ?" -members = "SELECT gid FROM group_members WHERE uid = ?" -recipients = "SELECT id FROM emails WHERE address = ?" -emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC" +name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true" +members = "SELECT member_of FROM group_members WHERE name = ?" +recipients = "SELECT name FROM emails WHERE address = ?" +emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC" verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" -expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" +expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" [directory."sql".columns] name = "name" description = "description" secret = "secret" -id = "id" email = "address" quota = "quota" type = "type" @@ -269,7 +266,7 @@ async fn init_imap_tests(delete_if_exists: bool) -> IMAPTest { // Create tables and test accounts create_test_directory(jmap.directory.as_ref()).await; create_test_user(jmap.directory.as_ref(), "admin", "secret", "Superuser").await; - add_to_group_id(jmap.directory.as_ref(), "admin", 0).await; + add_to_group(jmap.directory.as_ref(), "admin", "superuser").await; create_test_user_with_email( jmap.directory.as_ref(), "jdoe@example.com", diff --git a/tests/src/jmap/auth_acl.rs b/tests/src/jmap/auth_acl.rs index c7402d48..e5959501 100644 --- a/tests/src/jmap/auth_acl.rs +++ b/tests/src/jmap/auth_acl.rs @@ -43,8 +43,7 @@ use store::ahash::AHashMap; use crate::{ directory::sql::{ - add_user_id_to_group_id, create_test_group_with_email, create_test_user_with_email, - remove_from_group, + add_to_group, create_test_group_with_email, create_test_user_with_email, remove_from_group, }, jmap::{mailbox::destroy_all_mailboxes, test_account_login}, }; @@ -57,15 +56,30 @@ pub async fn test(server: Arc, admin_client: &mut Client) { let trash_id = Id::new(TRASH_ID as u64).to_string(); let directory = server.directory.as_ref(); - let john_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; - let jane_id = - create_test_user_with_email(directory, "jane.smith@example.com", "abcde", "Jane Smith") - .await; - let bill_id = - create_test_user_with_email(directory, "bill@example.com", "098765", "Bill Foobar").await; - let sales_id = - create_test_group_with_email(directory, "sales@example.com", "Sales Group").await; + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + create_test_user_with_email(directory, "jane.smith@example.com", "abcde", "Jane Smith").await; + create_test_user_with_email(directory, "bill@example.com", "098765", "Bill Foobar").await; + create_test_group_with_email(directory, "sales@example.com", "Sales Group").await; + let john_id: Id = server + .get_account_id("jdoe@example.com") + .await + .unwrap() + .into(); + let jane_id: Id = server + .get_account_id("jane.smith@example.com") + .await + .unwrap() + .into(); + let bill_id: Id = server + .get_account_id("bill@example.com") + .await + .unwrap() + .into(); + let sales_id: Id = server + .get_account_id("sales@example.com") + .await + .unwrap() + .into(); // Authenticate all accounts let mut john_client = test_account_login("jdoe@example.com", "12345").await; @@ -652,8 +666,8 @@ pub async fn test(server: Arc, admin_client: &mut Client) { ); // Add John and Jane to the Sales group - for id in [jane_id.id(), john_id.id()] { - add_user_id_to_group_id(directory, id as u32, sales_id.id() as u32).await; + for name in ["jdoe@example.com", "jane.smith@example.com"] { + add_to_group(directory, name, "sales@example.com").await; } server.access_tokens.clear(); john_client.refresh_session().await.unwrap(); @@ -749,7 +763,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { ); // Remove John from the sales group - remove_from_group(directory, john_id.id() as u32, sales_id.id() as u32).await; + remove_from_group(directory, "jdoe@example.com", "sales@example.com").await; server.sessions.clear(); assert_forbidden( john_client diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs index 449c29e0..a1b35b6d 100644 --- a/tests/src/jmap/auth_limits.rs +++ b/tests/src/jmap/auth_limits.rs @@ -29,6 +29,7 @@ use jmap_client::{ core::set::{SetError, SetErrorType}, mailbox::{self}, }; +use jmap_proto::types::id::Id; use crate::{ directory::sql::{create_test_user_with_email, link_test_address}, @@ -40,10 +41,8 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Create test account let directory = server.directory.as_ref(); - let account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); link_test_address( directory, "jdoe@example.com", diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs index c15355cf..22ebebf3 100644 --- a/tests/src/jmap/auth_oauth.rs +++ b/tests/src/jmap/auth_oauth.rs @@ -32,6 +32,7 @@ use jmap_client::{ client::{Client, Credentials}, mailbox::query::Filter, }; +use jmap_proto::types::id::Id; use reqwest::{header, redirect::Policy}; use serde::de::DeserializeOwned; use store::ahash::AHashMap; @@ -43,9 +44,8 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Create test account let directory = server.directory.as_ref(); - let john_id = create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let john_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); // Obtain OAuth metadata let metadata: OAuthMetadata = diff --git a/tests/src/jmap/delivery.rs b/tests/src/jmap/delivery.rs index 1790d268..1ef0a96a 100644 --- a/tests/src/jmap/delivery.rs +++ b/tests/src/jmap/delivery.rs @@ -41,18 +41,15 @@ pub async fn test(server: Arc, client: &mut Client) { // Create a domain name and a test account let directory = server.directory.as_ref(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + create_test_user_with_email(directory, "jane@example.com", "abcdef", "Jane Smith").await; + create_test_user_with_email(directory, "bill@example.com", "098765", "Bill Foobar").await; let account_id_1 = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); let account_id_2 = - create_test_user_with_email(directory, "jane@example.com", "abcdef", "Jane Smith") - .await - .to_string(); + Id::from(server.get_account_id("jane@example.com").await.unwrap()).to_string(); let account_id_3 = - create_test_user_with_email(directory, "bill@example.com", "098765", "Bill Foobar") - .await - .to_string(); + Id::from(server.get_account_id("bill@example.com").await.unwrap()).to_string(); link_test_address( directory, "jdoe@example.com", diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index eb79be52..51f97c72 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -91,10 +91,8 @@ pub async fn test(server: Arc, client: &mut Client) { // Create a test account let directory = server.directory.as_ref(); - let account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); // Create an identity without using a valid address should fail match client diff --git a/tests/src/jmap/event_source.rs b/tests/src/jmap/event_source.rs index 6b16ddc8..8f3f6630 100644 --- a/tests/src/jmap/event_source.rs +++ b/tests/src/jmap/event_source.rs @@ -40,10 +40,8 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Create test account let directory = server.directory.as_ref(); - let account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); let client = test_account_login("jdoe@example.com", "12345").await; let mut changes = client diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 03ddd582..7d70e6df 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -33,7 +33,7 @@ use utils::{config::ServerProtocol, UnwrapFailure}; use crate::{ add_test_certs, - directory::sql::{add_to_group_id, create_test_directory, create_test_user}, + directory::sql::{add_to_group, create_test_directory, create_test_user}, store::TempDir, }; @@ -180,21 +180,18 @@ address = "sqlite::memory:" max-connections = 1 [directory."sql".query] -login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'" -name = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ?" -id = "SELECT id, name, type, secret, description, quota FROM accounts WHERE id = ?" -members = "SELECT gid FROM group_members WHERE uid = ?" -recipients = "SELECT id FROM emails WHERE address = ?" -emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC" +name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true" +members = "SELECT member_of FROM group_members WHERE name = ?" +recipients = "SELECT name FROM emails WHERE address = ?" +emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC" verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" -expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" +expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" [directory."sql".columns] name = "name" description = "description" secret = "secret" -id = "id" email = "address" quota = "quota" type = "type" @@ -299,7 +296,7 @@ async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest { // Create tables create_test_directory(jmap.directory.as_ref()).await; create_test_user(jmap.directory.as_ref(), "admin", "secret", "Superuser").await; - add_to_group_id(jmap.directory.as_ref(), "admin", 0).await; + add_to_group(jmap.directory.as_ref(), "admin", "superusers").await; if delete_if_exists { jmap.store.destroy().await; diff --git a/tests/src/jmap/push_subscription.rs b/tests/src/jmap/push_subscription.rs index ee596e20..89bc4b81 100644 --- a/tests/src/jmap/push_subscription.rs +++ b/tests/src/jmap/push_subscription.rs @@ -80,8 +80,8 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Create test account let directory = server.directory.as_ref(); - let account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()); admin_client.set_default_account_id(account_id); let client = test_account_login("jdoe@example.com", "12345").await; diff --git a/tests/src/jmap/quota.rs b/tests/src/jmap/quota.rs index e1ea9bf5..372c5925 100644 --- a/tests/src/jmap/quota.rs +++ b/tests/src/jmap/quota.rs @@ -16,11 +16,10 @@ use crate::{ pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running quota tests..."); let directory = server.directory.as_ref(); - let other_account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; - let account_id = - create_test_user_with_email(directory, "robert@example.com", "aabbcc", "Robert Foobar") - .await; + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + create_test_user_with_email(directory, "robert@example.com", "aabbcc", "Robert Foobar").await; + let other_account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()); + let account_id = Id::from(server.get_account_id("robert@example.com").await.unwrap()); set_test_quota(directory, "robert@example.com", 1024).await; add_to_group(directory, "robert@example.com", "jdoe@example.com").await; diff --git a/tests/src/jmap/sieve_script.rs b/tests/src/jmap/sieve_script.rs index 386c6a45..dc3b2bbf 100644 --- a/tests/src/jmap/sieve_script.rs +++ b/tests/src/jmap/sieve_script.rs @@ -36,6 +36,7 @@ use jmap_client::{ sieve::query::{Comparator, Filter}, Error, }; +use jmap_proto::types::id::Id; use crate::{ directory::sql::create_test_user_with_email, @@ -51,10 +52,8 @@ pub async fn test(server: Arc, client: &mut Client) { // Create test account let directory = server.directory.as_ref(); - let account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); client.set_default_account_id(&account_id); // Validate scripts diff --git a/tests/src/jmap/vacation_response.rs b/tests/src/jmap/vacation_response.rs index 478dae0b..2292f889 100644 --- a/tests/src/jmap/vacation_response.rs +++ b/tests/src/jmap/vacation_response.rs @@ -26,6 +26,7 @@ use std::{sync::Arc, time::Instant}; use chrono::{Duration, Utc}; use jmap::JMAP; use jmap_client::client::Client; +use jmap_proto::types::id::Id; use crate::{ directory::sql::create_test_user_with_email, @@ -43,10 +44,8 @@ pub async fn test(server: Arc, client: &mut Client) { // Create test account let directory = server.directory.as_ref(); - let account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); client.set_default_account_id(&account_id); // Start mock SMTP server diff --git a/tests/src/jmap/websocket.rs b/tests/src/jmap/websocket.rs index 0b311240..75064bd1 100644 --- a/tests/src/jmap/websocket.rs +++ b/tests/src/jmap/websocket.rs @@ -35,6 +35,7 @@ use jmap_client::{ }, TypeState, }; +use jmap_proto::types::id::Id; use tokio::sync::mpsc; use crate::{ @@ -47,10 +48,8 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Authenticate all accounts let directory = server.directory.as_ref(); - let account_id = - create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); let client = test_account_login("jdoe@example.com", "12345").await; let mut ws_stream = client.connect_ws().await.unwrap(); diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index fad04b25..ea00f1bc 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -48,19 +48,21 @@ address = "sqlite::memory:" max-connections = 1 [directory."sql".query] -login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'" -recipients = "SELECT id FROM emails WHERE address = ?" -name = "SELECT id, name, type, description, quota FROM accounts WHERE name = ?" -emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC" +name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true" +members = "SELECT member_of FROM group_members WHERE name = ?" +recipients = "SELECT name FROM emails WHERE address = ?" +emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC" verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" -expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" +expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" [directory."sql".columns] name = "name" +description = "description" secret = "secret" -id = "id" email = "address" +quota = "quota" +type = "type" [directory."sql".lookup] domains = "SELECT name FROM domains WHERE name = ? LIMIT 1"