use email_address_parser::EmailAddress; use fqdn::FQDN; use std::cmp::Ordering; use std::collections::BTreeMap; use std::env; use std::fs::File; use std::io::{self, BufRead}; use std::path::Path; use std::str::FromStr; struct AliasFile { content: io::Lines>, default_domain: FQDN, } #[derive(PartialEq, Eq, Clone)] struct OrdEmailAddress(EmailAddress); impl PartialOrd for OrdEmailAddress { fn partial_cmp(&self, other: &Self) -> Option { Some(self.0.to_string().cmp(&other.0.to_string())) } } impl Ord for OrdEmailAddress { fn cmp(&self, other: &Self) -> Ordering { self.0.to_string().cmp(&other.0.to_string()) } } type AliasMap = BTreeMap>; fn main() { let args: Vec = env::args().collect(); if args.len() < 2 || args.len() % 2 == 0 { print_help(); return; } // Collect alias files and their default domains let mut alias_files: Vec = Vec::new(); for i in (1..args.len()).step_by(2) { if let Ok(lines) = read_lines(&args[i]) { alias_files.push(AliasFile { content: lines, default_domain: FQDN::from_str(&args[i + 1]).unwrap(), }); } } println!("{}", generate_sieve_script(parse_alias_to_map(alias_files))); } /// Read a virtual alias file /// and convert it to a map of destination addresses to a list of their final forwarding addresses. fn parse_alias_to_map(alias_files: Vec) -> AliasMap { // File must exist in the current path let mut redirect_map: AliasMap = AliasMap::new(); let mut destinations: Vec = Vec::new(); // Extract all pairs (destination to redirect addresses) from the alias files for alias_file in alias_files { for line in alias_file.content { let line = line.unwrap(); // Ignore comments in the alias file let line = String::from(line.split_at(line.find('#').unwrap_or(line.len())).0); let destination = line.split_at(line.find(char::is_whitespace).unwrap_or(0)).0; if destination.is_empty() { continue; } let redirects: Vec = 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)) .collect(); if redirects.is_empty() { continue; } destinations.push(to_mailaddress(destination, &alias_file.default_domain)); redirect_map.insert( to_mailaddress(destination, &alias_file.default_domain), redirects, ); } } // Replace redirects that are again forwarded elsewhere by that. // Break after depth max_iterations and assume infinite recursion afterwards. let mut changed = true; let mut iterations = 0; let max_iterations = 100; while changed && iterations < max_iterations { changed = false; iterations += 1; let mut all_new_redirects: AliasMap = AliasMap::new(); for destination in &destinations { for forward_to in redirect_map.get(destination).unwrap() { if let Some(new_redirects) = redirect_map.get(forward_to) { changed = true; all_new_redirects .entry(destination.clone()) .or_insert(redirect_map.get(destination).unwrap().clone()) .retain(|dest| *dest != *forward_to); all_new_redirects .entry(destination.clone()) .and_modify(|d| d.extend(new_redirects.iter().cloned())); } } } for (destination, new_redirect) in all_new_redirects { *redirect_map.get_mut(&destination).unwrap() = new_redirect; } } assert!(iterations != max_iterations, "Possibly infinite recursion detected in parse_alias_map. Did not terminate after {max_iterations} rounds."); redirect_map } /// Generate a Sieve script /// from a map of destination addresses to a list of their forwarding addresses. /// /// Addresses are sorted according to the order on `OrdEmailAddress`. fn generate_sieve_script(redirects: AliasMap) -> String { let mut script: String = "require [\"envelope\", \"copy\"];\n\n".to_string(); for (redirect, mut destinations) in redirects { script += format!("if envelope :is \"to\" \"{}\" {{\n{}}}\n", redirect.0, { let mut subscript: String = String::new(); destinations.sort(); for destination in destinations.iter().rev().skip(1).rev() { subscript += format!(" redirect :copy \"{}\";\n", destination.0).as_str(); } subscript + format!( " redirect \"{}\";\n", destinations.iter().next_back().unwrap().0 ) .as_str() }) .as_str(); } script } /// 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) -> OrdEmailAddress { let mut addr = alias_entry.trim().to_string(); addr = addr.replace(',', ""); if addr.contains('@') { return OrdEmailAddress(EmailAddress::parse(&addr, None).unwrap()); } OrdEmailAddress(EmailAddress::new(&addr, &default_domain.to_string(), None).unwrap()) } // The output is wrapped in a Result to allow matching on errors. // Returns an Iterator to the Reader of the lines of the file. fn read_lines

(filename: P) -> io::Result>> where P: AsRef, { let file = File::open(filename)?; Ok(io::BufReader::new(file).lines()) } fn print_help() { print!( "Reads a virtual alias file and needs a default domain to append to local paths, e.g. ./alias_to_sieve example.com.txt example.com" ); }