use crate::{
auth::SpfResultKind,
header::{format, HeaderField, HEADER_LINE_WIDTH},
verify::{Identity, VerificationResult},
};
use std::{
borrow::Cow,
fmt::{self, Display, Formatter, Write},
};
use viaspf::{DomainName, SpfResultCause};
pub fn extract_authserv_id(value: &str) -> Option<Cow<'_, str>> {
let value = format::strip_cfws(value).unwrap_or(value);
if let Some(rest) = format::strip_mime_value(value) {
if rest.starts_with(';') || format::strip_cfws(rest).is_some() {
let authserv_id = &value[..(value.len() - rest.len())];
return Some(if authserv_id.starts_with('"') {
format::decode_quoted_string(authserv_id).into()
} else {
authserv_id.into()
});
}
}
None
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct AuthenticationResultsHeader {
body_pieces: Vec<String>,
}
impl AuthenticationResultsHeader {
pub const NAME: &'static str = "Authentication-Results";
pub fn new(
authserv_id: &str,
results: &[VerificationResult],
include_mailfrom_local_part: bool,
) -> Self {
assert!(!results.is_empty());
Self {
body_pieces: prepare_field_body(authserv_id, results, include_mailfrom_local_part),
}
}
}
fn prepare_field_body(
authserv_id: &str,
results: &[VerificationResult],
include_mailfrom_local_part: bool,
) -> Vec<String> {
let mut parts = make_auth_results_parts(authserv_id, results, include_mailfrom_local_part);
if let [parts @ .., _] = &mut parts[..] {
for part in parts {
part.push(';');
}
}
parts
}
fn make_auth_results_parts(
authserv_id: &str,
results: &[VerificationResult],
include_mailfrom_local_part: bool,
) -> Vec<String> {
let mut parts = Vec::new();
parts.push(format::encode_mime_value(authserv_id).into());
for VerificationResult { identity, spf_result, cause } in results {
let mut resinfo = String::new();
write!(resinfo, "spf={}", spf_result.kind()).unwrap();
if let Some(SpfResultCause::Error(error_cause)) = cause {
write!(
resinfo,
" reason={}",
format::encode_mime_value(&error_cause.to_string())
)
.unwrap();
}
write!(
resinfo,
" smtp.{}={}",
identity.name(),
encode_sender(identity, include_mailfrom_local_part)
)
.unwrap();
parts.push(resinfo);
}
parts
}
fn encode_sender(identity: &Identity, include_mailfrom_local_part: bool) -> String {
let mut sender = identity.as_ref();
let s;
if let Identity::MailFrom(mail_from) = identity {
if let Some((l, d)) = mail_from.rsplit_once('@') {
let (udomain, e) = idna::domain_to_unicode(d);
if e.is_ok() && !udomain.eq_ignore_ascii_case(d) {
if include_mailfrom_local_part {
s = format!("{l}@{udomain}");
} else {
s = udomain;
}
sender = &s;
} else if !include_mailfrom_local_part {
sender = d;
}
}
};
if is_valid_unquoted_pvalue(sender) {
sender.into()
} else {
format::encode_mime_value(sender).into()
}
}
fn is_valid_unquoted_pvalue(mut s: &str) -> bool {
fn is_local_part(s: &str) -> bool {
format::is_dot_atom(s) || format::is_quoted_string(s)
}
fn is_domain_name(s: &str) -> bool {
DomainName::new(s).is_ok()
}
if let Some((l, d)) = s.rsplit_once('@') {
if !is_local_part(l) {
return false;
}
s = d;
}
is_domain_name(s)
}
impl Display for AuthenticationResultsHeader {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", Self::NAME, self.body_pieces.join(" "))
}
}
impl HeaderField for AuthenticationResultsHeader {
fn name(&self) -> &'static str {
Self::NAME
}
fn format_body(&self) -> String {
let body_len: usize = self
.body_pieces
.iter()
.map(|p| p.chars().count() + 1) .sum();
let name_len = Self::NAME.len() + 1; self.body_pieces
.join(if name_len + body_len <= HEADER_LINE_WIDTH {
" "
} else {
"\n\t"
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use viaspf::{
record::{Mechanism, A},
ErrorCause, SpfResult,
};
#[test]
fn extract_authserv_id_ok() {
assert_eq!(extract_authserv_id("x"), None);
assert_eq!(extract_authserv_id("x;"), Some("x".into()));
assert_eq!(extract_authserv_id("\"x\";"), Some("x".into()));
assert_eq!(extract_authserv_id("x\n\t;"), Some("x".into()));
assert_eq!(extract_authserv_id("xy.abc "), Some("xy.abc".into()));
assert_eq!(
extract_authserv_id(" (w h\n\tat ) x.34-q*y(\n\t());"),
Some("x.34-q*y".into())
);
assert_eq!(
extract_authserv_id(" (w h\n\tat ) \"x.34🚀-q?y\\ \"(\n\t());"),
Some("x.34🚀-q?y ".into())
);
}
#[test]
fn encode_sender_ok() {
use Identity::*;
assert_eq!(encode_sender(&Helo("mail.gluet.ch".into()), false), "mail.gluet.ch");
assert_eq!(encode_sender(&Helo("mail.gluet.ch.".into()), false), "mail.gluet.ch.");
assert_eq!(encode_sender(&Helo("_mail.gluet.ch".into()), false), "_mail.gluet.ch");
assert_eq!(encode_sender(&Helo("mail.例子.cn".into()), false), "mail.例子.cn");
assert_eq!(encode_sender(&Helo(":unknown".into()), false), "\":unknown\"");
assert_eq!(encode_sender(&MailFrom("me@localhost".into()), false), "localhost");
assert_eq!(encode_sender(&MailFrom("me@例子.cn".into()), false), "例子.cn");
assert_eq!(encode_sender(&MailFrom("me@例子.cn".into()), true), "me@例子.cn");
assert_eq!(encode_sender(&MailFrom("me+you=♡@例子.cn".into()), true), "me+you=♡@例子.cn");
assert_eq!(encode_sender(&MailFrom("\"me\"@例子.cn".into()), true), "\"me\"@例子.cn");
assert_eq!(encode_sender(&MailFrom("me@例子".into()), true), "\"me@例子\"");
assert_eq!(encode_sender(&MailFrom("me.@例子.cn".into()), true), "\"me.@例子.cn\"");
assert_eq!(
encode_sender(&Helo("mail.xn--fsqu00a.xn--fiqs8s".into()), false),
"mail.xn--fsqu00a.xn--fiqs8s"
);
assert_eq!(
encode_sender(&MailFrom("me@mail.xn--fsqu00a.xn--fiqs8s".into()), false),
"mail.例子.中国"
);
assert_eq!(
encode_sender(&MailFrom("me@mail.xn--fsqu00a.xn--fiqs8s".into()), true),
"me@mail.例子.中国"
);
assert_eq!(
encode_sender(&MailFrom("me@MAIL.example.com".into()), false),
"MAIL.example.com"
);
}
#[test]
fn auth_results_header_display_error() {
let results = [VerificationResult {
identity: Identity::MailFrom("me@example.org".into()),
spf_result: SpfResult::Permerror,
cause: Some(SpfResultCause::Error(ErrorCause::LookupLimitExceeded)),
}];
let header = AuthenticationResultsHeader::new("mail.example.com", &results, false);
assert_eq!(
header.to_string(),
"Authentication-Results: \
mail.example.com; \
spf=permerror reason=\"lookup limit exceeded\" smtp.mailfrom=example.org"
);
}
#[test]
fn auth_results_header_display_all_results() {
let results = [
VerificationResult {
identity: Identity::Helo("mail.example.org".into()),
spf_result: SpfResult::None,
cause: None,
},
VerificationResult {
identity: Identity::MailFrom("me@example.org".into()),
spf_result: SpfResult::Pass,
cause: Some(SpfResultCause::Match(Mechanism::A(A {
domain_spec: None,
prefix_len: None,
}))),
},
];
let header = AuthenticationResultsHeader::new("mail.example.com", &results, true);
assert_eq!(
header.to_string(),
"Authentication-Results: \
mail.example.com; \
spf=none smtp.helo=mail.example.org; \
spf=pass smtp.mailfrom=me@example.org"
);
}
}