diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ed88cf..00da9537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. This projec If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. ## Added +- `is_ip_in_cidr` expression function for CIDR matching. ## Changed - Bump `mail-auth` to 0.9 (which bumps `hickory-resolver` to 0.26). @@ -22,7 +23,6 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If - ACME: - Include apex domains when requesting certificates for subdomains. - Use the public suffix list to determine the zone name when no origin is provided. -- Registry: Reload `SieveUserScript` and `SieveSystemScript` entries after they are modified or deleted. ## [0.16.4] - 2026-05-05 diff --git a/crates/common/src/expr/functions/misc.rs b/crates/common/src/expr/functions/misc.rs index 692767c7..924a6258 100644 --- a/crates/common/src/expr/functions/misc.rs +++ b/crates/common/src/expr/functions/misc.rs @@ -7,7 +7,8 @@ use crate::expr::Variable; use compact_str::CompactString; use mail_auth::common::resolver::ToReverseName; -use std::net::IpAddr; +use registry::types::ipmask::IpAddrOrMask; +use std::{net::IpAddr, str::FromStr}; pub(crate) fn fn_is_empty(v: Vec) -> Variable { match &v[0] { @@ -46,6 +47,16 @@ pub(crate) fn fn_is_ipv6_addr(v: Vec) -> Variable { .into() } +pub(crate) fn fn_is_ip_in_cidr(v: Vec) -> Variable { + let Ok(ip) = v[0].to_string().as_str().parse::() else { + return false.into(); + }; + IpAddrOrMask::from_str(v[1].to_string().as_str()) + .map(|mask| mask.matches(&ip)) + .unwrap_or(false) + .into() +} + pub(crate) fn fn_ip_reverse_name(v: Vec) -> Variable { CompactString::new( v[0].to_string() diff --git a/crates/common/src/expr/functions/mod.rs b/crates/common/src/expr/functions/mod.rs index 0f09cf1d..78cc03e3 100644 --- a/crates/common/src/expr/functions/mod.rs +++ b/crates/common/src/expr/functions/mod.rs @@ -49,6 +49,7 @@ pub(crate) const FUNCTIONS: &[(&str, fn(Vec) -> Variable, u32)] = &[ ("is_ip_addr", misc::fn_is_ip_addr, 1), ("is_ipv4_addr", misc::fn_is_ipv4_addr, 1), ("is_ipv6_addr", misc::fn_is_ipv6_addr, 1), + ("is_ip_in_cidr", misc::fn_is_ip_in_cidr, 2), ("ip_reverse_name", misc::fn_ip_reverse_name, 1), ("trim", text::fn_trim, 1), ("trim_end", text::fn_trim_end, 1), diff --git a/crates/common/src/scripts/functions/misc.rs b/crates/common/src/scripts/functions/misc.rs index 3fa6dc3d..723d337e 100644 --- a/crates/common/src/scripts/functions/misc.rs +++ b/crates/common/src/scripts/functions/misc.rs @@ -4,9 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::net::IpAddr; +use std::{net::IpAddr, str::FromStr}; use mail_auth::common::resolver::ToReverseName; +use registry::types::ipmask::IpAddrOrMask; use sha1::Sha1; use sha2::{Sha256, Sha512}; use sieve::{Context, runtime::Variable}; @@ -45,6 +46,16 @@ pub fn fn_is_ipv6_addr<'x>(_: &'x Context<'x>, v: Vec) -> Variable { .into() } +pub fn fn_is_ip_in_cidr<'x>(_: &'x Context<'x>, v: Vec) -> Variable { + let Ok(ip) = v[0].to_string().parse::() else { + return false.into(); + }; + IpAddrOrMask::from_str(v[1].to_string().as_ref()) + .map(|mask| mask.matches(&ip)) + .unwrap_or(false) + .into() +} + pub fn fn_ip_reverse_name<'x>(_: &'x Context<'x>, v: Vec) -> Variable { v[0].to_string() .parse::() diff --git a/crates/common/src/scripts/functions/mod.rs b/crates/common/src/scripts/functions/mod.rs index 6ef84006..2312e564 100644 --- a/crates/common/src/scripts/functions/mod.rs +++ b/crates/common/src/scripts/functions/mod.rs @@ -48,6 +48,7 @@ pub fn register_functions_trusted() -> FunctionMap { .with_function("is_ipv4_addr", fn_is_ipv4_addr) .with_function("is_ipv6_addr", fn_is_ipv6_addr) .with_function("ip_reverse_name", fn_ip_reverse_name) + .with_function_args("is_ip_in_cidr", fn_is_ip_in_cidr, 2) .with_function("winnow", fn_winnow) .with_function("has_zwsp", fn_has_zwsp) .with_function("has_obscured", fn_has_obscured) diff --git a/crates/jmap/src/registry/set.rs b/crates/jmap/src/registry/set.rs index dcf1f70c..ab72e643 100644 --- a/crates/jmap/src/registry/set.rs +++ b/crates/jmap/src/registry/set.rs @@ -28,11 +28,8 @@ use crate::registry::{ }, }; use common::{ - Server, - auth::AccessToken, - cache::invalidate::CacheInvalidationBuilder, - expr::if_block::BootstrapExprExt, - ipc::{BroadcastEvent, CacheInvalidation, RegistryChange}, + Server, auth::AccessToken, cache::invalidate::CacheInvalidationBuilder, + expr::if_block::BootstrapExprExt, ipc::CacheInvalidation, }; use http_proto::HttpSessionData; use jmap_proto::{ @@ -681,30 +678,6 @@ impl RegistrySet for Server { // Finalize cache invalidation self.invalidate_caches(cache_invalidator).await?; - // Sieve scripts: refresh the in-memory compiled script maps - if matches!( - object_type, - ObjectType::SieveUserScript | ObjectType::SieveSystemScript - ) && (!set.response.created.is_empty() - || !set.response.updated.is_empty() - || !set.response.destroyed.is_empty()) - { - let result = Box::pin( - set.server - .reload_registry(RegistryChange::Reload(object_type)), - ) - .await?; - if !result.has_errors() { - set.server - .cluster_broadcast(BroadcastEvent::RegistryChange( - RegistryChange::Reload(object_type), - )) - .await; - } else { - result.log(); - } - } - Ok(set.into_response()) } ObjectType::ArfExternalReport