Disallow apostrophies in mail addresses of alias files

This commit is contained in:
Gonne 2025-03-30 09:35:09 +02:00
parent 1a819e1ca4
commit eb86511fe7
Signed by: Gonne
SSH key fingerprint: SHA256:J8w3ZCNyz9MoTLV+eU7YRTVw59NYig44i0IWhbsgQG8
3 changed files with 65 additions and 29 deletions

View file

@ -12,29 +12,59 @@ pub struct AliasFile {
pub default_domain: FQDN,
}
#[derive(PartialEq, Eq, Clone)]
pub struct OrdEmailAddress(EmailAddress);
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct AliasEmailAddress(EmailAddress);
impl PartialOrd for OrdEmailAddress {
impl AliasEmailAddress {
/// Create an `AliasEmailAddress` from some alias entry.
/// Return parameter for complete mail addresses and append the default domain for local parts.
pub fn new(
alias_entry: &str,
default_domain: &FQDN,
) -> Result<AliasEmailAddress, Box<dyn Error>> {
let mut addr = alias_entry.trim().to_string();
addr = addr.replace(',', "");
// The domain already fails on instantiation of the FQDN type if it contains an apostrophe.
if addr.contains('\'') {
return Err(format!(
"Mailaddress {addr} contains an apostrophe which breaks the script generation."
)
.into());
}
if addr.contains('@') {
return Ok(AliasEmailAddress(
EmailAddress::parse(&addr, None).ok_or::<Box<dyn Error>>(
String::from("Mailaddress {addr} not parsable.").into(),
)?,
));
}
let unsortable_mail = EmailAddress::new(&addr, &default_domain.to_string(), None)?;
Ok(AliasEmailAddress(unsortable_mail))
}
}
impl PartialOrd for AliasEmailAddress {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.0.to_string().cmp(&other.0.to_string()))
}
}
impl Ord for OrdEmailAddress {
impl Ord for AliasEmailAddress {
fn cmp(&self, other: &Self) -> Ordering {
self.0.to_string().cmp(&other.0.to_string())
}
}
pub type AliasMap = BTreeMap<OrdEmailAddress, Vec<OrdEmailAddress>>;
pub type AliasMap = BTreeMap<AliasEmailAddress, Vec<AliasEmailAddress>>;
/// Read a virtual alias file <https://www.postfix.org/virtual.5.html>
/// and convert it to a map of destination addresses to a list of their final forwarding addresses.
pub fn parse_alias_to_map(alias_files: Vec<AliasFile>) -> Result<AliasMap, Box<dyn Error>> {
// File must exist in the current path
let mut redirect_map: AliasMap = AliasMap::new();
let mut destinations: Vec<OrdEmailAddress> = Vec::new();
let mut destinations: Vec<AliasEmailAddress> = Vec::new();
// Extract all pairs (destination to redirect addresses) from the alias files
for alias_file in alias_files {
@ -48,20 +78,23 @@ pub fn parse_alias_to_map(alias_files: Vec<AliasFile>) -> Result<AliasMap, Box<d
continue;
}
let redirects: Vec<OrdEmailAddress> = line
let redirects: Vec<AliasEmailAddress> = line
.split_at(line.find(char::is_whitespace).unwrap_or(0))
.1
.split(' ')
.filter(|address| !address.trim().to_string().replace(',', "").is_empty())
.map(|addr| to_mailaddress(addr, &alias_file.default_domain))
.map(|addr| AliasEmailAddress::new(addr, &alias_file.default_domain))
.collect::<Result<Vec<_>, _>>()?;
if redirects.is_empty() {
continue;
}
destinations.push(to_mailaddress(destination, &alias_file.default_domain)?);
destinations.push(AliasEmailAddress::new(
destination,
&alias_file.default_domain,
)?);
redirect_map.insert(
to_mailaddress(destination, &alias_file.default_domain)?,
AliasEmailAddress::new(destination, &alias_file.default_domain)?,
redirects,
);
}
@ -95,29 +128,11 @@ pub fn parse_alias_to_map(alias_files: Vec<AliasFile>) -> Result<AliasMap, Box<d
}
}
if iterations == max_iterations {
return Err(String::from("Possibly infinite recursion detected in parse_alias_map. Did not terminate after {max_iterations} rounds.").into());
return Err(format!("Possibly infinite recursion detected in parse_alias_map. Did not terminate after {max_iterations} rounds.").into());
}
Ok(redirect_map)
}
/// Create an `OrdEmailAddress` from some alias entry.
/// Return parameter for complete mail addresses and append the default domain for local parts.
fn to_mailaddress(
alias_entry: &str,
default_domain: &FQDN,
) -> Result<OrdEmailAddress, Box<dyn Error>> {
let mut addr = alias_entry.trim().to_string();
addr = addr.replace(',', "");
if addr.contains('@') {
return Ok(OrdEmailAddress(
EmailAddress::parse(&addr, None)
.ok_or::<Box<dyn Error>>(String::from("Mailaddress {addr} not parsable.").into())?,
));
}
let unsortable_mail = EmailAddress::new(&addr, &default_domain.to_string(), None)?;
Ok(OrdEmailAddress(unsortable_mail))
}
// The output is wrapped in a Result to allow matching on errors.
// Returns an Iterator to the Reader of the lines of the file.
pub fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
@ -190,6 +205,23 @@ mod tests {
assert!(result.is_err());
}
#[test]
fn apostrophe_destination_detection() {
let result = parse_alias_to_map(vec![AliasFile {
content: read_lines("testdata/apostrophe_destination.aliases").unwrap(),
default_domain: FQDN::from_str("example.com").unwrap(),
}]);
assert!(result.is_err());
}
#[test]
fn apostrophe_redirect_detection() {
let result = parse_alias_to_map(vec![AliasFile {
content: read_lines("testdata/apostrophe_redirect.aliases").unwrap(),
default_domain: FQDN::from_str("example.com").unwrap(),
}]);
assert!(result.is_err());
}
#[test]
fn basic_parsing() {
let result = parse_alias_to_map(vec![AliasFile {

View file

@ -0,0 +1,2 @@
# Apostrophes are not allowed
'orga me@example.com

View file

@ -0,0 +1,2 @@
# Apostrophes are not allowed
orga me@e'xample.com