Merge upstream v0.16.23

Five conflicts, resolved:

- crates/common/src/auth/authentication.rs: upstream's get_directory_for_token
  and JwtClaims replace extract_jwt_domain; the per-domain directory code
  (DIR-1, DIR-5 to DIR-7) is kept, and the token lookup routes through it.
  The release's one new Enterprise snippet was the body of
  get_directory_for_issuer, which stays returning None: a token naming no
  address gets the server default, as DIR-2 specifies and as v0.16.22 did.
- crates/common/src/manager/application.rs: upstream's rewrite of the tests,
  with the temp directory names renamed again, and the 5(a) notice the
  name-purge change should have added.
- crates/common/src/network/mta.rs: both sides' imports.
- crates/main/Cargo.toml: the AGPL-only license kept, version 0.16.23.
- Cargo.lock: upstream's, with the fork's crates added by Cargo.
This commit is contained in:
2026-09-22 16:57:06 -07:00
82 changed files with 1527 additions and 611 deletions
+57 -1
View File
@@ -23,6 +23,7 @@ use crate::{
manager::SPAM_CLASSIFIER_KEY,
network::RcptResolution,
};
use ahash::AHashSet;
use directory::Recipient;
use mail_auth::IpLookupStrategy;
use registry::schema::enums::ExpressionVariable;
@@ -37,6 +38,8 @@ use store::{
write::{AlignedBytes, Archive, QueueClass, ValueClass},
};
use trc::{AddContext, SpamEvent};
use types::id::Id;
use utils::DomainPart;
impl Server {
pub async fn rcpt_resolve(
@@ -163,7 +166,10 @@ impl Server {
}
EmailCache::MailingList(id) => {
if let Some(list) = self.try_list(id).await? {
return Ok(RcptResolution::Expand(list.recipients.clone()));
return Ok(RcptResolution::Expand(
self.expand_nested_lists(id, list.recipients.clone())
.await?,
));
} else {
self.inner
.cache
@@ -195,6 +201,56 @@ impl Server {
}
}
async fn expand_nested_lists(
&self,
list_id: u32,
recipients: Arc<[Box<str>]>,
) -> trc::Result<Arc<[Box<str>]>> {
let mut has_nested = false;
for member in recipients.iter() {
if let Some(EmailCache::MailingList(_)) = self.rcpt_id_from_email(member).await? {
has_nested = true;
break;
}
}
if !has_nested {
return Ok(recipients);
}
let mut expanded = Vec::with_capacity(recipients.len());
let mut seen: AHashSet<Box<str>> = AHashSet::with_capacity(recipients.len());
let mut visited = AHashSet::from_iter([list_id]);
let mut pending: Vec<Arc<[Box<str>]>> = Vec::new();
let mut members = recipients;
loop {
for member in members.iter() {
if let Some(EmailCache::MailingList(nested_id)) =
self.rcpt_id_from_email(member).await?
{
if !visited.insert(nested_id) {
continue;
}
if let Some(nested) = self.try_list(nested_id).await? {
pending.push(nested.recipients.clone());
continue;
}
}
if seen.insert(member.to_canonical_address().into()) {
expanded.push(member.clone());
}
}
let Some(next) = pending.pop() else {
break;
};
members = next;
}
Ok(expanded.into())
}
pub async fn get_dkim_signers(
&self,
domain: &str,