mod parse;
use crate::name;
use std::{
error::Error,
fmt::{self, Display, Formatter},
iter::FromIterator,
net::{Ipv4Addr, Ipv6Addr},
num::NonZeroU8,
vec::IntoIter,
};
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Record {
pub terms: Vec<Term>,
}
impl Record {
pub fn directives(&self) -> impl Iterator<Item = &Directive> {
self.terms.iter().filter_map(|term| match term {
Term::Directive(d) => Some(d),
_ => None,
})
}
pub fn modifiers(&self) -> impl Iterator<Item = &Modifier> {
self.terms.iter().filter_map(|term| match term {
Term::Modifier(m) => Some(m),
_ => None,
})
}
}
impl IntoIterator for Record {
type Item = Term;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.terms.into_iter()
}
}
impl Display for Record {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "v=spf1")?;
for term in &self.terms {
write!(f, " {}", term)?;
}
Ok(())
}
}
impl From<Vec<Term>> for Record {
fn from(terms: Vec<Term>) -> Self {
Self { terms }
}
}
impl FromIterator<Term> for Record {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = Term>,
{
Self {
terms: iter.into_iter().collect(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ParseRecordError;
impl Error for ParseRecordError {}
impl Display for ParseRecordError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse SPF record")
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Term {
Directive(Directive),
Modifier(Modifier),
}
impl Display for Term {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Directive(directive) => directive.fmt(f),
Self::Modifier(modifier) => modifier.fmt(f),
}
}
}
impl From<Directive> for Term {
fn from(directive: Directive) -> Self {
Self::Directive(directive)
}
}
impl From<Modifier> for Term {
fn from(modifier: Modifier) -> Self {
Self::Modifier(modifier)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Directive {
pub qualifier: Option<Qualifier>,
pub mechanism: Mechanism,
}
impl Display for Directive {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(qualifier) = self.qualifier {
qualifier.fmt(f)?;
}
self.mechanism.fmt(f)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Qualifier {
Pass,
Fail,
Neutral,
Softfail,
}
impl Default for Qualifier {
fn default() -> Self {
Self::Pass
}
}
impl Display for Qualifier {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Pass => write!(f, "+"),
Self::Fail => write!(f, "-"),
Self::Neutral => write!(f, "?"),
Self::Softfail => write!(f, "~"),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Mechanism {
All,
Include(Include),
A(A),
Mx(Mx),
Ptr(Ptr),
Ip4(Ip4),
Ip6(Ip6),
Exists(Exists),
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::All => write!(f, "all"),
Self::Include(include) => include.fmt(f),
Self::A(a) => a.fmt(f),
Self::Mx(mx) => mx.fmt(f),
Self::Ptr(ptr) => ptr.fmt(f),
Self::Ip4(ip4) => ip4.fmt(f),
Self::Ip6(ip6) => ip6.fmt(f),
Self::Exists(exists) => exists.fmt(f),
}
}
}
impl From<Include> for Mechanism {
fn from(include: Include) -> Self {
Self::Include(include)
}
}
impl From<A> for Mechanism {
fn from(a: A) -> Self {
Self::A(a)
}
}
impl From<Mx> for Mechanism {
fn from(mx: Mx) -> Self {
Self::Mx(mx)
}
}
impl From<Ptr> for Mechanism {
fn from(ptr: Ptr) -> Self {
Self::Ptr(ptr)
}
}
impl From<Ip4> for Mechanism {
fn from(ip4: Ip4) -> Self {
Self::Ip4(ip4)
}
}
impl From<Ip6> for Mechanism {
fn from(ip6: Ip6) -> Self {
Self::Ip6(ip6)
}
}
impl From<Exists> for Mechanism {
fn from(exists: Exists) -> Self {
Self::Exists(exists)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Include {
pub domain_spec: DomainSpec,
}
impl Display for Include {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "include:{}", self.domain_spec)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct A {
pub domain_spec: Option<DomainSpec>,
pub prefix_len: Option<DualCidrLength>,
}
impl Display for A {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "a")?;
if let Some(domain_spec) = &self.domain_spec {
write!(f, ":{}", domain_spec)?;
}
if let Some(len) = self.prefix_len {
fmt_dual_cidr_length(f, len)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Mx {
pub domain_spec: Option<DomainSpec>,
pub prefix_len: Option<DualCidrLength>,
}
impl Display for Mx {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "mx")?;
if let Some(domain_spec) = &self.domain_spec {
write!(f, ":{}", domain_spec)?;
}
if let Some(len) = self.prefix_len {
fmt_dual_cidr_length(f, len)?;
}
Ok(())
}
}
fn fmt_dual_cidr_length(f: &mut Formatter<'_>, prefix_len: DualCidrLength) -> fmt::Result {
use DualCidrLength::*;
match prefix_len {
Ip4(len) => write!(f, "/{}", len),
Ip6(len) => write!(f, "//{}", len),
Both(len4, len6) => write!(f, "/{}//{}", len4, len6),
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Ptr {
pub domain_spec: Option<DomainSpec>,
}
impl Display for Ptr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "ptr")?;
if let Some(domain_spec) = &self.domain_spec {
write!(f, ":{}", domain_spec)?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Ip4 {
pub addr: Ipv4Addr,
pub prefix_len: Option<Ip4CidrLength>,
}
impl Display for Ip4 {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "ip4:{}", self.addr)?;
if let Some(len) = self.prefix_len {
write!(f, "/{}", len)?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Ip6 {
pub addr: Ipv6Addr,
pub prefix_len: Option<Ip6CidrLength>,
}
impl Display for Ip6 {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "ip6:{}", self.addr)?;
if let Some(len) = self.prefix_len {
write!(f, "/{}", len)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Exists {
pub domain_spec: DomainSpec,
}
impl Display for Exists {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "exists:{}", self.domain_spec)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Modifier {
Redirect(Redirect),
Explanation(Explanation),
Unknown(Unknown),
}
impl Display for Modifier {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Redirect(redirect) => redirect.fmt(f),
Self::Explanation(explanation) => explanation.fmt(f),
Self::Unknown(unknown) => unknown.fmt(f),
}
}
}
impl From<Redirect> for Modifier {
fn from(redirect: Redirect) -> Self {
Self::Redirect(redirect)
}
}
impl From<Explanation> for Modifier {
fn from(explanation: Explanation) -> Self {
Self::Explanation(explanation)
}
}
impl From<Unknown> for Modifier {
fn from(unknown: Unknown) -> Self {
Self::Unknown(unknown)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Redirect {
pub domain_spec: DomainSpec,
}
impl Display for Redirect {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "redirect={}", self.domain_spec)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Explanation {
pub domain_spec: DomainSpec,
}
impl Display for Explanation {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "exp={}", self.domain_spec)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Unknown {
name: Name,
value: MacroString,
}
impl Unknown {
pub fn new<N, V>(name: N, value: V) -> Result<Self, Name>
where
N: Into<Name>,
V: Into<MacroString>,
{
let name = name.into();
if is_unknown_modifier_name(&name) {
Ok(Self {
name,
value: value.into(),
})
} else {
Err(name)
}
}
pub fn name(&self) -> &Name {
&self.name
}
pub fn value(&self) -> &MacroString {
&self.value
}
}
fn is_unknown_modifier_name(name: &Name) -> bool {
!name.0.eq_ignore_ascii_case("redirect") && !name.0.eq_ignore_ascii_case("exp")
}
impl Display for Unknown {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}={}", self.name, self.value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Name(String);
impl Name {
pub fn new<S>(s: S) -> Result<Self, String>
where
S: Into<String>,
{
let s = s.into();
if is_name_str(&s) {
Ok(Self(s))
} else {
Err(s)
}
}
}
fn is_name_str(s: &str) -> bool {
s.starts_with(is_alpha) && s.chars().all(is_name_char)
}
fn is_name_char(c: char) -> bool {
is_alphanum(c) || matches!(c, '-' | '_' | '.')
}
impl Display for Name {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<Name> for String {
fn from(name: Name) -> Self {
name.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum DualCidrLength {
Ip4(Ip4CidrLength),
Ip6(Ip6CidrLength),
Both(Ip4CidrLength, Ip6CidrLength),
}
impl DualCidrLength {
pub fn ip4(self) -> Option<Ip4CidrLength> {
match self {
Self::Ip4(len) | Self::Both(len, _) => Some(len),
Self::Ip6(_) => None,
}
}
pub fn ip6(self) -> Option<Ip6CidrLength> {
match self {
Self::Ip6(len) | Self::Both(_, len) => Some(len),
Self::Ip4(_) => None,
}
}
}
impl Default for DualCidrLength {
fn default() -> Self {
Self::Both(Default::default(), Default::default())
}
}
impl From<Ip4CidrLength> for DualCidrLength {
fn from(len: Ip4CidrLength) -> Self {
Self::Ip4(len)
}
}
impl From<Ip6CidrLength> for DualCidrLength {
fn from(len: Ip6CidrLength) -> Self {
Self::Ip6(len)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Ip4CidrLength(u8);
impl Ip4CidrLength {
pub fn new(len: u8) -> Option<Self> {
if is_ip4_cidr_length(len) {
Some(Self(len))
} else {
None
}
}
pub fn get(self) -> u8 {
self.0
}
}
fn is_ip4_cidr_length(len: u8) -> bool {
matches!(len, 0..=32)
}
impl Default for Ip4CidrLength {
fn default() -> Self {
Self(32)
}
}
impl Display for Ip4CidrLength {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<Ip4CidrLength> for u8 {
fn from(len: Ip4CidrLength) -> Self {
len.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Ip6CidrLength(u8);
impl Ip6CidrLength {
pub fn new(len: u8) -> Option<Self> {
if is_ip6_cidr_length(len) {
Some(Self(len))
} else {
None
}
}
pub fn get(self) -> u8 {
self.0
}
}
fn is_ip6_cidr_length(len: u8) -> bool {
matches!(len, 0..=128)
}
impl Default for Ip6CidrLength {
fn default() -> Self {
Self(128)
}
}
impl Display for Ip6CidrLength {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<Ip6CidrLength> for u8 {
fn from(len: Ip6CidrLength) -> Self {
len.0
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DomainSpec(MacroString);
impl DomainSpec {
pub fn new<M>(macro_string: M) -> Result<Self, MacroString>
where
M: Into<MacroString>,
{
let macro_string = macro_string.into();
if has_valid_domain_end(¯o_string) {
Ok(Self(macro_string))
} else {
Err(macro_string)
}
}
}
fn has_valid_domain_end(macro_string: &MacroString) -> bool {
match ¯o_string.segments[..] {
[.., MacroStringSegment::MacroExpand(_)] => true,
[.., MacroStringSegment::MacroLiteral(literal)] => {
let mut s = literal.as_ref();
if s.ends_with('.') {
s = &s[..(s.len() - 1)];
}
matches!(s.rfind('.'), Some(i) if is_toplabel(&s[(i + 1)..]))
}
_ => false,
}
}
fn is_toplabel(s: &str) -> bool {
name::is_tld(s)
}
impl Display for DomainSpec {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl AsRef<MacroString> for DomainSpec {
fn as_ref(&self) -> &MacroString {
&self.0
}
}
impl From<DomainSpec> for MacroString {
fn from(domain_spec: DomainSpec) -> Self {
domain_spec.0
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct ExplainString {
pub segments: Vec<ExplainStringSegment>,
}
impl IntoIterator for ExplainString {
type Item = ExplainStringSegment;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.segments.into_iter()
}
}
impl Display for ExplainString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for segment in &self.segments {
segment.fmt(f)?;
}
Ok(())
}
}
impl From<Vec<ExplainStringSegment>> for ExplainString {
fn from(segments: Vec<ExplainStringSegment>) -> Self {
Self { segments }
}
}
impl FromIterator<ExplainStringSegment> for ExplainString {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = ExplainStringSegment>,
{
Self {
segments: iter.into_iter().collect(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ParseExplainStringError;
impl Error for ParseExplainStringError {}
impl Display for ParseExplainStringError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse explain string")
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ExplainStringSegment {
MacroString(MacroString),
Space,
}
impl Display for ExplainStringSegment {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::MacroString(macro_string) => macro_string.fmt(f),
Self::Space => write!(f, " "),
}
}
}
impl From<MacroString> for ExplainStringSegment {
fn from(macro_string: MacroString) -> Self {
Self::MacroString(macro_string)
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct MacroString {
pub segments: Vec<MacroStringSegment>,
}
impl IntoIterator for MacroString {
type Item = MacroStringSegment;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.segments.into_iter()
}
}
impl Display for MacroString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for segment in &self.segments {
segment.fmt(f)?;
}
Ok(())
}
}
impl From<Vec<MacroStringSegment>> for MacroString {
fn from(segments: Vec<MacroStringSegment>) -> Self {
Self { segments }
}
}
impl FromIterator<MacroStringSegment> for MacroString {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = MacroStringSegment>,
{
Self {
segments: iter.into_iter().collect(),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum MacroStringSegment {
MacroExpand(MacroExpand),
MacroLiteral(MacroLiteral),
}
impl Display for MacroStringSegment {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::MacroExpand(macro_expand) => macro_expand.fmt(f),
Self::MacroLiteral(macro_literal) => macro_literal.fmt(f),
}
}
}
impl From<MacroExpand> for MacroStringSegment {
fn from(macro_expand: MacroExpand) -> Self {
Self::MacroExpand(macro_expand)
}
}
impl From<MacroLiteral> for MacroStringSegment {
fn from(macro_literal: MacroLiteral) -> Self {
Self::MacroLiteral(macro_literal)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum MacroExpand {
Macro(Macro),
Escape(Escape),
}
impl Display for MacroExpand {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Macro(macro_) => macro_.fmt(f),
Self::Escape(escape) => escape.fmt(f),
}
}
}
impl From<Macro> for MacroExpand {
fn from(macro_: Macro) -> Self {
Self::Macro(macro_)
}
}
impl From<Escape> for MacroExpand {
fn from(escape: Escape) -> Self {
Self::Escape(escape)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Macro {
kind: MacroKind,
url_encode: bool,
limit: Option<NonZeroU8>,
reverse: bool,
delimiters: Option<Delimiters>,
}
impl Macro {
pub fn new(kind: MacroKind) -> Macro {
Self {
kind,
url_encode: Default::default(),
limit: Default::default(),
reverse: Default::default(),
delimiters: Default::default(),
}
}
pub fn kind(&self) -> MacroKind {
self.kind
}
pub fn set_kind(&mut self, value: MacroKind) {
self.kind = value;
}
pub fn url_encode(&self) -> bool {
self.url_encode
}
pub fn set_url_encode(&mut self, value: bool) {
self.url_encode = value;
}
pub fn limit(&self) -> Option<NonZeroU8> {
self.limit
}
pub fn set_limit<L>(&mut self, value: L)
where
L: Into<Option<NonZeroU8>>,
{
self.limit = value.into();
}
pub fn reverse(&self) -> bool {
self.reverse
}
pub fn set_reverse(&mut self, value: bool) {
self.reverse = value;
}
pub fn delimiters(&self) -> Option<&Delimiters> {
self.delimiters.as_ref()
}
pub fn set_delimiters<D>(&mut self, value: D)
where
D: Into<Option<Delimiters>>,
{
self.delimiters = value.into();
}
}
impl Display for Macro {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "%{{")?;
fmt_macro_letter(f, self.kind(), self.url_encode())?;
if let Some(limit) = self.limit {
limit.fmt(f)?;
}
if self.reverse {
write!(f, "r")?;
}
if let Some(delimiters) = &self.delimiters {
delimiters.0.fmt(f)?;
}
write!(f, "}}")
}
}
fn fmt_macro_letter(f: &mut Formatter<'_>, kind: MacroKind, url_encode: bool) -> fmt::Result {
use MacroKind::*;
let c = match kind {
Sender => if url_encode { 'S' } else { 's' },
LocalPart => if url_encode { 'L' } else { 'l' },
DomainPart => if url_encode { 'O' } else { 'o' },
Domain => if url_encode { 'D' } else { 'd' },
Ip => if url_encode { 'I' } else { 'i' },
ValidatedDomain => if url_encode { 'P' } else { 'p' },
HeloDomain => if url_encode { 'H' } else { 'h' },
PrettyIp => if url_encode { 'C' } else { 'c' },
Receiver => if url_encode { 'R' } else { 'r' },
Timestamp => if url_encode { 'T' } else { 't' },
VersionLabel => if url_encode { 'V' } else { 'v' },
};
c.fmt(f)
}
impl From<MacroKind> for Macro {
fn from(kind: MacroKind) -> Self {
Macro::new(kind)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum MacroKind {
Sender,
LocalPart,
DomainPart,
Domain,
Ip,
ValidatedDomain,
HeloDomain,
PrettyIp,
Receiver,
Timestamp,
VersionLabel,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Delimiters(String);
impl Delimiters {
pub fn new<S>(s: S) -> Result<Self, String>
where
S: Into<String>,
{
let s = s.into();
if !s.is_empty() && s.chars().all(is_delimiter_char) {
Ok(Self(s))
} else {
Err(s)
}
}
}
fn is_delimiter_char(c: char) -> bool {
matches!(c, '.' | '-' | '+' | ',' | '/' | '_' | '=')
}
impl Default for Delimiters {
fn default() -> Self {
Self(String::from("."))
}
}
impl AsRef<str> for Delimiters {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<Delimiters> for String {
fn from(delimiters: Delimiters) -> Self {
delimiters.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Escape {
Percent,
Space,
UrlEncodedSpace,
}
impl Display for Escape {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Percent => write!(f, "%%"),
Self::Space => write!(f, "%_"),
Self::UrlEncodedSpace => write!(f, "%-"),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MacroLiteral(String);
impl MacroLiteral {
pub fn new<S>(s: S) -> Result<Self, String>
where
S: Into<String>,
{
let s = s.into();
if !s.is_empty() && s.chars().all(is_macro_literal_char) {
Ok(Self(s))
} else {
Err(s)
}
}
}
fn is_macro_literal_char(c: char) -> bool {
matches!(c, '!'..='$' | '&'..='~')
}
impl Display for MacroLiteral {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(fmt)
}
}
impl AsRef<str> for MacroLiteral {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<MacroLiteral> for String {
fn from(macro_literal: MacroLiteral) -> Self {
macro_literal.0
}
}
fn is_alpha(c: char) -> bool {
c.is_ascii_alphabetic()
}
fn is_digit(c: char) -> bool {
c.is_ascii_digit()
}
fn is_alphanum(c: char) -> bool {
c.is_ascii_alphanumeric()
}
fn is_hexdigit(c: char) -> bool {
c.is_ascii_hexdigit()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_roundtrip() {
let record = Record {
terms: vec![
Term::Directive(Directive {
qualifier: Some(Qualifier::Pass),
mechanism: Mechanism::A(A {
domain_spec: Some(
DomainSpec::new(MacroString {
segments: vec![MacroStringSegment::MacroLiteral(
MacroLiteral::new("_spf.gluet.ch").unwrap(),
)],
})
.unwrap(),
),
prefix_len: Some(DualCidrLength::Both(
Ip4CidrLength::new(24).unwrap(),
Ip6CidrLength::new(64).unwrap(),
)),
}),
}),
Term::Modifier(Modifier::Redirect(Redirect {
domain_spec: DomainSpec::new(MacroString {
segments: vec![MacroStringSegment::MacroLiteral(
MacroLiteral::new("test.gluet.ch").unwrap(),
)],
})
.unwrap(),
})),
],
};
let s = record.to_string();
assert_eq!(s, "v=spf1 +a:_spf.gluet.ch/24//64 redirect=test.gluet.ch");
assert_eq!(record, s.parse().unwrap());
}
#[test]
fn record_match_destructuring() {
let record = "v=spf1 +a:myhost.ch/24".parse::<Record>().unwrap();
let ch_tld = match record.into_iter().next() {
Some(term) => match term {
Term::Directive(directive) => match directive.mechanism {
Mechanism::A(a) => match a.domain_spec {
Some(domain_spec) => {
let macro_string = MacroString::from(domain_spec);
match macro_string.segments.into_iter().next() {
Some(segment) => match segment {
MacroStringSegment::MacroLiteral(literal) => {
literal.as_ref().ends_with(".ch")
}
_ => false,
},
None => false,
}
}
None => false,
},
_ => false,
},
_ => false,
},
None => false,
};
assert!(ch_tld);
}
#[test]
fn record_display_ok() {
let s = "v=spf1 a:wh%_is%{Sr}.com";
assert_eq!(s.parse::<Record>().unwrap().to_string(), s);
}
#[test]
fn ip6_addr_display_ok() {
let ip6 = Ip6 {
addr: Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0),
prefix_len: None,
};
assert_eq!(ip6.to_string(), "ip6:ff00::");
let ip6 = Ip6 {
addr: Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff),
prefix_len: None,
};
assert_eq!(ip6.to_string(), "ip6:::ffff:192.10.2.255");
}
#[test]
fn ip4_cidr_length_ok() {
assert_eq!(Ip4CidrLength::new(0).map(From::from), Some(0));
assert_eq!(Ip4CidrLength::new(1).map(From::from), Some(1));
assert_eq!(Ip4CidrLength::new(28).map(From::from), Some(28));
assert_eq!(Ip4CidrLength::new(32).map(From::from), Some(32));
assert_eq!(Ip4CidrLength::new(33), None);
}
#[test]
fn domain_spec_multiple_final_literals() {
let ms = MacroString {
segments: vec![
MacroStringSegment::MacroLiteral(MacroLiteral::new("examp").unwrap()),
MacroStringSegment::MacroLiteral(MacroLiteral::new("le.o").unwrap()),
MacroStringSegment::MacroLiteral(MacroLiteral::new("rg").unwrap()),
],
};
assert_eq!(DomainSpec::new(ms.clone()), Err(ms));
let ms = vec![MacroStringSegment::MacroLiteral(
MacroLiteral::new("example.org").unwrap(),
)];
assert!(DomainSpec::new(ms).is_ok());
}
#[test]
fn explain_string_display_ok() {
let s = "See http://%{d}/why.html?s=%{S}&i=%{I}";
assert_eq!(s.parse::<ExplainString>().unwrap().to_string(), s);
}
#[test]
fn macro_expand_display_ok() {
assert_eq!(MacroExpand::Escape(Escape::Percent).to_string(), "%%");
}
}