SPF: Adding Support for A and MX Mechanisms in Rust

This is a quick followup on my progress in deconstructing SPF using Rust.
Today I was able to implement both A and MX mechanisms.

In my previous articles on rust and Regex

I came up with a possible regular expression that I thought would work. I have since made some adjustments.

My final regular expressions became:

  • r"^(?P<qualifier>[+?~-])?(?P<mechanism>a(?:[:/]{0,1}.+)?)"
  • r"^(?P<qualifier>[+?~-])?(?P<mechanism>mx(?:[:/]{0,1}.+)?)"

This is far simpler and cleaner for my needs. Though I might find a use for my previous versions in the future.

First let’s take a look at the helper function which returns and Option of type SpfMechanism<String>

fn capture_matches(
    pattern: Regex,
    string: &str,
    kind: kinds::MechanismKind,
) -> Option<SpfMechanism<String>> {
    let caps = pattern.captures(string);
    let mut q: char = '+';
    let m: String;
    match caps {
        None => return None,
        Some(caps) => {
            // There was a match
            // qualifier may be a `None` so check before unwrapping.  
            if caps.name("qualifier").is_some() {
                q = caps
                    .name("qualifier")
                    .unwrap()
                    .as_str()
                    .chars()
                    .nth(0)
                    .unwrap();
            };
            m = caps.name("mechanism").unwrap().as_str().to_string();
            let mechanism = SpfMechanism::new(kind, q, (*m).to_string());
            Some(mechanism)
        }
    }
}
#[test]
fn test_match_on_a_only() {
    let string = "a";
    let pattern = Regex::new(r"^(?P<qualifier>[+?~-])?(?P<mechanism>a(?:[:/]{0,1}.+)?)").unwrap();
    let option_test: Option<SpfMechanism<String>>;

    option_test = capture_matches(pattern, &string, kinds::MechanismKind::A);

    let test = option_test.unwrap();
    assert_eq!(test.is_pass(), true);
    assert_eq!(test.as_string(), "a");
    assert_eq!(test.as_mechanism(), "a");
}
//snip//
#[test]
fn test_match_on_mx_only() {
    let string = "mx";
    let pattern = Regex::new(r"^(?P<qualifier>[+?~-])?(?P<mechanism>mx(?:[:/]{0,1}.+)?)").unwrap();
    let option_test: Option<SpfMechanism<String>>;

    option_test = capture_matches(pattern, &string, kinds::MechanismKind::MX);

    let test = option_test.unwrap();
    assert_eq!(test.is_pass(), true);
    assert_eq!(test.as_string(), "mx");
    assert_eq!(test.as_mechanism(), "mx");
}

capture_matches() has been written with reuse in mind. So it will work with both A and MX mechanisms.

What does it do?

We pass it a regular expression string, the string to be checked, and an SPF MechanismKind.
If there is no match, a None is returned as such.
If there is a match, we check that the capture group qualifer is Some() and if so we get the value stored. It should be a single char
If there is a match as determined previously, mechanism will always be valid.
From this we construct a Mechanism using SpfMechanism::new() with the kind passed in earlier.

Now let’s see how it’s actually put into play in parse()

pub fn parse(&mut self) {
    // initialises required variables.
    let records = self.source.split_whitespace();
    let mut vec_of_includes: Vec<SpfMechanism<String>> = Vec::new();
    let mut vec_of_ip4: Vec<SpfMechanism<IpNetwork>> = Vec::new();
    let mut vec_of_ip6: Vec<SpfMechanism<IpNetwork>> = Vec::new();
    let mut vec_of_a: Vec<SpfMechanism<String>> = Vec::new();
    let mut vec_of_mx: Vec<SpfMechanism<String>> = Vec::new();
    for record in records {
        // Make this lazy.
        let a_pattern =
            Regex::new(r"^(?P<qualifier>[+?~-])?(?P<mechanism>a(?:[:/]{0,1}.+)?)").unwrap();
        let mx_pattern =
            Regex::new(r"^(?P<qualifier>[+?~-])?(?P<mechanism>mx(?:[:/]{0,1}.+)?)").unwrap();
        if record.contains("redirect=") {
          //snip
        } else if record.contains("include:") {
          //snip
            }
        } else if record.contains("ip4:") {
          //snip
            }
        } else if record.contains("ip6:") {
          //snip
            }
        } else if record.ends_with("all") {
          //snip
        } else if let Some(a_mechanism) =
            capture_matches(a_pattern, record, kinds::MechanismKind::A) {
            vec_of_a.push(a_mechanism);
        } else if let Some(mx_mechanism) =
            capture_matches(mx_pattern, record, kinds::MechanismKind::MX) {
            vec_of_mx.push(mx_mechanism);
        }
    }
    if !vec_of_includes.is_empty() {
        self.include = Some(vec_of_includes);
    };
    if !vec_of_ip4.is_empty() {
        self.ip4 = Some(vec_of_ip4);
    };
    if !vec_of_ip6.is_empty() {
        self.ip6 = Some(vec_of_ip6);
    };
    if !vec_of_a.is_empty() {
        self.a = Some(vec_of_a);
    }
    if !vec_of_mx.is_empty() {
        self.mx = Some(vec_of_mx);
    }
}

It is used here:

} else if let Some(a_mechanism) = 
    capture_matches(a_pattern, record, kinds::MechanismKind::A) {
    vec_of_a.push(a_mechanism);
} else if let Some(mx_mechanism) = 
    capture_matches(mx_pattern, record, kinds::MechanismKind::MX) {
    vec_of_mx.push(mx_mechanism);
}

capture_matches() returns None if there is no match, so we can use this to determine if we should push the returned value in to the Vec<..>

Checking things on GitHub

If you want to check things in more gory detail. You can take a look at these diffs

A-Mechanism <-> MX-Mechanism click here

BASIC-TESTING <-> A-Mechanism click here


See also