The AGPL asks a modified version to carry prominent notices saying it was modified, and giving a date. Publishing the source is the conveyance that asks for it, so it wants doing before the repository is public rather than at the release. Every upstream file the fork changed now says so in its header, beneath the notice it came with: 164 files, found by diffing against the upstream snapshot branch rather than by guessing, so the list is what actually differs. Files the fork wrote itself already carry their own copyright and need nothing. Upstream's notices are untouched, which its licence requires and which was already true. The README says the same thing in prose, since the obligation is on the work as a whole and not only its Rust files. Builds unchanged: the server and the test binary both compile.
471 lines
18 KiB
Rust
471 lines
18 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
use crate::{
|
|
Server,
|
|
auth::{EmailAddressRef, EmailCache},
|
|
ipc::{BroadcastEvent, CacheInvalidation},
|
|
};
|
|
use ahash::AHashSet;
|
|
use registry::{
|
|
schema::{
|
|
prelude::{Object, ObjectInner, ObjectType},
|
|
structs::{Account, EmailAlias},
|
|
},
|
|
types::id::ObjectId,
|
|
};
|
|
use types::id::Id;
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct CacheInvalidationBuilder {
|
|
changes: AHashSet<CacheInvalidation>,
|
|
}
|
|
|
|
impl CacheInvalidationBuilder {
|
|
pub fn process_update(&mut self, id: Id, current_object: &Object, new_object: &Object) {
|
|
let id = id.document_id();
|
|
match (¤t_object.inner, &new_object.inner) {
|
|
(
|
|
ObjectInner::Account(Account::User(current)),
|
|
ObjectInner::Account(Account::User(new)),
|
|
) => {
|
|
let was_renamed =
|
|
(current.name != new.name) || (current.domain_id != new.domain_id);
|
|
let quota_changed = current.quotas != new.quotas;
|
|
let permissions_changed = current.permissions != new.permissions;
|
|
let roles_changed = current.roles != new.roles;
|
|
let tenant_changed = current.member_tenant_id != new.member_tenant_id;
|
|
let details_changed =
|
|
current.locale != new.locale || current.description != new.description;
|
|
let groups_changed = current.member_group_ids != new.member_group_ids;
|
|
let aliases_changed = current.aliases != new.aliases;
|
|
let credentials_changed = current.credentials != new.credentials;
|
|
let encryption_changed = current.encryption_at_rest != new.encryption_at_rest;
|
|
|
|
if was_renamed
|
|
|| aliases_changed
|
|
|| tenant_changed
|
|
|| groups_changed
|
|
|| quota_changed
|
|
|| details_changed
|
|
|| encryption_changed
|
|
{
|
|
self.invalidate(CacheInvalidation::Account(id));
|
|
}
|
|
|
|
if was_renamed || aliases_changed {
|
|
self.invalidate_negative_email(&new_object.inner);
|
|
}
|
|
|
|
if tenant_changed
|
|
|| groups_changed
|
|
|| credentials_changed
|
|
|| roles_changed
|
|
|| permissions_changed
|
|
{
|
|
self.invalidate(CacheInvalidation::AccessToken(id));
|
|
}
|
|
|
|
if was_renamed {
|
|
self.invalidate(CacheInvalidation::DavResources(id));
|
|
}
|
|
}
|
|
|
|
(
|
|
ObjectInner::Account(Account::Group(current)),
|
|
ObjectInner::Account(Account::Group(new)),
|
|
) => {
|
|
let was_renamed =
|
|
(current.name != new.name) || (current.domain_id != new.domain_id);
|
|
let quota_changed = current.quotas != new.quotas;
|
|
let permissions_changed = current.permissions != new.permissions;
|
|
let roles_changed = current.roles != new.roles;
|
|
let tenant_changed = current.member_tenant_id != new.member_tenant_id;
|
|
let details_changed =
|
|
current.locale != new.locale || current.description != new.description;
|
|
let aliases_changed = current.aliases != new.aliases;
|
|
|
|
if was_renamed
|
|
|| aliases_changed
|
|
|| tenant_changed
|
|
|| quota_changed
|
|
|| details_changed
|
|
{
|
|
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));
|
|
}
|
|
|
|
if was_renamed {
|
|
self.invalidate(CacheInvalidation::DavResources(id));
|
|
}
|
|
}
|
|
|
|
(ObjectInner::Domain(current), ObjectInner::Domain(new)) => {
|
|
if (current.name != new.name)
|
|
|| (current.aliases != new.aliases)
|
|
|| (current.directory_id != new.directory_id)
|
|
|| (current.member_tenant_id != new.member_tenant_id)
|
|
|| (current.catch_all_address != new.catch_all_address)
|
|
|| (current.sub_addressing != new.sub_addressing)
|
|
|| (current.allow_relaying != new.allow_relaying)
|
|
|| (current.is_enabled != new.is_enabled)
|
|
// inbuxa: SCIM-60, the flag is cached as DOMAIN_FLAG_SCIM,
|
|
// so turning SCIM's authority on or off has to take effect
|
|
// without a restart
|
|
|| (current.allow_scim_provisioning != new.allow_scim_provisioning)
|
|
{
|
|
self.invalidate(CacheInvalidation::Domain(id));
|
|
}
|
|
|
|
if (current.name != new.name) || (current.aliases != new.aliases) {
|
|
self.invalidate(CacheInvalidation::DomainNegative);
|
|
}
|
|
|
|
if current.logo != new.logo {
|
|
self.invalidate(CacheInvalidation::DomainLogo(id));
|
|
}
|
|
}
|
|
|
|
(ObjectInner::DkimSignature(current), ObjectInner::DkimSignature(new)) => {
|
|
let current_domain_id = current.domain_id().document_id();
|
|
let new_domain_id = new.domain_id().document_id();
|
|
self.invalidate(CacheInvalidation::DkimSignature(current_domain_id));
|
|
if current_domain_id != new_domain_id {
|
|
self.invalidate(CacheInvalidation::DkimSignature(new_domain_id));
|
|
}
|
|
}
|
|
|
|
(ObjectInner::Tenant(current), ObjectInner::Tenant(new)) => {
|
|
if (current.permissions != new.permissions)
|
|
|| (current.roles != new.roles)
|
|
|| (current.quotas != new.quotas)
|
|
{
|
|
self.invalidate(CacheInvalidation::Tenant(id));
|
|
}
|
|
|
|
if current.logo != new.logo {
|
|
self.invalidate(CacheInvalidation::TenantLogo(id));
|
|
}
|
|
}
|
|
|
|
(ObjectInner::Role(current), ObjectInner::Role(new))
|
|
if (current.enabled_permissions != new.enabled_permissions)
|
|
|| (current.disabled_permissions != new.disabled_permissions)
|
|
|| (current.member_tenant_id != new.member_tenant_id)
|
|
|| (current.role_ids != new.role_ids) =>
|
|
{
|
|
self.invalidate(CacheInvalidation::Role(id));
|
|
}
|
|
|
|
(ObjectInner::MailingList(current), ObjectInner::MailingList(new))
|
|
if (current.aliases != new.aliases)
|
|
|| (current.name != new.name)
|
|
|| (current.recipients != new.recipients)
|
|
|| (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);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn process_delete(&mut self, id: Id, object: &Object) {
|
|
let id = id.document_id();
|
|
match &object.inner {
|
|
ObjectInner::Account(_) => {
|
|
self.invalidate(CacheInvalidation::AccessToken(id));
|
|
self.invalidate(CacheInvalidation::Account(id));
|
|
self.invalidate(CacheInvalidation::DavResources(id));
|
|
}
|
|
ObjectInner::Domain(_) => {
|
|
self.invalidate(CacheInvalidation::Domain(id));
|
|
self.invalidate(CacheInvalidation::DomainLogo(id));
|
|
}
|
|
ObjectInner::DkimSignature(object) => {
|
|
self.invalidate(CacheInvalidation::DkimSignature(
|
|
object.domain_id().document_id(),
|
|
));
|
|
}
|
|
ObjectInner::Tenant(_) => {
|
|
self.invalidate(CacheInvalidation::Tenant(id));
|
|
self.invalidate(CacheInvalidation::TenantLogo(id));
|
|
}
|
|
ObjectInner::Role(_) => {
|
|
self.invalidate(CacheInvalidation::Role(id));
|
|
}
|
|
ObjectInner::MailingList(_) => {
|
|
self.invalidate(CacheInvalidation::List(id));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn process_create(&mut self, object: &Object) {
|
|
if matches!(&object.inner, ObjectInner::Domain(_)) {
|
|
self.invalidate(CacheInvalidation::DomainNegative);
|
|
}
|
|
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);
|
|
}
|
|
|
|
pub fn with_invalidation(mut self, change: CacheInvalidation) -> Self {
|
|
self.invalidate(change);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Server {
|
|
pub async fn invalidate_caches(&self, changes: CacheInvalidationBuilder) -> trc::Result<()> {
|
|
let mut changes = changes.changes;
|
|
if changes.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Invalidate objects linking roles
|
|
let mut role_ids = changes
|
|
.iter()
|
|
.filter_map(|change| {
|
|
if let CacheInvalidation::Role(role_id) = change {
|
|
Some(*role_id)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if !role_ids.is_empty() {
|
|
let mut fetched_role_ids = AHashSet::new();
|
|
|
|
while let Some(role_id) = role_ids.pop() {
|
|
if fetched_role_ids.insert(role_id) {
|
|
let linked_objects = self
|
|
.registry()
|
|
.linked_objects(ObjectId::new(ObjectType::Role, role_id.into()))
|
|
.await?;
|
|
// inbuxa: MT-16: a role a tenant holds sets its ceiling
|
|
for tenant_id in inbuxa_features::tenancy::members::tenants_using_role(
|
|
self.registry(),
|
|
&linked_objects,
|
|
)
|
|
.await?
|
|
{
|
|
changes.insert(CacheInvalidation::Tenant(tenant_id));
|
|
}
|
|
for linked_object in linked_objects {
|
|
match linked_object.object() {
|
|
ObjectType::Account => {
|
|
changes.insert(CacheInvalidation::AccessToken(
|
|
linked_object.id().document_id(),
|
|
));
|
|
}
|
|
ObjectType::Role => {
|
|
role_ids.push(linked_object.id().document_id());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// inbuxa: MT-16: a tenant's change reaches its people on their next request
|
|
let tenant_ids = changes
|
|
.iter()
|
|
.filter_map(|change| match change {
|
|
CacheInvalidation::Tenant(tenant_id) => Some(*tenant_id),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
for tenant_id in tenant_ids {
|
|
for account_id in
|
|
inbuxa_features::tenancy::members::accounts(self.registry(), tenant_id).await?
|
|
{
|
|
changes.insert(CacheInvalidation::AccessToken(account_id));
|
|
}
|
|
}
|
|
|
|
let changes = changes.into_iter().collect::<Vec<_>>();
|
|
self.invalidate_local_caches(&changes).await;
|
|
self.cluster_broadcast(BroadcastEvent::CacheInvalidate(changes))
|
|
.await;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn invalidate_all_local_caches(&self) {
|
|
self.invalidate_all_local_negative_caches();
|
|
self.inner.cache.access_tokens.clear();
|
|
self.inner.cache.domains.clear();
|
|
self.inner.cache.domain_names.clear();
|
|
self.inner.cache.emails.clear();
|
|
self.inner.cache.tenants.clear();
|
|
self.inner.cache.files.clear();
|
|
self.inner.cache.contacts.clear();
|
|
self.inner.cache.events.clear();
|
|
self.inner.cache.scheduling.clear();
|
|
self.inner.cache.dkim_signers.clear();
|
|
self.inner.cache.accounts.clear();
|
|
self.inner.cache.roles.clear();
|
|
self.inner.cache.lists.clear();
|
|
self.inner.data.logos.lock().clear();
|
|
}
|
|
|
|
pub fn invalidate_all_local_negative_caches(&self) {
|
|
self.inner.cache.domain_names_negative.clear();
|
|
self.inner.cache.emails_negative.clear();
|
|
}
|
|
|
|
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))
|
|
.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 {
|
|
CacheInvalidation::AccessToken(id) => {
|
|
cache.access_tokens.remove(id);
|
|
cache.http_auth.inner().retain(|_, v| v.account_id != *id);
|
|
}
|
|
CacheInvalidation::DavResources(id) => {
|
|
cache.files.remove(id);
|
|
cache.contacts.remove(id);
|
|
cache.events.remove(id);
|
|
cache.scheduling.remove(id);
|
|
}
|
|
CacheInvalidation::Domain(id) => {
|
|
cache.domains.remove(id);
|
|
cache.dkim_signers.remove(id);
|
|
cache.domain_names.inner().retain(|_, v| v != id);
|
|
}
|
|
CacheInvalidation::Account(id) => {
|
|
cache.accounts.remove(id);
|
|
cache.emails.inner().retain(|_, v| {
|
|
!matches!(
|
|
v,
|
|
EmailCache::Account(account_id)
|
|
| EmailCache::DisabledAccountAddress(account_id)
|
|
if account_id == id
|
|
)
|
|
});
|
|
}
|
|
CacheInvalidation::DkimSignature(id) => {
|
|
cache.dkim_signers.remove(id);
|
|
}
|
|
CacheInvalidation::Tenant(id) => {
|
|
cache.tenants.remove(id);
|
|
}
|
|
CacheInvalidation::Role(id) => {
|
|
cache.roles.remove(id);
|
|
}
|
|
CacheInvalidation::List(id) => {
|
|
cache.lists.remove(id);
|
|
cache.emails.inner().retain(|_, v| {
|
|
!matches!(
|
|
v,
|
|
EmailCache::MailingList(list_id)
|
|
| EmailCache::DisabledListAddress(list_id)
|
|
if list_id == id
|
|
)
|
|
});
|
|
}
|
|
CacheInvalidation::DomainLogo(id) => {
|
|
self.inner
|
|
.data
|
|
.logos
|
|
.lock()
|
|
.retain(|_, v| v.domain_id != *id);
|
|
}
|
|
CacheInvalidation::TenantLogo(id) => {
|
|
self.inner
|
|
.data
|
|
.logos
|
|
.lock()
|
|
.retain(|_, v| v.tenant_id != Some(*id));
|
|
}
|
|
CacheInvalidation::EmailNegative {
|
|
domain_id,
|
|
local_part_hash,
|
|
} => {
|
|
negative_emails.insert((*domain_id, *local_part_hash));
|
|
}
|
|
CacheInvalidation::DomainNegative => {
|
|
cache.domain_names_negative.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
builder.invalidate(invalidation);
|
|
builder
|
|
}
|
|
}
|