From 4620a92fd853186651425a99bada7ccbe5c6e15a Mon Sep 17 00:00:00 2001 From: mdecimus Date: Tue, 21 Oct 2025 11:23:30 +0200 Subject: [PATCH] Principal storage format improvements --- crates/common/src/auth/access_token.rs | 17 +-- crates/dav/src/principal/propfind.rs | 9 +- .../directory/src/backend/internal/lookup.rs | 4 +- .../directory/src/backend/internal/manage.rs | 112 +++++++++------ crates/directory/src/backend/ldap/lookup.rs | 29 +++- crates/directory/src/backend/memory/config.rs | 12 +- crates/directory/src/backend/memory/lookup.rs | 4 +- crates/directory/src/backend/oidc/lookup.rs | 2 +- crates/directory/src/backend/sql/lookup.rs | 39 ++++-- crates/directory/src/core/principal.rs | 130 ++++++++++++++---- crates/directory/src/lib.rs | 3 +- crates/email/src/sieve/ingest.rs | 2 +- crates/http/src/autoconfig/mod.rs | 3 +- crates/jmap/src/identity/get.rs | 2 +- crates/jmap/src/identity/set.rs | 2 +- crates/jmap/src/participant_identity/get.rs | 2 +- crates/jmap/src/participant_identity/set.rs | 2 +- crates/jmap/src/principal/get.rs | 6 +- crates/migration/src/principal.rs | 16 ++- tests/src/directory/mod.rs | 2 +- tests/src/directory/oidc.rs | 2 +- tests/src/jmap/mod.rs | 4 +- 22 files changed, 287 insertions(+), 117 deletions(-) diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index e2789fbc..7e332f1d 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -73,7 +73,14 @@ impl Server { object_quota[typ as usize] = quota; } PrincipalData::Description(v) => description = Some(v), - PrincipalData::Email(v) => { + PrincipalData::PrimaryEmail(v) => { + if emails.is_empty() { + emails.push(v); + } else { + emails.insert(0, v); + } + } + PrincipalData::EmailAlias(v) => { emails.push(v); } PrincipalData::Locale(v) => locale = Some(v), @@ -129,13 +136,7 @@ impl Server { .caused_by(trc::location!())? && group.typ == Type::Group { - emails.extend(group.data.into_iter().filter_map(|data| { - if let PrincipalData::Email(email) = data { - Some(email) - } else { - None - } - })); + emails.extend(group.into_email_addresses()); } } diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index d749fb21..2c97fcaa 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -131,7 +131,14 @@ impl PrincipalPropFind for Server { PrincipalData::Description(desc) => { description = Some(desc); } - PrincipalData::Email(email) => { + PrincipalData::PrimaryEmail(email) => { + if emails.is_empty() { + emails.push(email); + } else { + emails.insert(0, email); + } + } + PrincipalData::EmailAlias(email) => { emails.push(email); } _ => {} diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index 8f781ce5..64c5d1b6 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -159,7 +159,9 @@ impl DirectoryStore for Store { for account_id in self.get_members(list_id).await? { if let Some(email) = self.get_principal(account_id).await?.and_then(|p| { p.data.into_iter().find_map(|data| { - if let PrincipalData::Email(email) = data { + if let PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) = + data + { Some(email) } else { None diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index ab5d2a04..c807854f 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -206,6 +206,7 @@ impl ManageDirectory for Store { .assert_value(name_key.clone(), ()) .create_document(principal_id); build_search_index(&mut batch, principal_id, None, Some(&principal)); + principal.sort(); batch .set( name_key, @@ -377,18 +378,19 @@ impl ManageDirectory for Store { // Set fields principal_create.name = name; - if let Some(description) = principal_set.take_str(PrincipalField::Description) { - principal_create - .data - .push(PrincipalData::Description(description)); - } - for secret in principal_set .take_str_array(PrincipalField::Secrets) .unwrap_or_default() { principal_create.data.push(PrincipalData::Secret(secret)); } + + if let Some(description) = principal_set.take_str(PrincipalField::Description) { + principal_create + .data + .push(PrincipalData::Description(description)); + } + if let Some(picture) = principal_set.take_str(PrincipalField::Picture) { principal_create.data.push(PrincipalData::Picture(picture)); } @@ -518,9 +520,11 @@ impl ManageDirectory for Store { // Make sure the e-mail is not taken and validate domain if principal_create.typ != Type::OauthClient { - for email in principal_set + for (idx, email) in principal_set .take_str_array(PrincipalField::Emails) .unwrap_or_default() + .into_iter() + .enumerate() { let email = email.to_lowercase(); if self.rcpt(&email).await.caused_by(trc::location!())? != RcptType::Invalid { @@ -535,7 +539,13 @@ impl ManageDirectory for Store { .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id)) .ok_or_else(|| not_found(domain.to_string()))?; } - principal_create.data.push(PrincipalData::Email(email)); + if idx == 0 { + principal_create + .data + .push(PrincipalData::PrimaryEmail(email)); + } else { + principal_create.data.push(PrincipalData::EmailAlias(email)); + } } } @@ -564,6 +574,7 @@ impl ManageDirectory for Store { } // Serialize + principal_create.sort(); let archiver = Archiver::new(principal_create); let principal_bytes = archiver.serialize().caused_by(trc::location!())?; let principal_create = archiver.into_inner(); @@ -592,7 +603,7 @@ impl ManageDirectory for Store { ); // Write email to id mapping - for email in principal_create.emails() { + for email in principal_create.email_addresses() { batch.set( ValueClass::Directory(DirectoryClass::EmailToId(email.as_bytes().to_vec())), pinfo_email.serialize(), @@ -830,7 +841,9 @@ impl ManageDirectory for Store { .clear(DirectoryClass::UsedQuota(principal_id)); for email in principal.data.iter() { - if let ArchivedPrincipalData::Email(email) = email { + if let ArchivedPrincipalData::PrimaryEmail(email) + | ArchivedPrincipalData::EmailAlias(email) = email + { batch.clear(DirectoryClass::EmailToId(email.as_bytes().to_vec())); } } @@ -1160,25 +1173,9 @@ impl ManageDirectory for Store { PrincipalValue::String(secret), ) => { if !principal.secrets().any(|v| *v == secret) { - if secret.is_otp_auth() { - // Add OTP Auth URLs to the beginning of the list - principal.data.insert(0, PrincipalData::Secret(secret)); - - // Password changed, update changed principals - changed_principals.add_change( - principal_id, - principal_type, - change.field, - ); - } else { - principal.data.push(PrincipalData::Secret(secret)); - // Password changed, update changed principals - changed_principals.add_change( - principal_id, - principal_type, - change.field, - ); - } + principal.data.push(PrincipalData::Secret(secret)); + // Password changed, update changed principals + changed_principals.add_change(principal_id, principal_type, change.field); } } ( @@ -1307,7 +1304,7 @@ impl ManageDirectory for Store { .map(|v| v.to_lowercase()) .collect::>(); for email in &emails { - if !principal.emails().any(|v| v == email) { + if !principal.email_addresses().any(|v| v == email) { if validate_emails { self.validate_email(email, tenant_id, params.create_domains) .await?; @@ -1321,7 +1318,7 @@ impl ManageDirectory for Store { } } - for email in principal.emails() { + for email in principal.email_addresses() { if !emails.contains(email) { batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( email.as_bytes().to_vec(), @@ -1332,11 +1329,18 @@ impl ManageDirectory for Store { // Emails changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); - principal - .data - .retain(|v| !matches!(v, PrincipalData::Email(_))); - for email in emails { - principal.data.push(PrincipalData::Email(email)); + principal.data.retain(|v| { + !matches!( + v, + PrincipalData::PrimaryEmail(_) | PrincipalData::EmailAlias(_) + ) + }); + for (idx, email) in emails.into_iter().enumerate() { + if idx == 0 { + principal.data.push(PrincipalData::PrimaryEmail(email)); + } else { + principal.data.push(PrincipalData::EmailAlias(email)); + } } } ( @@ -1345,7 +1349,11 @@ impl ManageDirectory for Store { PrincipalValue::String(email), ) => { let email = email.to_lowercase(); - if !principal.emails().any(|v| v == &email) { + let mut emails_iter = principal.email_addresses().peekable(); + let has_emails = emails_iter.peek().is_some(); + let email_exists = emails_iter.any(|v| v == &email); + drop(emails_iter); + if !email_exists { if validate_emails { self.validate_email(&email, tenant_id, params.create_domains) .await?; @@ -1356,7 +1364,11 @@ impl ManageDirectory for Store { )), pinfo_email.clone(), ); - principal.data.push(PrincipalData::Email(email)); + if has_emails { + principal.data.push(PrincipalData::EmailAlias(email)); + } else { + principal.data.push(PrincipalData::PrimaryEmail(email)); + } // Emails changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); @@ -1368,15 +1380,33 @@ impl ManageDirectory for Store { PrincipalValue::String(email), ) => { let email = email.to_lowercase(); - if principal.emails().any(|v| v == &email) { + if principal.email_addresses().any(|v| v == &email) { + let mut deleted_primary = false; principal.data.retain(|v| match v { - PrincipalData::Email(v) => v != &email, + PrincipalData::EmailAlias(v) => v != &email, + PrincipalData::PrimaryEmail(v) => { + if v == &email { + deleted_primary = true; + false + } else { + true + } + } _ => true, }); batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( email.as_bytes().to_vec(), ))); + if deleted_primary { + for data in &mut principal.data { + if let PrincipalData::EmailAlias(email) = data { + *data = PrincipalData::PrimaryEmail(std::mem::take(email)); + break; + } + } + } + // Emails changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); } @@ -2294,7 +2324,7 @@ impl ManageDirectory for Store { result.append_str(PrincipalField::Secrets, secret); } } - PrincipalData::Email(email) => { + PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) => { if fields.is_empty() || fields.contains(&PrincipalField::Emails) { result.append_str(PrincipalField::Emails, email); } diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index cdc75263..010f3cef 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -431,6 +431,7 @@ impl LdapMappings { let mut role = ROLE_USER; let mut member_of = vec![]; let mut description = None; + let mut has_primary_email = false; for (attr, value) in entry.attrs { if self.attr_name.contains(&attr) { @@ -438,9 +439,16 @@ impl LdapMappings { principal.name = value.into_iter().next().unwrap_or_default(); } else { for (idx, item) in value.into_iter().enumerate() { - principal - .data - .insert(0, PrincipalData::Email(item.to_lowercase())); + if !has_primary_email { + has_primary_email = true; + principal + .data + .push(PrincipalData::PrimaryEmail(item.to_lowercase())); + } else { + principal + .data + .push(PrincipalData::EmailAlias(item.to_lowercase())); + } if idx == 0 { principal.name = item; } @@ -461,15 +469,22 @@ impl LdapMappings { } } else if self.attr_email_address.contains(&attr) { for item in value { - principal - .data - .insert(0, PrincipalData::Email(item.to_lowercase())); + if !has_primary_email { + has_primary_email = true; + principal + .data + .push(PrincipalData::PrimaryEmail(item.to_lowercase())); + } else { + principal + .data + .push(PrincipalData::EmailAlias(item.to_lowercase())); + } } } else if self.attr_email_alias.contains(&attr) { for item in value { principal .data - .push(PrincipalData::Email(item.to_lowercase())); + .push(PrincipalData::EmailAlias(item.to_lowercase())); } } else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) { if (description.is_none() || idx == 0) diff --git a/crates/directory/src/backend/memory/config.rs b/crates/directory/src/backend/memory/config.rs index d29d1d10..35eec13a 100644 --- a/crates/directory/src/backend/memory/config.rs +++ b/crates/directory/src/backend/memory/config.rs @@ -108,9 +108,15 @@ impl MemoryDirectory { directory.domains.insert(domain.to_lowercase()); } - principal - .data - .push(PrincipalData::Email(email.to_lowercase())); + if pos == 0 { + principal + .data + .push(PrincipalData::PrimaryEmail(email.to_lowercase())); + } else { + principal + .data + .push(PrincipalData::EmailAlias(email.to_lowercase())); + } } // Parse mailing lists diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs index 81ffc34c..c17c4688 100644 --- a/crates/directory/src/backend/memory/lookup.rs +++ b/crates/directory/src/backend/memory/lookup.rs @@ -80,8 +80,8 @@ impl MemoryDirectory { if let EmailType::List(uid) = item { for principal in &self.principals { if principal.id == *uid { - if let Some(addr) = principal.emails().next() { - result.push(addr.clone()) + if let Some(addr) = principal.primary_email() { + result.push(addr.to_string()) } break; } diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index 684b1bd6..ce7c56da 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -183,7 +183,7 @@ impl BuildPrincipal for OpenIdResponse { // Build principal let mut data = Vec::with_capacity(3); - data.push(PrincipalData::Email(email)); + data.push(PrincipalData::PrimaryEmail(email)); if let Some(name) = full_name { data.push(PrincipalData::Description(name)); } diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index bf4a5c3b..1e5d6877 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -158,20 +158,29 @@ impl SqlDirectory { // Obtain emails if !self.mappings.query_emails.is_empty() { - let rows = self + let mut rows = self .sql_store .sql_query::( &self.mappings.query_emails, vec![external_principal.name().into()], ) .await - .caused_by(trc::location!())?; - external_principal.data.extend( - rows.rows - .into_iter() - .flat_map(|v| v.values.into_iter().map(|v| v.into_lower_string())) - .map(PrincipalData::Email), - ); + .caused_by(trc::location!())? + .rows + .into_iter() + .flat_map(|v| v.values.into_iter().map(|v| v.into_lower_string())); + + if external_principal.primary_email().is_none() + && let Some(email) = rows.next() + { + external_principal + .data + .push(PrincipalData::PrimaryEmail(email)); + } + + external_principal + .data + .extend(rows.map(PrincipalData::EmailAlias)); } // Obtain account ID if not available @@ -271,6 +280,7 @@ impl SqlMappings { let mut principal = Principal::new(u32::MAX, Type::Individual); let mut role = ROLE_USER; + let mut has_primary_email = false; if let Some(row) = rows.rows.into_iter().next() { for (name, value) in rows.names.into_iter().zip(row.values) { @@ -300,9 +310,16 @@ impl SqlMappings { } } else if name.eq_ignore_ascii_case(&self.column_email) { if let Value::Text(text) = value { - principal - .data - .push(PrincipalData::Email(text.to_lowercase())); + if !has_primary_email { + has_primary_email = true; + principal + .data + .push(PrincipalData::PrimaryEmail(text.to_lowercase())); + } else { + principal + .data + .push(PrincipalData::EmailAlias(text.to_lowercase())); + } } } else if name.eq_ignore_ascii_case(&self.column_quota) && let Value::Integer(quota) = value diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index 28368f5d..f896f421 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -7,7 +7,9 @@ use crate::{ ArchivedPrincipal, ArchivedPrincipalData, FALLBACK_ADMIN_ID, Permission, PermissionGrant, Principal, PrincipalData, ROLE_ADMIN, Type, - backend::internal::{PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue}, + backend::internal::{ + PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue, SpecialSecrets, + }, }; use ahash::AHashSet; use nlp::tokenizers::word::WordTokenizer; @@ -104,18 +106,64 @@ impl Principal { } pub fn secrets(&self) -> impl Iterator { - self.data.iter().filter_map(|item| { - if let PrincipalData::Secret(secret) = item { - Some(secret) + let mut found_secret = false; + self.data + .iter() + .take_while(move |item| { + if matches!(item, PrincipalData::Secret(_)) { + found_secret = true; + true + } else { + !found_secret + } + }) + .filter_map(|item| { + if let PrincipalData::Secret(secret) = item { + Some(secret) + } else { + None + } + }) + } + + pub fn primary_email(&self) -> Option<&str> { + self.data.iter().find_map(|item| { + if let PrincipalData::PrimaryEmail(email) = item { + Some(email.as_str()) } else { None } }) } - pub fn emails(&self) -> impl Iterator { - self.data.iter().filter_map(|item| { - if let PrincipalData::Email(email) = item { + pub fn email_addresses(&self) -> impl Iterator { + let mut found_email = false; + self.data + .iter() + .take_while(move |item| { + if matches!( + item, + PrincipalData::PrimaryEmail(_) | PrincipalData::EmailAlias(_) + ) { + found_email = true; + true + } else { + !found_email + } + }) + .filter_map(|item| { + if let PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) = item + { + Some(email) + } else { + None + } + }) + } + + pub fn into_primary_email(self) -> Option { + self.data.into_iter().find_map(|item| { + if let PrincipalData::PrimaryEmail(email) = item { Some(email) } else { None @@ -123,9 +171,9 @@ impl Principal { }) } - pub fn into_emails(self) -> impl Iterator { + pub fn into_email_addresses(self) -> impl Iterator { self.data.into_iter().filter_map(|item| { - if let PrincipalData::Email(email) = item { + if let PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) = item { Some(email) } else { None @@ -325,15 +373,27 @@ impl Principal { } // Update emails - if update_list(external.emails(), self.emails()) { + if update_list(external.email_addresses(), self.email_addresses()) { if overwrite_emails { let mut new_emails = Vec::new(); + self.data.retain(|item| { + !matches!( + item, + PrincipalData::PrimaryEmail(_) | PrincipalData::EmailAlias(_) + ) + }); self.data - .retain(|item| !matches!(item, PrincipalData::Email(_))); - self.data.extend(external.emails().map(|email| { - new_emails.push(email.to_string()); - PrincipalData::Email(email.to_string()) - })); + .extend(external.data.iter().filter_map(|v| match v { + PrincipalData::PrimaryEmail(email) => { + new_emails.push(email.clone()); + Some(PrincipalData::PrimaryEmail(email.clone())) + } + PrincipalData::EmailAlias(email) => { + new_emails.push(email.clone()); + Some(PrincipalData::EmailAlias(email.clone())) + } + _ => None, + })); updates.push(PrincipalUpdate::set( PrincipalField::Emails, PrincipalValue::StringList(new_emails), @@ -341,16 +401,16 @@ impl Principal { } else { // Missing emails are appended to avoid overwriting locally defined aliases // This means that old email addresses need to be deleted either manually or using the API - let current_emails = self.emails().collect::>(); + let current_emails = self.email_addresses().collect::>(); let mut new_emails = Vec::new(); - for email in external.emails() { + for email in external.email_addresses() { let email = email.to_lowercase(); if !current_emails.contains(&email) { updates.push(PrincipalUpdate::add_item( PrincipalField::Emails, PrincipalValue::String(email.clone()), )); - new_emails.push(PrincipalData::Email(email)); + new_emails.push(PrincipalData::EmailAlias(email)); } } self.data.extend(new_emails); @@ -396,6 +456,27 @@ impl Principal { ], } } + + pub fn sort(&mut self) { + self.data.sort_unstable_by_key(|d| d.rank()); + } +} + +impl PrincipalData { + fn rank(&self) -> usize { + match self { + PrincipalData::Secret(v) => { + if v.is_otp_auth() { + 0 + } else { + 1 + } + } + PrincipalData::PrimaryEmail(_) => 2, + PrincipalData::EmailAlias(_) => 3, + _ => 4, + } + } } fn update_list<'x>( @@ -432,7 +513,8 @@ impl PrincipalData { match self { PrincipalData::Secret(v) | PrincipalData::Description(v) - | PrincipalData::Email(v) + | PrincipalData::PrimaryEmail(v) + | PrincipalData::EmailAlias(v) | PrincipalData::Picture(v) | PrincipalData::ExternalMember(v) | PrincipalData::Url(v) @@ -955,8 +1037,9 @@ pub(crate) fn build_search_index( for word in [Some(current.name.as_str())] .into_iter() .chain(current.data.iter().map(|s| match s { - ArchivedPrincipalData::Description(v) => Some(v.as_str()), - ArchivedPrincipalData::Email(v) => Some(v.as_str()), + ArchivedPrincipalData::Description(v) + | ArchivedPrincipalData::PrimaryEmail(v) + | ArchivedPrincipalData::EmailAlias(v) => Some(v.as_str()), _ => None, })) .flatten() @@ -969,8 +1052,9 @@ pub(crate) fn build_search_index( for word in [Some(new.name.as_str())] .into_iter() .chain(new.data.iter().map(|s| match s { - PrincipalData::Description(v) => Some(v.as_str()), - PrincipalData::Email(v) => Some(v.as_str()), + PrincipalData::Description(v) + | PrincipalData::PrimaryEmail(v) + | PrincipalData::EmailAlias(v) => Some(v.as_str()), _ => None, })) .flatten() diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index c51e08e5..17d69eac 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -60,7 +60,8 @@ pub enum PrincipalData { // Profile data Description(String), - Email(String), + PrimaryEmail(String), + EmailAlias(String), Picture(String), ExternalMember(String), Url(String), diff --git a/crates/email/src/sieve/ingest.rs b/crates/email/src/sieve/ingest.rs index 92ddeb2c..d6f9d483 100644 --- a/crates/email/src/sieve/ingest.rs +++ b/crates/email/src/sieve/ingest.rs @@ -124,7 +124,7 @@ impl SieveScriptIngest for Server { .caused_by(trc::location!())? .and_then(|p| { instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); - p.into_emails().next() + p.into_primary_email() }); // Set account address diff --git a/crates/http/src/autoconfig/mod.rs b/crates/http/src/autoconfig/mod.rs index ec0b847d..849c4fd7 100644 --- a/crates/http/src/autoconfig/mod.rs +++ b/crates/http/src/autoconfig/mod.rs @@ -210,8 +210,7 @@ impl Autoconfig for Server { .query(QueryParams::id(id).with_return_member_of(false)) .await && principal - .emails() - .next() + .primary_email() .is_some_and(|email| email.eq_ignore_ascii_case(emailaddress)) { account_name = principal.name; diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index d0fbfd6f..92b8fe28 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -167,7 +167,7 @@ impl IdentityGet for Server { let mut description = None; for data in principal.data { match data { - PrincipalData::Email(v) => emails.push(v), + PrincipalData::PrimaryEmail(v) | PrincipalData::EmailAlias(v) => emails.push(v), PrincipalData::Description(v) => description = Some(v), _ => {} } diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index adf0851d..a3dbb477 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -68,7 +68,7 @@ impl IdentitySet for Server { .directory() .query(QueryParams::id(account_id).with_return_member_of(false)) .await? - .is_none_or(|p| !p.emails().any(|e| e == &identity.email)) + .is_none_or(|p| !p.email_addresses().any(|e| e == &identity.email)) { response.not_created.append( id, diff --git a/crates/jmap/src/participant_identity/get.rs b/crates/jmap/src/participant_identity/get.rs index 8afa494c..976d3181 100644 --- a/crates/jmap/src/participant_identity/get.rs +++ b/crates/jmap/src/participant_identity/get.rs @@ -144,7 +144,7 @@ impl ParticipantIdentityGet for Server { let mut description = None; for data in principal.data { match data { - PrincipalData::Email(v) => emails.push(v), + PrincipalData::PrimaryEmail(v) | PrincipalData::EmailAlias(v) => emails.push(v), PrincipalData::Description(v) => description = Some(v), _ => {} } diff --git a/crates/jmap/src/participant_identity/set.rs b/crates/jmap/src/participant_identity/set.rs index d8334097..84e65f58 100644 --- a/crates/jmap/src/participant_identity/set.rs +++ b/crates/jmap/src/participant_identity/set.rs @@ -58,7 +58,7 @@ impl ParticipantIdentitySet for Server { .directory() .query(QueryParams::id(account_id).with_return_member_of(false)) .await? - .map(|p| p.into_emails().collect::>()) + .map(|p| p.into_email_addresses().collect::>()) .unwrap_or_default(); // Process creates diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 8c51b9d9..f54529fb 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -123,8 +123,7 @@ impl PrincipalGet for Server { .map(|v| Value::Str(v.to_string().into())) .unwrap_or(Value::Null), PrincipalProperty::Email => principal - .emails() - .next() + .primary_email() .map(|email| Value::Str(email.to_string().into())) .unwrap_or(Value::Null), PrincipalProperty::Accounts => Value::Object(Map::from(vec![( @@ -175,8 +174,7 @@ impl PrincipalGet for Server { Key::Borrowed("calendarAddress"), Value::Str( principal - .emails() - .next() + .primary_email() .map(|email| format!("mailto:{}", email)) .unwrap_or_default() .into(), diff --git a/crates/migration/src/principal.rs b/crates/migration/src/principal.rs index 5145e73c..91e302ea 100644 --- a/crates/migration/src/principal.rs +++ b/crates/migration/src/principal.rs @@ -189,11 +189,21 @@ impl FromLegacy for Principal { { principal.data.push(PrincipalData::Secret(secret)); } - for email in legacy + for (idx, email) in legacy .take_str_array(PrincipalField::Emails) .unwrap_or_default() + .into_iter() + .enumerate() { - principal.data.push(PrincipalData::Email(email)); + if idx == 0 { + principal + .data + .push(PrincipalData::PrimaryEmail(email.clone())); + } else { + principal + .data + .push(PrincipalData::EmailAlias(email.clone())); + } } if let Some(picture) = legacy.take_str(PrincipalField::Picture) { principal.data.push(PrincipalData::Picture(picture)); @@ -361,7 +371,7 @@ pub(crate) fn build_search_index(batch: &mut BatchBuilder, principal_id: u32, ne for word in [Some(new.name.as_str()), new.description()] .into_iter() - .chain(new.emails().map(|s| Some(s.as_str()))) + .chain(new.email_addresses().map(|s| Some(s.as_str()))) .flatten() { new_words.extend(WordTokenizer::new(word, MAX_TOKEN_LENGTH).map(|t| t.word)); diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index ee636e38..0e91a74e 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -595,7 +595,7 @@ impl From for TestPrincipal { roles: value.roles().map(|v| v.to_string()).collect(), lists: value.lists().map(|v| v.to_string()).collect(), secrets: value.secrets().map(|v| v.to_string()).collect(), - emails: value.emails().map(|v| v.to_string()).collect(), + emails: value.email_addresses().map(|v| v.to_string()).collect(), description: value.description().map(|v| v.to_string()), name: value.name, } diff --git a/tests/src/directory/oidc.rs b/tests/src/directory/oidc.rs index 39bc35dc..d1bf9c9c 100644 --- a/tests/src/directory/oidc.rs +++ b/tests/src/directory/oidc.rs @@ -132,7 +132,7 @@ async fn oidc_directory() { .unwrap(); assert_eq!(principal.name(), "jdoe"); assert_eq!( - principal.emails().next().map(|s| s.as_str()), + principal.email_addresses().next().map(|s| s.as_str()), Some("john@example.org") ); assert_eq!(principal.description(), Some("John Doe")); diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 6bfdfdd6..dd6d478a 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -80,7 +80,7 @@ async fn jmap_tests() { server::webhooks::test(&mut params).await; - /*mail::get::test(&mut params).await; + mail::get::test(&mut params).await; mail::set::test(&mut params).await; mail::parse::test(&mut params).await; mail::query::test(&mut params, delete).await; @@ -116,7 +116,7 @@ async fn jmap_tests() { files::acl::test(&mut params).await; calendar::calendars::test(&mut params).await; - calendar::event::test(&mut params).await;*/ + calendar::event::test(&mut params).await; calendar::notification::test(&mut params).await; calendar::alarm::test(&mut params).await;