mirror of
https://github.com/mailscope/kumomta.git
synced 2026-09-07 19:18:57 +00:00
spf: move SpfContext::expand() to DomainSpec
This commit is contained in:
committed by
Wez Furlong
parent
2264706232
commit
e25029b613
+10
-107
@@ -1,18 +1,17 @@
|
||||
use crate::dns::{DnsError, IpDisplay, Lookup};
|
||||
use crate::dns::{DnsError, Lookup};
|
||||
use crate::record::Record;
|
||||
use crate::spec::{DomainSpec, MacroElement, MacroName};
|
||||
use crate::spec::DomainSpec;
|
||||
use crate::{SpfDisposition, SpfResult};
|
||||
use std::fmt::Write;
|
||||
use std::net::IpAddr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub struct SpfContext<'a> {
|
||||
sender: &'a str,
|
||||
local_part: &'a str,
|
||||
sender_domain: &'a str,
|
||||
pub(crate) sender: &'a str,
|
||||
pub(crate) local_part: &'a str,
|
||||
pub(crate) sender_domain: &'a str,
|
||||
pub(crate) domain: &'a str,
|
||||
pub(crate) client_ip: IpAddr,
|
||||
now: SystemTime,
|
||||
pub(crate) now: SystemTime,
|
||||
}
|
||||
|
||||
impl<'a> SpfContext<'a> {
|
||||
@@ -75,107 +74,11 @@ impl<'a> SpfContext<'a> {
|
||||
return Ok(self.domain.to_owned());
|
||||
};
|
||||
|
||||
self.expand(&spec.elements).map_err(|err| SpfResult {
|
||||
spec.expand(self).map_err(|err| SpfResult {
|
||||
disposition: SpfDisposition::TempError,
|
||||
context: format!("error evaluating domain spec: {err}"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expand(&self, elements: &[MacroElement]) -> Result<String, String> {
|
||||
let (mut result, mut buf) = (String::new(), String::new());
|
||||
for element in elements {
|
||||
let m = match element {
|
||||
MacroElement::Literal(t) => {
|
||||
result.push_str(&t);
|
||||
continue;
|
||||
}
|
||||
MacroElement::Macro(m) => m,
|
||||
};
|
||||
|
||||
buf.clear();
|
||||
match m.name {
|
||||
MacroName::Sender => buf.push_str(self.sender),
|
||||
MacroName::LocalPart => buf.push_str(&self.local_part),
|
||||
MacroName::SenderDomain => buf.push_str(&self.sender_domain),
|
||||
MacroName::Domain => buf.push_str(&self.domain),
|
||||
MacroName::ReverseDns => buf.push_str(match self.client_ip.is_ipv4() {
|
||||
true => "in-addr",
|
||||
false => "ip6",
|
||||
}),
|
||||
MacroName::ClientIp => {
|
||||
buf.write_fmt(format_args!("{}", self.client_ip)).unwrap();
|
||||
}
|
||||
MacroName::Ip => buf
|
||||
.write_fmt(format_args!(
|
||||
"{}",
|
||||
IpDisplay {
|
||||
ip: self.client_ip,
|
||||
reverse: false
|
||||
}
|
||||
))
|
||||
.unwrap(),
|
||||
MacroName::CurrentUnixTimeStamp => buf
|
||||
.write_fmt(format_args!(
|
||||
"{}",
|
||||
self.now
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0)
|
||||
))
|
||||
.unwrap(),
|
||||
MacroName::RelayingHostName
|
||||
| MacroName::HeloDomain
|
||||
| MacroName::ValidatedDomainName => {
|
||||
return Err(format!("{:?} has not been implemented", m.name))
|
||||
}
|
||||
};
|
||||
|
||||
let delimiters = if m.delimiters.is_empty() {
|
||||
"."
|
||||
} else {
|
||||
&m.delimiters
|
||||
};
|
||||
|
||||
let mut tokens: Vec<&str> = buf.split(|c| delimiters.contains(c)).collect();
|
||||
|
||||
if m.reverse {
|
||||
tokens.reverse();
|
||||
}
|
||||
|
||||
if let Some(n) = m.transformer_digits {
|
||||
let n = n as usize;
|
||||
while tokens.len() > n {
|
||||
tokens.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
let output = tokens.join(".");
|
||||
|
||||
if m.url_escape {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7208#section-7.3:
|
||||
// Uppercase macros expand exactly as their lowercase
|
||||
// equivalents, and are then URL escaped. URL escaping
|
||||
// MUST be performed for characters not in the
|
||||
// "unreserved" set.
|
||||
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3:
|
||||
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
for c in output.chars() {
|
||||
if c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' || c == '~' {
|
||||
result.push(c);
|
||||
} else {
|
||||
let mut bytes = [0u8; 4];
|
||||
for b in c.encode_utf8(&mut bytes).bytes() {
|
||||
result.push_str(&format!("%{b:02x}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push_str(&output);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -211,7 +114,7 @@ mod test {
|
||||
("%{l1r-}", "strong"),
|
||||
] {
|
||||
let spec = DomainSpec::parse(input).unwrap();
|
||||
let output = ctx.expand(&spec.elements).unwrap();
|
||||
let output = spec.expand(&ctx).unwrap();
|
||||
k9::assert_equal!(&output, expect, "{input}");
|
||||
}
|
||||
|
||||
@@ -236,7 +139,7 @@ mod test {
|
||||
("%{c}", "192.0.2.3"),
|
||||
] {
|
||||
let spec = DomainSpec::parse(input).unwrap();
|
||||
let output = ctx.expand(&spec.elements).unwrap();
|
||||
let output = spec.expand(&ctx).unwrap();
|
||||
k9::assert_equal!(&output, expect, "{input}");
|
||||
}
|
||||
|
||||
@@ -251,7 +154,7 @@ mod test {
|
||||
("%{C}", "2001%3adb8%3a%3acb01"),
|
||||
] {
|
||||
let spec = DomainSpec::parse(input).unwrap();
|
||||
let output = ctx.expand(&spec.elements).unwrap();
|
||||
let output = spec.expand(&ctx).unwrap();
|
||||
k9::assert_equal!(&output, expect, "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::context::SpfContext;
|
||||
use crate::dns::Lookup;
|
||||
use crate::spec::DomainSpec;
|
||||
use crate::{SpfDisposition, SpfResult};
|
||||
use hickory_resolver::Name;
|
||||
use std::fmt;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::str::FromStr;
|
||||
use crate::spec::DomainSpec;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Record {
|
||||
@@ -121,7 +121,7 @@ impl Record {
|
||||
Err(_) => return SpfResult::fail(failed),
|
||||
};
|
||||
|
||||
match cx.expand(&spec.elements) {
|
||||
match spec.expand(cx) {
|
||||
Ok(explanation) => SpfResult::fail(explanation),
|
||||
Err(_) => SpfResult::fail(failed),
|
||||
}
|
||||
|
||||
+100
-1
@@ -1,4 +1,7 @@
|
||||
use std::fmt;
|
||||
use crate::dns::IpDisplay;
|
||||
use crate::SpfContext;
|
||||
use std::fmt::{self, Write};
|
||||
use std::time::SystemTime;
|
||||
|
||||
fn starts_with_number(input: &str) -> Result<(Option<u32>, &str), String> {
|
||||
let i = input
|
||||
@@ -108,6 +111,102 @@ impl DomainSpec {
|
||||
|
||||
Ok(Self { elements })
|
||||
}
|
||||
|
||||
pub fn expand(&self, cx: &SpfContext<'_>) -> Result<String, String> {
|
||||
let (mut result, mut buf) = (String::new(), String::new());
|
||||
for element in &self.elements {
|
||||
let m = match element {
|
||||
MacroElement::Literal(t) => {
|
||||
result.push_str(&t);
|
||||
continue;
|
||||
}
|
||||
MacroElement::Macro(m) => m,
|
||||
};
|
||||
|
||||
buf.clear();
|
||||
match m.name {
|
||||
MacroName::Sender => buf.push_str(cx.sender),
|
||||
MacroName::LocalPart => buf.push_str(&cx.local_part),
|
||||
MacroName::SenderDomain => buf.push_str(&cx.sender_domain),
|
||||
MacroName::Domain => buf.push_str(&cx.domain),
|
||||
MacroName::ReverseDns => buf.push_str(match cx.client_ip.is_ipv4() {
|
||||
true => "in-addr",
|
||||
false => "ip6",
|
||||
}),
|
||||
MacroName::ClientIp => {
|
||||
buf.write_fmt(format_args!("{}", cx.client_ip)).unwrap();
|
||||
}
|
||||
MacroName::Ip => buf
|
||||
.write_fmt(format_args!(
|
||||
"{}",
|
||||
IpDisplay {
|
||||
ip: cx.client_ip,
|
||||
reverse: false
|
||||
}
|
||||
))
|
||||
.unwrap(),
|
||||
MacroName::CurrentUnixTimeStamp => buf
|
||||
.write_fmt(format_args!(
|
||||
"{}",
|
||||
cx.now
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0)
|
||||
))
|
||||
.unwrap(),
|
||||
MacroName::RelayingHostName
|
||||
| MacroName::HeloDomain
|
||||
| MacroName::ValidatedDomainName => {
|
||||
return Err(format!("{:?} has not been implemented", m.name))
|
||||
}
|
||||
};
|
||||
|
||||
let delimiters = if m.delimiters.is_empty() {
|
||||
"."
|
||||
} else {
|
||||
&m.delimiters
|
||||
};
|
||||
|
||||
let mut tokens: Vec<&str> = buf.split(|c| delimiters.contains(c)).collect();
|
||||
|
||||
if m.reverse {
|
||||
tokens.reverse();
|
||||
}
|
||||
|
||||
if let Some(n) = m.transformer_digits {
|
||||
let n = n as usize;
|
||||
while tokens.len() > n {
|
||||
tokens.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
let output = tokens.join(".");
|
||||
|
||||
if m.url_escape {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7208#section-7.3:
|
||||
// Uppercase macros expand exactly as their lowercase
|
||||
// equivalents, and are then URL escaped. URL escaping
|
||||
// MUST be performed for characters not in the
|
||||
// "unreserved" set.
|
||||
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3:
|
||||
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
for c in output.chars() {
|
||||
if c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' || c == '~' {
|
||||
result.push(c);
|
||||
} else {
|
||||
let mut bytes = [0u8; 4];
|
||||
for b in c.encode_utf8(&mut bytes).bytes() {
|
||||
result.push_str(&format!("%{b:02x}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push_str(&output);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DomainSpec {
|
||||
|
||||
Reference in New Issue
Block a user