use crate::{
auth::SpfResultKind,
header::{format, HeaderField, HEADER_LINE_WIDTH},
verify::{Identity, VerificationResult},
};
use std::{
borrow::Cow,
fmt::{self, Display, Formatter},
net::IpAddr,
};
use viaspf::{SpfResult, SpfResultCause};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ReceivedSpfHeader {
body_pieces: Vec<String>,
}
impl ReceivedSpfHeader {
pub const NAME: &'static str = "Received-SPF";
pub fn new(
result: &VerificationResult,
hostname: &str,
ip: IpAddr,
helo_host: Option<&str>,
) -> Self {
Self {
body_pieces: prepare_field_body(result, hostname, ip, helo_host),
}
}
}
fn prepare_field_body(
result: &VerificationResult,
hostname: &str,
ip: IpAddr,
helo_host: Option<&str>,
) -> Vec<String> {
let mut parts = Vec::new();
let VerificationResult { identity, spf_result, cause } = result;
parts.push(spf_result.kind().into());
add_comment(&mut parts, spf_result, hostname, ip, identity);
add_key_value_pairs(
&mut parts,
spf_result,
cause.as_ref(),
hostname,
ip,
helo_host,
identity,
);
parts
}
fn add_comment(
parts: &mut Vec<String>,
spf_result: &SpfResult,
hostname: &str,
ip: IpAddr,
identity: &Identity,
) {
use SpfResult::*;
match spf_result {
Pass => format_pass_parts(parts, hostname, ip, identity),
Fail(_) => format_fail_parts(parts, hostname, ip, identity),
Softfail => format_softfail_parts(parts, hostname, ip, identity),
Neutral => format_neutral_parts(parts, hostname, ip, identity),
None => format_none_parts(parts, hostname, identity),
Temperror => format_temperror_parts(parts, hostname, identity),
Permerror => format_permerror_parts(parts, hostname, identity),
}
}
fn format_pass_parts(parts: &mut Vec<String>, hostname: &str, ip: IpAddr, identity: &Identity) {
parts.push(format!("({}:", format::escape_comment_word(hostname)));
format_sender(parts, identity);
parts.extend("has authorized host".split_whitespace().map(From::from));
parts.push(format!("{ip})"));
}
fn format_fail_parts(parts: &mut Vec<String>, hostname: &str, ip: IpAddr, identity: &Identity) {
parts.push(format!("({}:", format::escape_comment_word(hostname)));
format_sender(parts, identity);
parts.extend("has not authorized host".split_whitespace().map(From::from));
parts.push(format!("{ip})"));
}
fn format_softfail_parts(parts: &mut Vec<String>, hostname: &str, ip: IpAddr, identity: &Identity) {
parts.push(format!("({}:", format::escape_comment_word(hostname)));
format_sender(parts, identity);
parts.extend("discourages use of host".split_whitespace().map(From::from));
parts.push(format!("{ip})"));
}
fn format_neutral_parts(parts: &mut Vec<String>, hostname: &str, ip: IpAddr, identity: &Identity) {
parts.push(format!("({}:", format::escape_comment_word(hostname)));
format_sender(parts, identity);
parts.extend("makes no definitive authorization statement for host".split_whitespace().map(From::from));
parts.push(format!("{ip})"));
}
fn format_none_parts(parts: &mut Vec<String>, hostname: &str, identity: &Identity) {
parts.push(format!("({}:", format::escape_comment_word(hostname)));
parts.extend("no authorization information available for sender".split_whitespace().map(From::from));
parts.push(format!("{})", format::escape_comment_word(identity.as_ref())));
}
fn format_temperror_parts(parts: &mut Vec<String>, hostname: &str, identity: &Identity) {
parts.push(format!("({}:", format::escape_comment_word(hostname)));
parts.push("sender".into());
parts.push(format::escape_comment_word(identity.as_ref()).into());
parts.extend("could not be authorized due to a transient DNS error)".split_whitespace().map(From::from));
}
fn format_permerror_parts(parts: &mut Vec<String>, hostname: &str, identity: &Identity) {
parts.push(format!("({}:", format::escape_comment_word(hostname)));
parts.push("sender".into());
parts.push(format::escape_comment_word(identity.as_ref()).into());
parts.extend("could not be authorized due to a permanent error in SPF records)".split_whitespace().map(From::from));
}
fn format_sender(parts: &mut Vec<String>, sender: &Identity) {
parts.push("domain".into());
if let Identity::MailFrom(_) = sender {
parts.push("of".into());
}
parts.push(format::escape_comment_word(sender.as_ref()).into());
}
fn add_key_value_pairs(
parts: &mut Vec<String>,
spf_result: &SpfResult,
cause: Option<&SpfResultCause>,
hostname: &str,
ip: IpAddr,
helo_host: Option<&str>,
identity: &Identity,
) {
let kvs = make_key_value_pairs(spf_result, cause, hostname, ip, helo_host, identity);
if let [kvs @ .., last] = &kvs[..] {
for (key, value) in kvs {
parts.push(format!("{key}={};", format::encode_value(value)));
}
let (key, value) = last;
parts.push(format!("{key}={}", format::encode_value(value)));
}
}
fn make_key_value_pairs<'a>(
spf_result: &'a SpfResult,
cause: Option<&'a SpfResultCause>,
hostname: &'a str,
ip: IpAddr,
helo_host: Option<&'a str>,
identity: &'a Identity,
) -> Vec<(&'static str, Cow<'a, str>)> {
let mut kvs = Vec::new();
kvs.push(("receiver", hostname.into()));
kvs.push(("client-ip", ip.to_string().into()));
if let Some(helo_host) = helo_host {
kvs.push(("helo", helo_host.into()));
}
if let Identity::MailFrom(mail_from) = &identity {
kvs.push(("envelope-from", mail_from.into()));
}
kvs.push(("identity", identity.name().into()));
match cause {
Some(SpfResultCause::Match(mechanism)) => {
kvs.push(("mechanism", mechanism.to_string().into()));
}
Some(SpfResultCause::Error(error)) => {
kvs.push(("problem", error.to_string().into()));
}
None => {
if let SpfResult::Neutral = spf_result {
kvs.push(("mechanism", "default".into()));
}
}
}
kvs
}
impl Display for ReceivedSpfHeader {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", Self::NAME, self.body_pieces.join(" "))
}
}
impl HeaderField for ReceivedSpfHeader {
fn name(&self) -> &'static str {
Self::NAME
}
fn format_body(&self) -> String {
format_header_value(
Self::NAME.len() + 1, HEADER_LINE_WIDTH,
&self.body_pieces,
)
}
}
fn format_header_value<I, S>(initlen: usize, limit: usize, items: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut i = initlen;
let mut value = String::new();
for (n, item) in items.into_iter().enumerate() {
let item = item.as_ref();
let len = item.chars().count() + 1; if i + len <= limit {
if n != 0 {
value.push(' ');
}
} else {
if n != 0 {
value.push_str("\n\t");
i = 0;
}
};
value.push_str(item);
i += len;
}
value
}
#[cfg(test)]
mod tests {
use super::*;
use viaspf::{
record::{Ip4CidrLength, Mechanism, A},
ErrorCause,
};
#[test]
fn format_pass_parts_ok() {
let mut parts = Vec::new();
format_pass_parts(
&mut parts,
"mail.example.org",
IpAddr::from([1, 2, 3, 4]),
&Identity::MailFrom("amy@example.com".into()),
);
assert_eq!(
parts.join(" "),
"(mail.example.org: domain of amy@example.com has authorized host 1.2.3.4)",
);
}
#[test]
fn format_none_parts_with_unusual_sender() {
let mut parts = Vec::new();
format_none_parts(
&mut parts,
"mail.example.org",
&Identity::MailFrom("\"what(!)\"@example.com".into()),
);
assert_eq!(
parts.join(" "),
"(mail.example.org: no authorization information available for sender \"what\\(!\\)\"@example.com)",
);
}
#[test]
fn received_spf_header_display_pass() {
let result = VerificationResult {
identity: Identity::MailFrom("me@example.org".into()),
spf_result: SpfResult::Pass,
cause: Some(SpfResultCause::Match(Mechanism::A(A {
domain_spec: None,
prefix_len: Some(Ip4CidrLength::new(24).unwrap().into()),
}))),
};
let header = ReceivedSpfHeader::new(
&result,
"mail.example.com",
IpAddr::from([1, 2, 3, 4]),
Some("mail.example.org"),
);
assert_eq!(
header.to_string(),
"Received-SPF: \
pass \
(mail.example.com: domain of me@example.org has authorized host 1.2.3.4) \
receiver=mail.example.com; \
client-ip=1.2.3.4; \
helo=mail.example.org; \
envelope-from=\"me@example.org\"; \
identity=mailfrom; \
mechanism=a/24"
);
}
#[test]
fn received_spf_header_display_error() {
let result = VerificationResult {
identity: Identity::Helo("mail.example.org".into()),
spf_result: SpfResult::Temperror,
cause: Some(SpfResultCause::Error(ErrorCause::Timeout)),
};
let header = ReceivedSpfHeader::new(
&result,
"mail.example.com",
IpAddr::from([1, 2, 3, 4]),
Some("mail.example.org"),
);
assert_eq!(
header.to_string(),
"Received-SPF: \
temperror \
(mail.example.com: sender mail.example.org could not be authorized due to a transient DNS error) \
receiver=mail.example.com; \
client-ip=1.2.3.4; \
helo=mail.example.org; \
identity=helo; \
problem=\"DNS lookup timed out\""
);
}
#[test]
fn received_spf_header_display_default_neutral_result() {
let result = VerificationResult {
identity: Identity::MailFrom("me@example.org".into()),
spf_result: SpfResult::Neutral,
cause: None,
};
let header = ReceivedSpfHeader::new(
&result,
"mail.example.com",
IpAddr::from([1, 2, 3, 4]),
Some("mail.example.org"),
);
assert_eq!(
header.to_string(),
"Received-SPF: \
neutral \
(mail.example.com: domain of me@example.org makes no definitive authorization statement for host 1.2.3.4) \
receiver=mail.example.com; \
client-ip=1.2.3.4; \
helo=mail.example.org; \
envelope-from=\"me@example.org\"; \
identity=mailfrom; \
mechanism=default"
);
}
#[test]
fn format_header_value_ok() {
let parts = ["one", "two", "three", "four", "five"];
assert_eq!(
format_header_value(0, 10, parts),
"one two\n\
\tthree\n\
\tfour five",
);
assert_eq!(
format_header_value(0, 11, parts),
"one two\n\
\tthree four\n\
\tfive",
);
assert_eq!(
format_header_value(7, 11, parts),
"one\n\
\ttwo three\n\
\tfour five",
);
assert_eq!(
format_header_value(8, 11, parts),
"one\n\
\ttwo three\n\
\tfour five",
);
let parts = ["一二三", "two", "three", "four", "five"];
assert_eq!(
format_header_value(0, 10, parts),
"一二三 two\n\
\tthree\n\
\tfour five",
);
}
}