GitLab's annual major release is around the corner. Along with a lot of new and exciting features, there will be a few breaking changes. Learn more here.

Commits (6)
# viaspf changelog
## 0.3.0 (2021-01-31)
* Validate a sender address’s *local-part* more accurately in line with RFC
5321 (instead of RFC 3696).
* Rewrite parsing implementation using standard library only, and drop
dependency on `nom`.
* Update dev dependency `trust-dns-resolver` to version 0.20.
* Update dependencies in `Cargo.lock`.
* Increase minimum supported Rust version to 1.45.0.
## 0.2.1 (2020-12-12)
* Improve parsing of *domain-spec* tokens. Previously, an SPF record
......
This diff is collapsed.
[package]
name = "viaspf"
version = "0.2.1" # remember to update html_root_url
version = "0.3.0" # remember to update html_root_url
edition = "2018"
description = "Implementation of the Sender Policy Framework (SPF) protocol"
authors = ["David Bürgin <dbuergin@gluet.ch>"]
......@@ -12,7 +12,6 @@ exclude = ["/.gitignore", "/.gitlab-ci.yml"]
[dependencies]
idna = "0.2"
nom = { version = "5.1", default-features = false, features = ["std"] }
[dev-dependencies]
trust-dns-resolver = "0.20.0-alpha.2"
trust-dns-resolver = "0.20"
......@@ -15,7 +15,7 @@ written from scratch, referring only to the RFC, and following it to the letter.
It can therefore be considered an independent alternative to existing SPF
protocol implementations.
The minimum supported Rust version is 1.42.0.
The minimum supported Rust version is 1.45.0.
[RFC 7208]: https://www.rfc-editor.org/rfc/rfc7208
......@@ -60,9 +60,9 @@ functionality, such as capturing a trace of the query execution. The trace
provides programmatic access to the protocol processing steps after the fact.
[Rust]: https://www.rust-lang.org
[`evaluate_spf`]: https://docs.rs/viaspf/0.2.1/viaspf/fn.evaluate_spf.html
[`Lookup`]: https://docs.rs/viaspf/0.2.1/viaspf/trait.Lookup.html
[`viaspf::record`]: https://docs.rs/viaspf/0.2.1/viaspf/record/index.html
[`evaluate_spf`]: https://docs.rs/viaspf/0.3.0/viaspf/fn.evaluate_spf.html
[`Lookup`]: https://docs.rs/viaspf/0.3.0/viaspf/trait.Lookup.html
[`viaspf::record`]: https://docs.rs/viaspf/0.3.0/viaspf/record/index.html
[section 12]: https://www.rfc-editor.org/rfc/rfc7208#section-12
[API documentation]: https://docs.rs/viaspf
......@@ -106,7 +106,7 @@ Trace:
## Licence
Copyright © 2020 David Bürgin
Copyright © 2020–2021 David Bürgin
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
......
......@@ -77,7 +77,7 @@ impl<'l> Resolver<'l> {
}
fn check_timeout(&self, timeout: Duration) -> LookupResult<()> {
// A `LookupError::Timeout` may also be returned from the `Lookup`
// `LookupError::Timeout` may also be returned from the `Lookup`
// implementation. They are not distinguished during evaluation.
if self.started.elapsed() > timeout {
Err(LookupError::Timeout)
......@@ -315,73 +315,75 @@ impl Display for Sender {
}
}
// ‘local-part’ is defined in RFC 5321, §4.1.2. Modifications for
// internationalisation are in RFC 6531, §3.3. An older summary can be found in
// RFC 3696, §3.
fn is_local_part(s: &str) -> bool {
// RFC 3696, §3: ‘In addition to restrictions on syntax, there is a length
// limit on email addresses. That limit is a maximum of 64 characters
// (octets) in the "local part" (before the "@")’
// See RFC 5321, §4.5.3.1.1.
if s.len() > 64 {
return false;
}
// ‘The exact rule is that any ASCII character, including control
// characters, may appear […] in a quoted string.’
if s.starts_with('"') && s.ends_with('"') {
return s.len() >= 2 && s.is_ascii();
if s.starts_with('"') {
is_quoted_string(s)
} else {
is_dot_string(s)
}
}
// ‘period (".") may also appear, but may not be used to start or end the
// local part’
if s.starts_with('.') || s.ends_with('.') {
return false;
fn is_quoted_string(s: &str) -> bool {
fn is_qtext_smtp(c: char) -> bool {
c == ' ' || c.is_ascii_graphic() && !matches!(c, '"' | '\\') || !c.is_ascii()
}
let mut chars = s.chars();
let mut cnext = None; // lookahead
while let Some(c) = cnext.take().or_else(|| chars.next()) {
// ‘Without quotes, local-parts may consist of any combination of
// alphabetic characters, digits, or any of the special characters […]’
// – see the function definition.
if is_local_part_ascii_char(c) {
if c != '.' {
continue;
}
// ‘[…] nor may two or more consecutive periods appear.’
cnext = chars.next();
match cnext {
Some('.') => return false,
_ => continue,
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
let mut quoted = false;
for c in s[1..(s.len() - 1)].chars() {
if quoted {
if c == ' ' || c.is_ascii_graphic() {
quoted = false;
} else {
return false;
}
} else if c == '\\' {
quoted = true;
} else if !is_qtext_smtp(c) {
return false;
}
}
!quoted
} else {
false
}
}
// ‘any ASCII character, including control characters, may appear quoted
// […]. When quoting is needed, the backslash character is used to quote
// the following character.’
if c == '\\' {
match chars.next() {
Some(c) if c.is_ascii() => continue,
_ => return false,
fn is_dot_string(s: &str) -> bool {
// See RFC 5322, §3.2.3, with the modifications in RFC 6531, §3.3.
fn is_atext(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_'
| '`' | '{' | '|' | '}' | '~'
)
|| !c.is_ascii()
}
let mut dot = true;
for c in s.chars() {
if dot {
if is_atext(c) {
dot = false;
} else {
return false;
}
}
// ASCII characters not covered by the above rules are invalid. Do allow
// non-ASCII UTF-8 characters in the local-part, though.
if c.is_ascii() {
} else if c == '.' {
dot = true;
} else if !is_atext(c) {
return false;
}
}
// Note: this allows an empty local-part.
true
}
fn is_local_part_ascii_char(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_' | '`'
| '.' | '{' | '|' | '}' | '~')
!dot
}
const MAX_LOOKUPS: usize = 10;
......@@ -456,25 +458,39 @@ mod tests {
#[test]
fn is_local_part_ok() {
assert!(is_local_part("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
assert!(!is_local_part("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
assert!(is_local_part(r#""""#));
assert!(!is_local_part(r#"""#));
assert!(is_local_part(r#""x y""#));
assert!(is_local_part(r#""xy""#));
assert!(is_local_part(r#""𝔵𝔶""#));
assert!(is_local_part(r#""@""#));
assert!(is_local_part("\"\n\""));
assert!(is_local_part(""));
assert!(is_local_part(r#""x y""#));
assert!(is_local_part(r#""\x""#));
assert!(!is_local_part(r#""\𝔵""#));
assert!(is_local_part(r#""\"""#));
assert!(!is_local_part(r#""x\""#));
assert!(!is_local_part(r#"""""#));
assert!(!is_local_part(""));
assert!(!is_local_part("."));
assert!(!is_local_part(".x"));
assert!(!is_local_part("x."));
assert!(is_local_part("겫13겫12겫"));
assert!(is_local_part("x"));
assert!(is_local_part("xy"));
assert!(is_local_part("xy.z"));
assert!(is_local_part("xy.z+tag"));
assert!(!is_local_part("xy..z"));
assert!(is_local_part("겫13.12겫"));
}
#[test]
fn is_local_part_rfc3696_ok() {
// See RFC 3696, §3.
assert!(is_local_part(r#"Abc\@def"#));
assert!(is_local_part(r#"Fred\ Bloggs"#));
assert!(is_local_part(r#"Joe.\\Blow"#));
// Examples from RFC 3696, §3 (with errata!).
assert!(is_local_part(r#""Abc\@def""#));
assert!(is_local_part(r#""Fred\ Bloggs""#));
assert!(is_local_part(r#""Joe.\\Blow""#));
assert!(is_local_part(r#""Abc@def""#));
assert!(is_local_part(r#""Fred Bloggs""#));
......
......@@ -63,7 +63,7 @@
//! [`evaluate_spf`]: fn.evaluate_spf.html
//! [*check_host()*]: https://www.rfc-editor.org/rfc/rfc7208#section-4
#![doc(html_root_url = "https://docs.rs/viaspf/0.2.1")]
#![doc(html_root_url = "https://docs.rs/viaspf/0.3.0")]
macro_rules! trace {
($query:ident, $tracepoint:expr) => {
......
......@@ -663,7 +663,7 @@ impl Ip4CidrLength {
/// Creates an IPv4 CIDR prefix length with the given value, if it is within
/// bounds.
pub fn new(len: u8) -> Option<Self> {
if is_ip4_cidr_length(len) {
if is_ip4_cidr_length(&len) {
Some(Self(len))
} else {
None
......@@ -676,7 +676,7 @@ impl Ip4CidrLength {
}
}
fn is_ip4_cidr_length(len: u8) -> bool {
fn is_ip4_cidr_length(len: &u8) -> bool {
matches!(len, 0..=32)
}
......@@ -706,7 +706,7 @@ impl Ip6CidrLength {
/// Creates an IPv6 CIDR prefix length with the given value, if it is within
/// bounds.
pub fn new(len: u8) -> Option<Self> {
if is_ip6_cidr_length(len) {
if is_ip6_cidr_length(&len) {
Some(Self(len))
} else {
None
......@@ -719,7 +719,7 @@ impl Ip6CidrLength {
}
}
fn is_ip6_cidr_length(len: u8) -> bool {
fn is_ip6_cidr_length(len: &u8) -> bool {
matches!(len, 0..=128)
}
......
This diff is collapsed.