#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum MilterActions {
Reject = 11,
AddHeader = 6,
Greylist = 4,
Pass = 0,
}
impl SPFResult {
fn weight(&self) -> f32 {
match self {
Self::RSpfAllow => -1.0,
Self::RSpfNa => 1.5,
Self::RSpfSoftfail => 2.5,
Self::RSpfFail => 4.5,
}
}
}
impl DKIMResult {
fn weight(&self) -> f32 {
match self {
Self::RDkimAllow => -1.0,
Self::RDkimNa => 1.0,
Self::RDkimTempfail => 1.5,
Self::RDkimPermfail => 4.5,
}
}
}
impl DMARCResult {
fn weight(&self) -> f32 {
match self {
Self::DmarcPolicyAllow => -1.0,
Self::DmarcPolicyAllowWithFailures => 0.0,
Self::DmarcPolicyNa => 0.5,
Self::DmarcPolicySoftfail => 1.5,
Self::DmarcPolicyQuarantine => 3.0,
Self::DmarcPolicyReject => 5.5,
}
}
}
impl MilterActions {
fn into_float(self) -> f32 {
self as usize as f32
}
fn from_combination(combination: Combination) -> (Self, f32) {
let score = combination.0.weight() + combination.1.weight() + combination.2.weight();
let action = if score >= Self::Reject.into_float() {
Self::Reject
} else if score >= Self::AddHeader.into_float() {
Self::AddHeader
} else if score >= Self::Greylist.into_float() {
Self::Greylist
} else {
Self::Pass
};
(action, score)
}
}
impl std::fmt::Display for MilterActions {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let self_string = match self {
Self::Reject => "`reject`",
Self::AddHeader => "`add_header`",
Self::Greylist => "`greylist`",
Self::Pass => "`pass`",
};
self_string.fmt(f)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum SPFResult {
RSpfAllow,
RSpfNa,
RSpfSoftfail,
RSpfFail,
}
impl std::fmt::Display for SPFResult {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::RSpfAllow => "`R_SPF_ALLOW`",
Self::RSpfNa => "`R_SPF_NA`",
Self::RSpfSoftfail => "`R_SPF_SOFTFAIL`",
Self::RSpfFail => "`R_SPF_FAIL`",
}.fmt(f)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum DKIMResult {
RDkimAllow,
RDkimNa,
RDkimTempfail,
RDkimPermfail,
}
impl std::fmt::Display for DKIMResult {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::RDkimAllow => "`R_DKIM_ALLOW`",
Self::RDkimNa => "`R_DKIM_NA`",
Self::RDkimTempfail => "`R_DKIM_TEMPFAIL`",
Self::RDkimPermfail => "`R_DKIM_PERMFAIL`",
}.fmt(f)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum DMARCResult {
DmarcPolicyAllow,
DmarcPolicyNa,
DmarcPolicySoftfail,
DmarcPolicyAllowWithFailures,
DmarcPolicyQuarantine,
DmarcPolicyReject,
}
impl std::fmt::Display for DMARCResult {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::DmarcPolicyAllow => "`DMARC_POLICY_ALLOW`",
Self::DmarcPolicyNa => "`DMARC_POLICY_NA`",
Self::DmarcPolicySoftfail => "`DMARC_POLICY_SOFTFAIL`",
Self::DmarcPolicyAllowWithFailures => "`DMARC_POLICY_ALLOW_WITH_FAILURES`",
Self::DmarcPolicyQuarantine => "`DMARC_POLICY_QUARANTINE`",
Self::DmarcPolicyReject => "`DMARC_POLICY_REJECT`",
}
.fmt(f)
}
}
type Combination = (SPFResult, DKIMResult, DMARCResult);
fn produce_combinations() -> Vec<Combination> {
let mut combinations = vec![];
use DKIMResult::*;
use DMARCResult::*;
use SPFResult::*;
for spf in [RSpfAllow, RSpfNa, RSpfSoftfail, RSpfFail] {
for dkim in [RDkimAllow, RDkimNa, RDkimTempfail, RDkimPermfail] {
// SPF and DKIM succeeded
if spf == RSpfAllow && dkim == RDkimAllow {
combinations.push((spf, dkim, DmarcPolicyAllow));
// SPF failed
} else if spf == RSpfNa || spf == RSpfSoftfail || spf == RSpfFail {
if dkim == RDkimAllow {
combinations.push((spf, dkim, DmarcPolicyAllowWithFailures));
} else {
combinations.push((spf, dkim, DmarcPolicyQuarantine));
combinations.push((spf, dkim, DmarcPolicyReject));
}
// DKIM failed
} else if dkim == RDkimNa || dkim == RDkimTempfail || dkim == RDkimPermfail {
if spf == RSpfAllow {
combinations.push((spf, dkim, DmarcPolicyAllowWithFailures));
} else {
combinations.push((spf, dkim, DmarcPolicyQuarantine));
combinations.push((spf, dkim, DmarcPolicyReject));
}
} else {
panic!("Leftover combination: {} {}", spf, dkim);
}
// these can always happen
combinations.push((spf, dkim, DmarcPolicyNa));
combinations.push((spf, dkim, DmarcPolicySoftfail));
}
}
combinations
}
fn print_row(combination: Combination) {
let (action, score) = MilterActions::from_combination(combination);
println!(
"| {:16} ({:>4}) | {:17} ({:>4}) | {:34} ({:>4}) | {:12} ({:>4}) |",
combination.0,
combination.0.weight(),
combination.1,
combination.1.weight(),
combination.2,
combination.2.weight(),
action,
score
);
}
fn main() {
println!(
"| {:16} | {:17} | {:34} | {:12} |",
"SPF", "DKIM", "DMARC", "Action"
);
println!("| :---------------------- | :----------------------- | :---------------------------------------- | :------------------ |");
for combination in produce_combinations() {
print_row(combination);
}
}
#[test]
fn combinations_produce_correct_milter_action() {
use DKIMResult::*;
use DMARCResult::*;
use SPFResult::*;
let combinations = produce_combinations();
macro_rules! is_equal {
($combination:expr, $milter_action:expr) => {
assert!(combinations.contains(&$combination));
assert!(MilterActions::from_combination($combination).0 == $milter_action);
};
}
is_equal!(
(RSpfAllow, RDkimAllow, DmarcPolicyAllow),
MilterActions::Pass
);
is_equal!(
(RSpfAllow, RDkimNa, DmarcPolicyAllowWithFailures),
MilterActions::Pass
);
is_equal!(
(RSpfAllow, RDkimTempfail, DmarcPolicyAllowWithFailures),
MilterActions::Pass
);
is_equal!(
(RSpfAllow, RDkimPermfail, DmarcPolicyAllowWithFailures),
MilterActions::Pass
);
is_equal!(
(RSpfAllow, RDkimPermfail, DmarcPolicyAllowWithFailures),
MilterActions::Pass
);
is_equal!(
(RSpfAllow, RDkimPermfail, DmarcPolicyAllowWithFailures),
MilterActions::Pass
);
is_equal!(
(RSpfNa, RDkimPermfail, DmarcPolicyReject),
MilterActions::Reject
);
is_equal!(
(RSpfFail, RDkimNa, DmarcPolicyReject),
MilterActions::Reject
);
is_equal!(
(RSpfFail, RDkimTempfail, DmarcPolicyReject),
MilterActions::Reject
);
is_equal!(
(RSpfFail, RDkimPermfail, DmarcPolicyReject),
MilterActions::Reject
);
}
#[test]
fn certain_combinations_must_not_exist() {
use DKIMResult::*;
use DMARCResult::*;
use SPFResult::*;
let combinations = produce_combinations();
macro_rules! must_not_exist {
($combination:expr) => {
assert!(!combinations.contains(&$combination));
};
}
must_not_exist!((RSpfAllow, RDkimAllow, DmarcPolicyReject));
must_not_exist!((RSpfNa, RDkimAllow, DmarcPolicyReject));
must_not_exist!((RSpfSoftfail, RDkimAllow, DmarcPolicyReject));
must_not_exist!((RSpfFail, RDkimAllow, DmarcPolicyReject));
must_not_exist!((RSpfAllow, RDkimNa, DmarcPolicyReject));
must_not_exist!((RSpfAllow, RDkimTempfail, DmarcPolicyReject));
must_not_exist!((RSpfAllow, RDkimTempfail, DmarcPolicyReject));
must_not_exist!((RSpfAllow, RDkimAllow, DmarcPolicyQuarantine));
must_not_exist!((RSpfNa, RDkimAllow, DmarcPolicyQuarantine));
must_not_exist!((RSpfSoftfail, RDkimAllow, DmarcPolicyQuarantine));
must_not_exist!((RSpfFail, RDkimAllow, DmarcPolicyQuarantine));
must_not_exist!((RSpfAllow, RDkimNa, DmarcPolicyQuarantine));
must_not_exist!((RSpfAllow, RDkimTempfail, DmarcPolicyQuarantine));
must_not_exist!((RSpfAllow, RDkimTempfail, DmarcPolicyQuarantine));
}
About
Rspamd should comply with RFC7489 to the best of its abilities.
DMS' Action Scores
Milter action scores are configured to:
Symbols
DMARC policies can be
DKIM policies can be
SPF policies can be
Behavior
Here is a table of possible combinations of what can happen with SPF, DKIM & DMARC.
R_SPF_ALLOW( -1)R_DKIM_ALLOW( -1)DMARC_POLICY_ALLOW( -1)pass( -3)R_SPF_ALLOW( -1)R_DKIM_ALLOW( -1)DMARC_POLICY_NA( 0.5)pass(-1.5)R_SPF_ALLOW( -1)R_DKIM_ALLOW( -1)DMARC_POLICY_SOFTFAIL( 1.5)pass(-0.5)R_SPF_ALLOW( -1)R_DKIM_NA( 1)DMARC_POLICY_ALLOW_WITH_FAILURES( 0)pass( 0)R_SPF_ALLOW( -1)R_DKIM_NA( 1)DMARC_POLICY_NA( 0.5)pass( 0.5)R_SPF_ALLOW( -1)R_DKIM_NA( 1)DMARC_POLICY_SOFTFAIL( 1.5)pass( 1.5)R_SPF_ALLOW( -1)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_ALLOW_WITH_FAILURES( 0)pass( 0.5)R_SPF_ALLOW( -1)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_NA( 0.5)pass( 1)R_SPF_ALLOW( -1)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_SOFTFAIL( 1.5)pass( 2)R_SPF_ALLOW( -1)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_ALLOW_WITH_FAILURES( 0)pass( 3.5)R_SPF_ALLOW( -1)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_NA( 0.5)greylist( 4)R_SPF_ALLOW( -1)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_SOFTFAIL( 1.5)greylist( 5)R_SPF_NA( 1.5)R_DKIM_ALLOW( -1)DMARC_POLICY_ALLOW_WITH_FAILURES( 0)pass( 0.5)R_SPF_NA( 1.5)R_DKIM_ALLOW( -1)DMARC_POLICY_NA( 0.5)pass( 1)R_SPF_NA( 1.5)R_DKIM_ALLOW( -1)DMARC_POLICY_SOFTFAIL( 1.5)pass( 2)R_SPF_NA( 1.5)R_DKIM_NA( 1)DMARC_POLICY_QUARANTINE( 3)greylist( 5.5)R_SPF_NA( 1.5)R_DKIM_NA( 1)DMARC_POLICY_REJECT( 5.5)add_header( 8)R_SPF_NA( 1.5)R_DKIM_NA( 1)DMARC_POLICY_NA( 0.5)pass( 3)R_SPF_NA( 1.5)R_DKIM_NA( 1)DMARC_POLICY_SOFTFAIL( 1.5)greylist( 4)R_SPF_NA( 1.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_QUARANTINE( 3)add_header( 6)R_SPF_NA( 1.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_REJECT( 5.5)add_header( 8.5)R_SPF_NA( 1.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_NA( 0.5)pass( 3.5)R_SPF_NA( 1.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_SOFTFAIL( 1.5)greylist( 4.5)R_SPF_NA( 1.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_QUARANTINE( 3)add_header( 9)R_SPF_NA( 1.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_REJECT( 5.5)reject(11.5)R_SPF_NA( 1.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_NA( 0.5)add_header( 6.5)R_SPF_NA( 1.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_SOFTFAIL( 1.5)add_header( 7.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_ALLOW( -1)DMARC_POLICY_ALLOW_WITH_FAILURES( 0)pass( 1.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_ALLOW( -1)DMARC_POLICY_NA( 0.5)pass( 2)R_SPF_SOFTFAIL( 2.5)R_DKIM_ALLOW( -1)DMARC_POLICY_SOFTFAIL( 1.5)pass( 3)R_SPF_SOFTFAIL( 2.5)R_DKIM_NA( 1)DMARC_POLICY_QUARANTINE( 3)add_header( 6.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_NA( 1)DMARC_POLICY_REJECT( 5.5)add_header( 9)R_SPF_SOFTFAIL( 2.5)R_DKIM_NA( 1)DMARC_POLICY_NA( 0.5)greylist( 4)R_SPF_SOFTFAIL( 2.5)R_DKIM_NA( 1)DMARC_POLICY_SOFTFAIL( 1.5)greylist( 5)R_SPF_SOFTFAIL( 2.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_QUARANTINE( 3)add_header( 7)R_SPF_SOFTFAIL( 2.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_REJECT( 5.5)add_header( 9.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_NA( 0.5)greylist( 4.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_SOFTFAIL( 1.5)greylist( 5.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_QUARANTINE( 3)add_header( 10)R_SPF_SOFTFAIL( 2.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_REJECT( 5.5)reject(12.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_NA( 0.5)add_header( 7.5)R_SPF_SOFTFAIL( 2.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_SOFTFAIL( 1.5)add_header( 8.5)R_SPF_FAIL( 4.5)R_DKIM_ALLOW( -1)DMARC_POLICY_ALLOW_WITH_FAILURES( 0)pass( 3.5)R_SPF_FAIL( 4.5)R_DKIM_ALLOW( -1)DMARC_POLICY_NA( 0.5)greylist( 4)R_SPF_FAIL( 4.5)R_DKIM_ALLOW( -1)DMARC_POLICY_SOFTFAIL( 1.5)greylist( 5)R_SPF_FAIL( 4.5)R_DKIM_NA( 1)DMARC_POLICY_QUARANTINE( 3)add_header( 8.5)R_SPF_FAIL( 4.5)R_DKIM_NA( 1)DMARC_POLICY_REJECT( 5.5)reject( 11)R_SPF_FAIL( 4.5)R_DKIM_NA( 1)DMARC_POLICY_NA( 0.5)add_header( 6)R_SPF_FAIL( 4.5)R_DKIM_NA( 1)DMARC_POLICY_SOFTFAIL( 1.5)add_header( 7)R_SPF_FAIL( 4.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_QUARANTINE( 3)add_header( 9)R_SPF_FAIL( 4.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_REJECT( 5.5)reject(11.5)R_SPF_FAIL( 4.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_NA( 0.5)add_header( 6.5)R_SPF_FAIL( 4.5)R_DKIM_TEMPFAIL( 1.5)DMARC_POLICY_SOFTFAIL( 1.5)add_header( 7.5)R_SPF_FAIL( 4.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_QUARANTINE( 3)reject( 12)R_SPF_FAIL( 4.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_REJECT( 5.5)reject(14.5)R_SPF_FAIL( 4.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_NA( 0.5)add_header( 9.5)R_SPF_FAIL( 4.5)R_DKIM_PERMFAIL( 4.5)DMARC_POLICY_SOFTFAIL( 1.5)add_header(10.5)Rspamd Configuration File
Please see
scores.d/policies_group.confThe Code that Generates All of This
Click me to unveil the Rust code behind the scenes.
You can copy this code and run it to print the table seen above. When you use Cargo, you may also use
cargo testto check whether changes still conform to specifications.Additional Rspamd Symbols
Rspamd has so-called composite symbols that trigger when a condition is met. Especially
AUTH_NAandAUTH_NA_OR_FAILwill adjust the scores of various lines in the table above. This needs to be taken into account.You Think There is Something Wrong Here?
In case you think a value should be changed, please copy the Rust code, apply your changes to the top, and then test the result. You may add additional tests to
combinations_produce_correct_milter_actionas well. Please justify why you disagree with the current setup.