#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub(crate) mod builder;
#[cfg(feature = "builder")]
mod conv_traits;
mod errors;
mod kind;
mod parsedmechanism;
mod qualifier;
#[cfg(test)]
mod tests;
pub use crate::spf::mechanism::errors::MechanismError;
pub use crate::spf::mechanism::kind::Kind;
pub use crate::spf::mechanism::parsedmechanism::ParsedMechanism;
pub use crate::spf::mechanism::qualifier::Qualifier;
use crate::core;
use ipnetwork::{IpNetwork, IpNetworkError};
use std::fmt::{Display, Formatter};
use std::{convert::TryFrom, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Clone, PartialEq, Copy, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Mechanism<T> {
kind: Kind,
qualifier: Qualifier,
rrdata: Option<T>,
}
impl FromStr for Mechanism<String> {
type Err = MechanismError;
fn from_str(s: &str) -> Result<Mechanism<String>, Self::Err> {
if s.ends_with(':') || s.ends_with('/') {
return Err(MechanismError::InvalidMechanismFormat(s.to_string()));
};
if s.contains(core::IP4) || s.contains(core::IP6) {
return Err(MechanismError::InvalidMechanismFormat(s.to_string()));
}
let mut m: Option<Mechanism<String>> = None;
if s.contains(core::REDIRECT) {
let mut items = s.rsplit('=');
if let Some(rrdata) = items.next() {
m = Some(Mechanism::generic_inclusive(
Kind::Redirect,
Qualifier::Pass,
Some(rrdata.to_string()),
));
}
} else if s.contains(core::INCLUDE) {
let qualifier_and_modified_str = core::return_and_remove_qualifier(s, 'i');
if let Some(rrdata) = s.rsplit(':').next() {
m = Some(Mechanism::generic_inclusive(
Kind::Include,
qualifier_and_modified_str.0,
Some(rrdata.to_string()),
));
}
} else if s.ends_with(core::ALL) && (s.len() == 3 || s.len() == 4) {
m = Some(Mechanism::generic_inclusive(
Kind::All,
core::return_and_remove_qualifier(s, 'a').0,
None,
));
} else if let Ok(mechanism) = core::spf_regex::capture_matches(s, Kind::A) {
m = Some(mechanism);
} else if let Ok(mechanism) = core::spf_regex::capture_matches(s, Kind::MX) {
m = Some(mechanism);
} else if let Ok(mechanism) = core::spf_regex::capture_matches(s, Kind::Ptr) {
m = Some(mechanism);
} else if let Ok(mechanism) = core::spf_regex::capture_matches(s, Kind::Exists) {
m = Some(mechanism);
}
if let Some(value) = m {
#[cfg(feature = "strict-dns")]
{
if !core::dns::is_dns_suffix_valid(core::dns::get_domain_before_slash(&value.raw()))
{
return Err(MechanismError::InvalidDomainHost(value.raw()));
}
}
return Ok(value);
}
Err(MechanismError::InvalidMechanismFormat(s.to_string()))
}
}
impl TryFrom<&str> for Mechanism<String> {
type Error = MechanismError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Mechanism::from_str(s)
}
}
impl FromStr for Mechanism<IpNetwork> {
type Err = MechanismError;
fn from_str(s: &str) -> Result<Mechanism<IpNetwork>, Self::Err> {
if s.contains(core::IP4) || s.contains(core::IP6) {
let raw_ip: Option<&str>;
let qualifier_and_modified_str = core::return_and_remove_qualifier(s, 'i');
let kind = match qualifier_and_modified_str {
(_, str) if str.contains(core::IP4) => Kind::IpV4,
(_, str) if str.contains(core::IP6) => Kind::IpV6,
_ => return Err(MechanismError::InvalidMechanismFormat(s.to_string())),
};
raw_ip = qualifier_and_modified_str.1.splitn(2, ":").last();
return match raw_ip.unwrap().parse::<IpNetwork>() {
Err(e) => Err(MechanismError::InvalidIPNetwork(e)),
Ok(ip) => {
if ip.is_ipv4() && !kind.is_ip_v4() {
return Err(MechanismError::NotIP6Network(ip.to_string()));
}
if ip.is_ipv6() && !kind.is_ip_v6() {
return Err(MechanismError::NotIP4Network(ip.to_string()));
}
Ok(Mechanism::generic_inclusive(
kind,
qualifier_and_modified_str.0,
Some(ip),
))
}
};
}
Err(MechanismError::InvalidMechanismFormat(s.to_string()))
}
}
impl TryFrom<&str> for Mechanism<IpNetwork> {
type Error = MechanismError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Mechanism::from_str(s)
}
}
impl<T> Mechanism<T> {
#[doc(hidden)]
pub fn generic_inclusive(kind: Kind, qualifier: Qualifier, mechanism: Option<T>) -> Self {
Self {
kind,
qualifier,
rrdata: mechanism,
}
}
#[doc(hidden)]
pub fn new(kind: Kind, qualifier: Qualifier) -> Self {
Self {
kind,
qualifier,
rrdata: None,
}
}
pub fn is_pass(&self) -> bool {
self.qualifier == Qualifier::Pass
}
pub fn is_fail(&self) -> bool {
self.qualifier == Qualifier::Fail
}
pub fn is_softfail(&self) -> bool {
self.qualifier == Qualifier::SoftFail
}
pub fn is_neutral(&self) -> bool {
self.qualifier == Qualifier::Neutral
}
pub fn kind(&self) -> &Kind {
&self.kind
}
pub fn qualifier(&self) -> &Qualifier {
&self.qualifier
}
#[deprecated(since = "0.3.5", note = "Please use `rr_data`")]
pub fn mechanism(&self) -> &Option<T> {
&self.rrdata
}
pub fn rr_data(&self) -> &Option<T> {
&self.rrdata
}
}
impl Mechanism<String> {
pub fn redirect(qualifier: Qualifier, rrdata: &str) -> Result<Self, MechanismError> {
Ok(Mechanism::new(Kind::Redirect, qualifier).with_rrdata(rrdata)?)
}
pub fn a(qualifier: Qualifier) -> Self {
Mechanism::new(Kind::A, qualifier)
}
pub fn mx(qualifier: Qualifier) -> Self {
Mechanism::new(Kind::MX, qualifier)
}
pub fn include(qualifier: Qualifier, rrdata: &str) -> Result<Self, MechanismError> {
Ok(Mechanism::new(Kind::Include, qualifier).with_rrdata(rrdata)?)
}
pub fn ptr(qualifier: Qualifier) -> Self {
Mechanism::new(Kind::Ptr, qualifier)
}
pub fn exists(qualifier: Qualifier, rrdata: &str) -> Result<Self, MechanismError> {
Ok(Mechanism::new(Kind::Exists, qualifier).with_rrdata(rrdata)?)
}
pub fn with_rrdata(mut self, rrdata: impl Into<String>) -> Result<Self, MechanismError> {
let rrdata_string = rrdata.into();
#[cfg(feature = "strict-dns")]
{
match self.kind() {
Kind::A | Kind::MX | Kind::Include | Kind::Ptr | Kind::Exists => {
if !core::dns::is_dns_suffix_valid(core::dns::get_domain_before_slash(
rrdata_string.as_str(),
)) {
return Err(MechanismError::InvalidDomainHost(rrdata_string));
};
}
_ => {}
};
}
self.rrdata = Some(rrdata_string);
Ok(self)
}
pub fn raw(&self) -> String {
if self.rrdata.is_none() {
self.kind().to_string()
} else {
self.rrdata
.as_ref()
.expect("This should not be None.")
.to_string()
}
}
fn build_string(&self) -> String {
let mut mechanism_str = String::new();
let tmp_mechanism_str;
if self.qualifier != Qualifier::Pass {
mechanism_str.push_str(self.qualifier.as_str());
};
mechanism_str.push_str(self.kind().as_str());
if let Some(ref rrdata) = self.rrdata {
tmp_mechanism_str = rrdata.as_str();
} else {
tmp_mechanism_str = "";
}
match self.kind {
Kind::A | Kind::MX => {
if !tmp_mechanism_str.is_empty() && !tmp_mechanism_str.starts_with('/') {
mechanism_str.push(':')
}
}
Kind::Ptr => {
if !tmp_mechanism_str.is_empty() {
mechanism_str.push(':')
}
}
_ => {}
}
mechanism_str.push_str(tmp_mechanism_str);
mechanism_str
}
}
impl Display for Mechanism<String> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.build_string())
}
}
impl From<IpNetworkError> for MechanismError {
fn from(err: IpNetworkError) -> Self {
MechanismError::InvalidIPNetwork(err)
}
}
impl Mechanism<IpNetwork> {
pub fn ip_from_string(string: &str) -> Result<Mechanism<IpNetwork>, MechanismError> {
Ok(Mechanism::<IpNetwork>::from_str(string)?)
}
pub fn ip(qualifier: Qualifier, rrdata: IpNetwork) -> Mechanism<IpNetwork> {
if rrdata.is_ipv4() {
Mechanism::ip4(qualifier, rrdata)
} else {
Mechanism::ip6(qualifier, rrdata)
}
}
fn ip4(qualifier: Qualifier, rrdata: IpNetwork) -> Self {
Mechanism::generic_inclusive(Kind::IpV4, qualifier, Some(rrdata))
}
fn ip6(qualifier: Qualifier, rrdata: IpNetwork) -> Self {
Mechanism::generic_inclusive(Kind::IpV6, qualifier, Some(rrdata))
}
pub fn raw(&self) -> String {
Self::sanitize_ip_addr(self.rrdata.as_ref().expect("Missing IpNetwork"))
}
fn build_string(&self) -> String {
let mut ip_mechanism_str = String::new();
if self.qualifier != Qualifier::Pass {
ip_mechanism_str.push_str(self.qualifier.as_str());
};
ip_mechanism_str.push_str(self.kind().as_str());
let ip = self.rrdata.as_ref().unwrap();
let ip_str = Self::sanitize_ip_addr(ip);
ip_mechanism_str.push_str(ip_str.as_str());
ip_mechanism_str
}
fn sanitize_ip_addr(ip: &IpNetwork) -> String {
let ip_binding = ip.to_string();
match ip.is_ipv4() {
true => match ip.prefix() {
32 => ip.network().to_string(),
_ => ip_binding,
},
false => match ip.prefix() {
128 => ip.network().to_string(),
_ => ip_binding,
},
}
}
pub fn as_network(&self) -> &IpNetwork {
self.rrdata.as_ref().expect("Missing IpNetwork")
}
}
impl Display for Mechanism<IpNetwork> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.build_string())
}
}
#[cfg(test)]
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
use serde_json;
#[test]
fn a() {
let a: Mechanism<String> = "a".parse().unwrap();
let json = serde_json::to_string(&a).unwrap();
assert_eq!(
json,
"{\"kind\":\"A\",\"qualifier\":\"Pass\",\"rrdata\":null}"
);
let deserialized: Mechanism<String> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, a);
}
#[test]
fn mx() {
let mx = "mx:example.com".parse::<Mechanism<String>>().unwrap();
let json = serde_json::to_string(&mx).unwrap();
assert_eq!(
json,
"{\"kind\":\"MX\",\"qualifier\":\"Pass\",\"rrdata\":\"example.com\"}"
);
let deserialized: Mechanism<String> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, mx);
}
}
impl From<Mechanism<IpNetwork>> for Mechanism<String> {
fn from(value: Mechanism<IpNetwork>) -> Self {
Mechanism::generic_inclusive(
*value.kind(),
value.qualifier,
Some(Mechanism::sanitize_ip_addr(
value.rr_data().as_ref().expect("Not IpNetwork"),
)),
)
}
}
impl TryFrom<Mechanism<String>> for Mechanism<IpNetwork> {
type Error = MechanismError;
fn try_from(value: Mechanism<String>) -> Result<Self, Self::Error> {
match value.kind {
Kind::IpV4 | Kind::IpV6 => Ok(Mechanism::ip(
value.qualifier,
value.rrdata.expect("Missing RRData").parse::<IpNetwork>()?,
)),
_ => Err(MechanismError::InvalidMechanismFormat(value.to_string())),
}
}
}
#[cfg(test)]
mod string_ip_conversion {
use crate::mechanism::Kind::{IpV4, A};
use crate::mechanism::Qualifier;
use crate::mechanism::*;
use std::convert::TryInto;
#[test]
fn ip_to_string_mechanism() {
let s = "ip4:192.168.0.1".parse::<Mechanism<IpNetwork>>().unwrap();
let m = Mechanism::<IpNetwork>::ip(Qualifier::Pass, "192.168.0.1".parse().unwrap());
assert_eq!(s, m);
let s2: Mechanism<String> = s.into();
assert_eq!("ip4:192.168.0.1", s2.to_string());
}
#[test]
fn string_to_ip() {
let s: Mechanism<String> =
Mechanism::generic_inclusive(IpV4, Qualifier::Pass, Some("192.168.0.1".to_string()));
let ip: Mechanism<IpNetwork> = s.try_into().expect("Expected string to be ip4/6:");
assert_eq!(
ip,
Mechanism::<IpNetwork>::ip(
Qualifier::Pass,
"192.168.0.1".parse::<IpNetwork>().unwrap()
)
);
}
#[test]
fn string_to_ip_fail() {
let s: Mechanism<String> =
Mechanism::generic_inclusive(A, Qualifier::Pass, Some("host.example.com".to_string()));
let res: Result<Mechanism<IpNetwork>, MechanismError> = s.try_into();
assert!(res.is_err());
assert_eq!(
res.unwrap_err(),
MechanismError::InvalidMechanismFormat("a:host.example.com".to_string())
);
}
}