Phase 2e of the DLP and mail flow rules spec, in the features crate. - rules.rs: a rule (§2.2) with its conditions (§2.3) and actions (§2.4), as JSON under R/r in the fork's subspace. validate() enforces the spec's shape: DLP rules check outgoing mail and have exactly one of block, warn or hold; transport rules have neither those nor detectors; lists, header names, header values (one line), addresses, texts, word lists, patterns and detector ids are checked. - engine.rs: rules compiled once (word lists to automata, patterns to size-limited regexes) and run in priority order with exceptions and stop processing. Each detector runs at most once per message and only when a rule asks for it. The outcome lists what matched with each detector's count, and decides DLP strictest first: block, hold, warn; an override answers warnings only (§2.5). - cache.rs: each node's compiled copy, refreshed after 30 seconds or at once when this node changes a rule. Nothing calls this yet: the JMAP object and the check at DATA follow. 55 unit tests in mailflow.
54 lines
1.6 KiB
Rust
54 lines
1.6 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
//! The compiled rules, kept per node so a message doesn't read the store.
|
|
//! A change made on this node applies at once; one made on another node
|
|
//! within [`TTL`], when the copy here is next refreshed.
|
|
|
|
use super::{engine::Compiled, rules};
|
|
use std::{
|
|
sync::{Arc, RwLock},
|
|
time::{Duration, Instant},
|
|
};
|
|
use store::Store;
|
|
|
|
/// How long a node keeps its copy before reading the rules again.
|
|
pub const TTL: Duration = Duration::from_secs(30);
|
|
|
|
static CACHE: RwLock<Option<(Instant, Arc<Compiled>)>> = RwLock::new(None);
|
|
|
|
/// Forgets the copy, so the next message reads the rules again.
|
|
pub fn invalidate() {
|
|
if let Ok(mut cache) = CACHE.write() {
|
|
*cache = None;
|
|
}
|
|
}
|
|
|
|
/// The enabled rules, compiled. A rule that no longer compiles is left out
|
|
/// and reported, once per refresh.
|
|
pub async fn compiled(data: &Store) -> trc::Result<Arc<Compiled>> {
|
|
if let Ok(cache) = CACHE.read()
|
|
&& let Some((at, compiled)) = cache.as_ref()
|
|
&& at.elapsed() < TTL
|
|
{
|
|
return Ok(compiled.clone());
|
|
}
|
|
let (compiled, skipped) = Compiled::new(&rules::all(data).await?);
|
|
for (id, reason) in skipped {
|
|
trc::event!(
|
|
Store(trc::StoreEvent::DataCorruption),
|
|
Id = u64::from(id),
|
|
Reason = reason,
|
|
Details = "Mail rule skipped: it no longer compiles"
|
|
);
|
|
}
|
|
let compiled = Arc::new(compiled);
|
|
if let Ok(mut cache) = CACHE.write() {
|
|
*cache = Some((Instant::now(), compiled.clone()));
|
|
}
|
|
Ok(compiled)
|
|
}
|