Phase 2f of the DLP and mail flow rules spec: the rules now run on mail
an authenticated sender submits, after the DATA system script and
before headers and DKIM signing (§2.1).
- smtp/inbound/mailflow.rs: builds what the rules look at from the
message (subject, the text version of each body, one level of attached
messages, attachment text via the extractor, 10 MB of text at most)
and the envelope (sender's groups and tenant; each recipient local or
not, and its groups). Skipped entirely when no enabled rule applies to
outgoing mail. Rules that can't be loaded refuse with a 451: nothing
unchecked leaves.
- Block: 550 5.7.1 with the rule's notice. Warn: 550 5.7.1 with the
notice and how to override: "[override: reason]" at the start of the
subject, taken out before the message goes on (settled answer 1).
Until phase 3, a hold rule blocks rather than let mail through.
- JMAP: EmailSubmission takes inbuxa:dlpOverride {reason}; a refusal
comes back as inbuxa:dlpWarning or inbuxa:dlpBlocked with each rule's
name and notice (description too, for older clients).
- Audit: one record per DLP match, the sender as actor, action create,
target a message: the recipient domains, each rule with its detectors'
counts, the outcome, an override's reason. Never the matched text. No
new audit action: an older node that meets one fails its daily
clean-up, which would make rolling back unsafe (spec §2.7 updated).
Tests: mail_rules_tests gains the DLP flow over JMAP (no rules, warning
with rule and notice, local recipient not warned, override with a
reason, block that no reason passes, the subject tag stripped from the
delivered message, audit records with no card or key text). smtp
inbound tests pass; system_tests passed twice after one timeout in the
email delivery tests that didn't recur.
147 lines
3.6 KiB
Rust
147 lines
3.6 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 mail_auth::{DkimResult, DmarcResult, IprevResult, SpfResult, dmarc::Policy};
|
|
use std::borrow::Cow;
|
|
|
|
pub mod auth;
|
|
pub mod data;
|
|
pub mod dkim;
|
|
pub mod ehlo;
|
|
pub mod hooks;
|
|
pub mod mail;
|
|
pub mod mailflow; // inbuxa: DLP and mail flow rules
|
|
pub mod milter;
|
|
pub mod rcpt;
|
|
pub mod session;
|
|
pub mod spam;
|
|
pub mod spawn;
|
|
pub mod vrfy;
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct FilterResponse {
|
|
pub message: Cow<'static, str>,
|
|
pub disconnect: bool,
|
|
}
|
|
|
|
pub trait AuthResult {
|
|
fn as_str(&self) -> &'static str;
|
|
}
|
|
|
|
impl AuthResult for SpfResult {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
SpfResult::Pass => "pass",
|
|
SpfResult::Fail => "fail",
|
|
SpfResult::SoftFail => "softfail",
|
|
SpfResult::Neutral => "neutral",
|
|
SpfResult::None => "none",
|
|
SpfResult::TempError => "temperror",
|
|
SpfResult::PermError => "permerror",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AuthResult for IprevResult {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
IprevResult::Pass => "pass",
|
|
IprevResult::Fail(_) => "fail",
|
|
IprevResult::TempError(_) => "temperror",
|
|
IprevResult::PermError(_) => "permerror",
|
|
IprevResult::None => "none",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AuthResult for DkimResult {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
DkimResult::Pass => "pass",
|
|
DkimResult::None => "none",
|
|
DkimResult::Neutral(_) => "neutral",
|
|
DkimResult::Fail(_) => "fail",
|
|
DkimResult::PermError(_) => "permerror",
|
|
DkimResult::TempError(_) => "temperror",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AuthResult for DmarcResult {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
DmarcResult::Pass => "pass",
|
|
DmarcResult::Fail(_) => "fail",
|
|
DmarcResult::TempError(_) => "temperror",
|
|
DmarcResult::PermError(_) => "permerror",
|
|
DmarcResult::None => "none",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AuthResult for Policy {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Policy::Reject => "reject",
|
|
Policy::Quarantine => "quarantine",
|
|
Policy::None | Policy::Unspecified => "none",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FilterResponse {
|
|
pub fn accept() -> Self {
|
|
Self {
|
|
message: Cow::Borrowed("250 2.0.0 Message queued for delivery.\r\n"),
|
|
disconnect: false,
|
|
}
|
|
}
|
|
|
|
pub fn reject() -> Self {
|
|
Self {
|
|
message: Cow::Borrowed("503 5.5.3 Message rejected.\r\n"),
|
|
disconnect: false,
|
|
}
|
|
}
|
|
|
|
pub fn temp_fail() -> Self {
|
|
Self {
|
|
message: Cow::Borrowed("451 4.3.5 Unable to accept message at this time.\r\n"),
|
|
disconnect: false,
|
|
}
|
|
}
|
|
|
|
pub fn shutdown() -> Self {
|
|
Self {
|
|
message: Cow::Borrowed("421 4.3.0 Server shutting down.\r\n"),
|
|
disconnect: true,
|
|
}
|
|
}
|
|
|
|
pub fn server_failure() -> Self {
|
|
Self {
|
|
message: Cow::Borrowed("451 4.3.5 Unable to accept message at this time.\r\n"),
|
|
disconnect: false,
|
|
}
|
|
}
|
|
|
|
pub fn disconnect(self) -> Self {
|
|
Self {
|
|
disconnect: true,
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub fn into_bytes(self) -> Cow<'static, [u8]> {
|
|
match self.message {
|
|
Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()),
|
|
Cow::Owned(s) => Cow::Owned(s.into_bytes()),
|
|
}
|
|
}
|
|
}
|