Fix Cache: Invalidate negative email caches when an account is created
This commit is contained in:
@@ -44,6 +44,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
|
||||
- OIDC: Add default domain name to groups that are not email addresses.
|
||||
- RocksDB: Enable blob garbage collection to reclaim disk space from deleted blobs.
|
||||
- Sieve: `include` statements ignore capitalisation of sub-script names (#1643)
|
||||
- Cache: Invalidate negative email caches when an account is created.
|
||||
- Troubleshoot tool: Use the configured source IP address when connecting to remote servers (#2867).
|
||||
|
||||
## [0.16.9] - 2026-06-15
|
||||
|
||||
22
crates/common/src/cache/directory.rs
vendored
22
crates/common/src/cache/directory.rs
vendored
@@ -4,9 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Server, auth::DomainCache, cache::invalidate::CacheInvalidationBuilder, ipc::BroadcastEvent,
|
||||
};
|
||||
use crate::{Server, auth::DomainCache, cache::invalidate::CacheInvalidationBuilder};
|
||||
use registry::{
|
||||
schema::{
|
||||
prelude::{Object, ObjectType},
|
||||
@@ -169,8 +167,6 @@ impl Server {
|
||||
enabled: true,
|
||||
description: None,
|
||||
});
|
||||
|
||||
self.invalidate_local_negative_account_cache(local, alias_domain.id);
|
||||
}
|
||||
}
|
||||
let mut member_group_ids = Vec::with_capacity(account.groups.len());
|
||||
@@ -222,9 +218,11 @@ impl Server {
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
RegistryWriteResult::Success(id) => {
|
||||
self.invalidate_local_negative_account_cache(local, domain.id);
|
||||
self.cluster_broadcast(BroadcastEvent::CacheInvalidateNegative)
|
||||
.await;
|
||||
let mut invalidator = CacheInvalidationBuilder::default();
|
||||
invalidator.process_create(&account);
|
||||
self.invalidate_caches(invalidator)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(AccountWithId {
|
||||
id: id.document_id(),
|
||||
@@ -343,8 +341,6 @@ impl Server {
|
||||
enabled: true,
|
||||
description: None,
|
||||
});
|
||||
|
||||
self.invalidate_local_negative_account_cache(local, alias_domain.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +373,11 @@ impl Server {
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
RegistryWriteResult::Success(id) => {
|
||||
self.invalidate_local_negative_account_cache(local, domain.id);
|
||||
let mut invalidator = CacheInvalidationBuilder::default();
|
||||
invalidator.process_create(&account);
|
||||
self.invalidate_caches(invalidator)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(id.document_id())
|
||||
}
|
||||
|
||||
71
crates/common/src/cache/invalidate.rs
vendored
71
crates/common/src/cache/invalidate.rs
vendored
@@ -13,7 +13,7 @@ use ahash::AHashSet;
|
||||
use registry::{
|
||||
schema::{
|
||||
prelude::{Object, ObjectInner, ObjectType},
|
||||
structs::Account,
|
||||
structs::{Account, EmailAlias},
|
||||
},
|
||||
types::id::ObjectId,
|
||||
};
|
||||
@@ -57,6 +57,10 @@ impl CacheInvalidationBuilder {
|
||||
self.invalidate(CacheInvalidation::Account(id));
|
||||
}
|
||||
|
||||
if was_renamed || aliases_changed {
|
||||
self.invalidate_negative_email(&new_object.inner);
|
||||
}
|
||||
|
||||
if tenant_changed
|
||||
|| groups_changed
|
||||
|| credentials_changed
|
||||
@@ -94,6 +98,10 @@ impl CacheInvalidationBuilder {
|
||||
self.invalidate(CacheInvalidation::Account(id));
|
||||
}
|
||||
|
||||
if was_renamed || aliases_changed {
|
||||
self.invalidate_negative_email(&new_object.inner);
|
||||
}
|
||||
|
||||
if tenant_changed || roles_changed || permissions_changed {
|
||||
self.invalidate(CacheInvalidation::AccessToken(id));
|
||||
}
|
||||
@@ -158,6 +166,12 @@ impl CacheInvalidationBuilder {
|
||||
|| (current.domain_id != new.domain_id) =>
|
||||
{
|
||||
self.invalidate(CacheInvalidation::List(id));
|
||||
if (current.aliases != new.aliases)
|
||||
|| (current.name != new.name)
|
||||
|| (current.domain_id != new.domain_id)
|
||||
{
|
||||
self.invalidate_negative_email(&new_object.inner);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -194,6 +208,34 @@ impl CacheInvalidationBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_create(&mut self, object: &Object) {
|
||||
self.invalidate_negative_email(&object.inner);
|
||||
}
|
||||
|
||||
fn invalidate_negative_email(&mut self, object: &ObjectInner) {
|
||||
let (name, domain_id, aliases) = match object {
|
||||
ObjectInner::Account(Account::User(account)) => {
|
||||
(&account.name, account.domain_id, &account.aliases)
|
||||
}
|
||||
ObjectInner::Account(Account::Group(account)) => {
|
||||
(&account.name, account.domain_id, &account.aliases)
|
||||
}
|
||||
ObjectInner::MailingList(list) => (&list.name, list.domain_id, &list.aliases),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
self.invalidate(CacheInvalidation::EmailNegative {
|
||||
domain_id: domain_id.document_id(),
|
||||
local_part_hash: hash_local_part(name),
|
||||
});
|
||||
for alias in aliases.iter().filter(|alias: &&EmailAlias| alias.enabled) {
|
||||
self.invalidate(CacheInvalidation::EmailNegative {
|
||||
domain_id: alias.domain_id.document_id(),
|
||||
local_part_hash: hash_local_part(&alias.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, change: CacheInvalidation) {
|
||||
self.changes.insert(change);
|
||||
}
|
||||
@@ -298,15 +340,21 @@ impl Server {
|
||||
self.inner.cache.emails_negative.clear();
|
||||
}
|
||||
|
||||
pub fn invalidate_local_negative_account_cache(&self, local_part: &str, domain_id: u32) {
|
||||
pub fn invalidate_local_negative_account_cache(
|
||||
&self,
|
||||
local_part: &str,
|
||||
domain_id: u32,
|
||||
) -> bool {
|
||||
self.inner
|
||||
.cache
|
||||
.emails_negative
|
||||
.remove(&EmailAddressRef::new(local_part, domain_id));
|
||||
.remove(&EmailAddressRef::new(local_part, domain_id))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn invalidate_local_caches(&self, changes: &[CacheInvalidation]) {
|
||||
let cache = &self.inner.cache;
|
||||
let mut negative_emails: AHashSet<(u32, u32)> = AHashSet::new();
|
||||
|
||||
for change in changes {
|
||||
match change {
|
||||
@@ -370,11 +418,28 @@ impl Server {
|
||||
.lock()
|
||||
.retain(|_, v| v.tenant_id != Some(*id));
|
||||
}
|
||||
CacheInvalidation::EmailNegative {
|
||||
domain_id,
|
||||
local_part_hash,
|
||||
} => {
|
||||
negative_emails.insert((*domain_id, *local_part_hash));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !negative_emails.is_empty() {
|
||||
cache.emails_negative.retain(|key| {
|
||||
!negative_emails.contains(&(key.domain_id, hash_local_part(&key.local_part)))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn hash_local_part(local_part: &str) -> u32 {
|
||||
xxhash_rust::xxh3::xxh3_64(local_part.as_bytes()) as u32
|
||||
}
|
||||
|
||||
impl From<CacheInvalidation> for CacheInvalidationBuilder {
|
||||
fn from(invalidation: CacheInvalidation) -> Self {
|
||||
let mut builder = CacheInvalidationBuilder::default();
|
||||
|
||||
@@ -99,6 +99,10 @@ pub enum CacheInvalidation {
|
||||
List(u32),
|
||||
DomainLogo(u32),
|
||||
TenantLogo(u32),
|
||||
EmailNegative {
|
||||
domain_id: u32,
|
||||
local_part_hash: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -602,6 +602,7 @@ impl RegistrySet for Server {
|
||||
Modification::Create { client_id, .. },
|
||||
RegistryWriteResult::Success(id),
|
||||
) => {
|
||||
cache_invalidator.process_create(&new_object);
|
||||
response.object.insert(Property::Id, RegistryValue::Id(id));
|
||||
set.response
|
||||
.created
|
||||
|
||||
@@ -107,6 +107,15 @@ impl BroadcastBatch<Vec<BroadcastEvent>> {
|
||||
CacheInvalidation::List(id) => (7u8, *id),
|
||||
CacheInvalidation::DomainLogo(id) => (8u8, *id),
|
||||
CacheInvalidation::TenantLogo(id) => (9u8, *id),
|
||||
CacheInvalidation::EmailNegative {
|
||||
domain_id,
|
||||
local_part_hash,
|
||||
} => {
|
||||
serialized.push(10u8);
|
||||
let _ = serialized.write_leb128(*domain_id);
|
||||
let _ = serialized.write_leb128(*local_part_hash);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
serialized.push(marker);
|
||||
@@ -240,6 +249,14 @@ where
|
||||
7 => CacheInvalidation::List(id),
|
||||
8 => CacheInvalidation::DomainLogo(id),
|
||||
9 => CacheInvalidation::TenantLogo(id),
|
||||
10 => {
|
||||
let local_part_hash =
|
||||
self.messages.next_leb128::<u32>().ok_or(())?;
|
||||
CacheInvalidation::EmailNegative {
|
||||
domain_id: id,
|
||||
local_part_hash,
|
||||
}
|
||||
}
|
||||
_ => return Err(()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,6 +183,11 @@ impl<K: Eq + Hash + CacheItemWeight, V: Clone + CacheItemWeight> CacheWithTtl<K,
|
||||
self.0.remove(key).map(|(_, v)| v.value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn retain(&self, f: impl Fn(&K) -> bool) {
|
||||
self.0.retain(|key, _| f(key));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&self) {
|
||||
self.0.clear();
|
||||
|
||||
@@ -211,5 +211,7 @@ async fn rcpt() {
|
||||
session.ehlo("mx1.foobar.org").await;
|
||||
session.mail_from("idn2@example.net", "250").await;
|
||||
session.rcpt_to("nobody@straß6.de", "550 5.1.2").await;
|
||||
session.rcpt_to("nobody@xn--stra6-oqa.de", "550 5.1.2").await;
|
||||
session
|
||||
.rcpt_to("nobody@xn--stra6-oqa.de", "550 5.1.2")
|
||||
.await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user