use crate::config::{
cli_opts::CliOptions,
model::{
DefinitiveHeloResults, EnhancedStatusCode, ExpExplainString, ExplainStringMod, Header,
HeaderType, LogDestination, LogLevel, ReasonExplainString, RejectResults, ReplyCode,
SkipSenders, Socket, SyslogFacility, TrustedNetworks,
},
Config, ConfigBuilder, ConfigError,
};
use std::{
collections::HashSet,
error::Error,
fmt::{self, Display, Formatter},
io,
net::IpAddr,
path::{Path, PathBuf},
time::Duration,
};
use tokio::fs;
use viaspf::record::ExplainString;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct RawConfig {
authserv_id: Option<String>,
definitive_helo_results: Option<DefinitiveHeloResults>,
delete_incoming_authentication_results: Option<bool>,
dry_run: Option<bool>,
fail_reply_code: Option<ReplyCode>,
fail_reply_text: Option<ExplainString>,
fail_reply_text_exp: Option<ExpExplainString>,
fail_status_code: Option<EnhancedStatusCode>,
header: Option<Header>,
hostname: Option<String>,
include_all_results: Option<bool>,
include_mailfrom_local_part: Option<bool>,
log_destination: Option<LogDestination>,
log_level: Option<LogLevel>,
max_lookups: Option<u32>,
max_void_lookups: Option<u32>,
permerror_reply_code: Option<ReplyCode>,
permerror_reply_text: Option<ReasonExplainString>,
permerror_status_code: Option<EnhancedStatusCode>,
reject_helo_results: Option<RejectResults>,
reject_results: Option<RejectResults>,
skip_senders: Option<SkipSenders>,
skip_senders_file: Option<SkipSenders>,
socket: Option<Socket>,
softfail_reply_code: Option<ReplyCode>,
softfail_reply_text: Option<ExplainString>,
softfail_status_code: Option<EnhancedStatusCode>,
syslog_facility: Option<SyslogFacility>,
temperror_reply_code: Option<ReplyCode>,
temperror_reply_text: Option<ReasonExplainString>,
temperror_status_code: Option<EnhancedStatusCode>,
timeout_secs: Option<u32>,
trust_authenticated_senders: Option<bool>,
trusted_networks: Option<TrustedNetworks>,
verify_helo: Option<bool>,
}
impl RawConfig {
fn into_builder(self, cli_opts: &CliOptions) -> Result<ConfigBuilder, ConfigError> {
let socket = cli_opts
.socket()
.cloned()
.or(self.socket)
.ok_or_else(|| ConfigError::MissingMandatoryParam("socket".into()))?;
let mut builder = Config::builder(socket);
if let Some(v) = self.authserv_id {
builder = builder.authserv_id(v);
}
if let Some(v) = self.definitive_helo_results {
builder = builder.definitive_helo_results(v);
}
if let Some(v) = self.delete_incoming_authentication_results {
builder = builder.delete_incoming_authentication_results(v);
}
if let Some(v) = self.dry_run {
builder = builder.dry_run(v);
}
if let Some(v) = self.fail_reply_code {
builder = builder.fail_reply_code(v);
}
if let Some(v) = self.fail_reply_text {
builder = builder.fail_reply_text(v);
}
if let Some(v) = self.fail_reply_text_exp {
builder = builder.fail_reply_text_exp(v);
}
if let Some(v) = self.fail_status_code {
builder = builder.fail_status_code(v);
}
if let Some(v) = self.header {
builder = builder.header(v);
}
if let Some(v) = self.hostname {
builder = builder.hostname(v);
}
if let Some(v) = self.include_all_results {
builder = builder.include_all_results(v);
}
if let Some(v) = self.include_mailfrom_local_part {
builder = builder.include_mailfrom_local_part(v);
}
if let Some(v) = self.log_destination {
builder = builder.log_destination(v);
}
if let Some(v) = self.log_level {
builder = builder.log_level(v);
}
if let Some(v) = self.max_lookups {
builder = builder.max_lookups(
v.try_into()
.map_err(|_| ConfigError::TypeConversion("max_lookups".into()))?,
);
}
if let Some(v) = self.max_void_lookups {
builder = builder.max_void_lookups(
v.try_into()
.map_err(|_| ConfigError::TypeConversion("max_void_lookups".into()))?,
);
}
if let Some(v) = self.permerror_reply_code {
builder = builder.permerror_reply_code(v);
}
if let Some(v) = self.permerror_reply_text {
builder = builder.permerror_reply_text(v);
}
if let Some(v) = self.permerror_status_code {
builder = builder.permerror_status_code(v);
}
if let Some(v) = self.reject_helo_results {
builder = builder.reject_helo_results(v);
}
if let Some(v) = self.reject_results {
builder = builder.reject_results(v);
}
match (self.skip_senders, self.skip_senders_file) {
(Some(v), None) | (None, Some(v)) => {
builder = builder.skip_senders(v);
}
(Some(v1), Some(v2)) => {
builder = builder.skip_senders(v1.extended_with(v2));
}
(None, None) => {}
}
if let Some(v) = self.softfail_reply_code {
builder = builder.softfail_reply_code(v);
}
if let Some(v) = self.softfail_reply_text {
builder = builder.softfail_reply_text(v);
}
if let Some(v) = self.softfail_status_code {
builder = builder.softfail_status_code(v);
}
if let Some(v) = self.syslog_facility {
builder = builder.syslog_facility(v);
}
if let Some(v) = self.temperror_reply_code {
builder = builder.temperror_reply_code(v);
}
if let Some(v) = self.temperror_reply_text {
builder = builder.temperror_reply_text(v);
}
if let Some(v) = self.temperror_status_code {
builder = builder.temperror_status_code(v);
}
if let Some(v) = self.timeout_secs {
builder = builder.timeout(Duration::from_secs(v.into()));
}
if let Some(v) = self.trust_authenticated_senders {
builder = builder.trust_authenticated_senders(v);
}
if let Some(v) = self.trusted_networks {
builder = builder.trusted_networks(v);
}
if let Some(v) = self.verify_helo {
builder = builder.verify_helo(v);
}
let dry_run = cli_opts.dry_run();
if dry_run {
builder = builder.dry_run(dry_run);
}
if let Some(destination) = cli_opts.log_destination() {
builder = builder.log_destination(destination);
}
if let Some(level) = cli_opts.log_level() {
builder = builder.log_level(level);
}
if let Some(facility) = cli_opts.syslog_facility() {
builder = builder.syslog_facility(facility);
}
Ok(builder)
}
}
#[derive(Debug)]
pub enum ReadConfigError {
Io(io::Error),
Parse(ParseConfigError),
}
impl Error for ReadConfigError {}
impl Display for ReadConfigError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::Parse(e) => write!(f, "error parsing configuration: {e}"),
}
}
}
impl From<io::Error> for ReadConfigError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<ReadConfigError> for ConfigError {
fn from(error: ReadConfigError) -> Self {
Self::ReadConfig(error)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ParseConfigError {
line: usize,
kind: ParseParamError,
}
impl Error for ParseConfigError {}
impl Display for ParseConfigError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "line {}: {}", self.line, self.kind)
}
}
impl From<ParseConfigError> for ReadConfigError {
fn from(error: ParseConfigError) -> Self {
Self::Parse(error)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ParseParamError {
InvalidLine,
UnknownKey(String),
DuplicateKey(String),
InvalidValue,
InvalidSocket(String),
InvalidLogDestination(String),
InvalidLogLevel(String),
InvalidSyslogFacility(String),
InvalidReplyCode(String),
InvalidEnhancedStatusCode(String),
InvalidBoolean(String),
InvalidExplainString(String),
InvalidNetworkAddress(String),
InvalidU32(String),
InvalidI32(String),
InvalidRejectResult(String),
InvalidDefinitiveHeloResult(String),
InvalidHeaderField(String),
DuplicateHeaderField(HeaderType),
ReadIncludedFile(PathBuf, String), InvalidSkipEntry(String),
InvalidSkipEntryInFile(String, PathBuf),
}
impl Error for ParseParamError {}
impl Display for ParseParamError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLine => write!(f, "invalid line syntax"),
Self::UnknownKey(key) => write!(f, "unknown parameter \"{key}\""),
Self::DuplicateKey(key) => write!(f, "duplicate parameter \"{key}\""),
Self::InvalidValue => write!(f, "invalid parameter value syntax"),
Self::InvalidSocket(s) => write!(f, "invalid socket \"{s}\""),
Self::InvalidLogDestination(s) => write!(f, "invalid log destination \"{s}\""),
Self::InvalidLogLevel(s) => write!(f, "invalid log level \"{s}\""),
Self::InvalidSyslogFacility(s) => write!(f, "invalid syslog facility \"{s}\""),
Self::InvalidReplyCode(s) => write!(f, "invalid reply code \"{s}\""),
Self::InvalidEnhancedStatusCode(s) => write!(f, "invalid enhanced status code \"{s}\""),
Self::InvalidBoolean(s) => write!(f, "invalid Boolean value \"{s}\""),
Self::InvalidExplainString(s) => write!(f, "invalid explain-string \"{s}\""),
Self::InvalidNetworkAddress(s) => write!(f, "invalid network address \"{s}\""),
Self::InvalidU32(i) => write!(f, "invalid integer \"{i}\""),
Self::InvalidI32(i) => write!(f, "invalid integer \"{i}\""),
Self::InvalidRejectResult(s) => write!(f, "invalid SPF result \"{s}\""),
Self::InvalidDefinitiveHeloResult(s) => write!(f, "invalid SPF result \"{s}\""),
Self::InvalidHeaderField(s) => write!(f, "invalid header field \"{s}\""),
Self::DuplicateHeaderField(h) => write!(f, "duplicate header field \"{h}\""),
Self::ReadIncludedFile(p, e) => write!(f, "failed to read file {}: {}", p.display(), e),
Self::InvalidSkipEntry(s) => write!(f, "invalid sender \"{s}\""),
Self::InvalidSkipEntryInFile(s, p) => write!(f, "invalid sender \"{s}\" in {}", p.display()),
}
}
}
pub fn focus_error(error: &ConfigError) -> &dyn Error {
match error {
ConfigError::ReadConfig(e) => match e {
ReadConfigError::Parse(e) => e,
e => e,
},
e => e,
}
}
pub async fn read_config(opts: &CliOptions) -> Result<Config, ConfigError> {
let config_file = opts.config_file();
let raw_config = read_raw_config(config_file).await?;
raw_config.into_builder(opts)?.build()
}
async fn read_raw_config<P: AsRef<Path>>(path: P) -> Result<RawConfig, ReadConfigError> {
let s = fs::read_to_string(path).await?;
let raw_config = parse_raw_config(&s).await?;
Ok(raw_config)
}
async fn parse_raw_config(s: &str) -> Result<RawConfig, ParseConfigError> {
let mut config = Default::default();
let mut keys_seen = HashSet::new();
for (num, line) in s.lines().enumerate() {
let num = num + 1;
let line = line.trim();
if is_ignored_line(line) {
continue;
}
match line.split_once('=') {
Some((k, v)) => {
let k = k.trim();
let v = v.trim();
if keys_seen.contains(k) {
return Err(ParseConfigError {
line: num,
kind: ParseParamError::DuplicateKey(k.into()),
});
}
parse_config_entry(&mut config, k, v)
.await
.map_err(|e| ParseConfigError { line: num, kind: e })?;
keys_seen.insert(k);
}
None => {
return Err(ParseConfigError {
line: num,
kind: ParseParamError::InvalidLine,
});
}
}
}
Ok(config)
}
type ParseParamResult<T> = Result<T, ParseParamError>;
async fn parse_config_entry(
config: &mut RawConfig,
key: &str,
value: &str,
) -> ParseParamResult<()> {
match key {
"authserv_id" => config.authserv_id = Some(value.to_owned()),
"definitive_helo_results" => config.definitive_helo_results = Some(value.parse_param()?),
"delete_incoming_authentication_results" => config.delete_incoming_authentication_results = Some(value.parse_param()?),
"dry_run" => config.dry_run = Some(value.parse_param()?),
"fail_reply_code" => config.fail_reply_code = Some(value.parse_param()?),
"fail_reply_text" => config.fail_reply_text = Some(value.parse_param()?),
"fail_reply_text_exp" => config.fail_reply_text_exp = Some(value.parse_param()?),
"fail_status_code" => config.fail_status_code = Some(value.parse_param()?),
"header" => config.header = Some(value.parse_param()?),
"hostname" => config.hostname = Some(value.to_owned()),
"include_all_results" => config.include_all_results = Some(value.parse_param()?),
"include_mailfrom_local_part" => config.include_mailfrom_local_part = Some(value.parse_param()?),
"log_destination" => config.log_destination = Some(value.parse_param()?),
"log_level" => config.log_level = Some(value.parse_param()?),
"max_lookups" => config.max_lookups = Some(value.parse_param()?),
"max_void_lookups" => config.max_void_lookups = Some(value.parse_param()?),
"permerror_reply_code" => config.permerror_reply_code = Some(value.parse_param()?),
"permerror_reply_text" => config.permerror_reply_text = Some(value.parse_param()?),
"permerror_status_code" => config.permerror_status_code = Some(value.parse_param()?),
"reject_helo_results" => config.reject_helo_results = Some(value.parse_param()?),
"reject_results" => config.reject_results = Some(value.parse_param()?),
"skip_senders" => config.skip_senders = Some(value.parse_param()?),
"skip_senders_file" => config.skip_senders_file = Some(parse_skip_senders_file(value).await?),
"socket" => config.socket = Some(value.parse_param()?),
"softfail_reply_code" => config.softfail_reply_code = Some(value.parse_param()?),
"softfail_reply_text" => config.softfail_reply_text = Some(value.parse_param()?),
"softfail_status_code" => config.softfail_status_code = Some(value.parse_param()?),
"syslog_facility" => config.syslog_facility = Some(value.parse_param()?),
"temperror_reply_code" => config.temperror_reply_code = Some(value.parse_param()?),
"temperror_reply_text" => config.temperror_reply_text = Some(value.parse_param()?),
"temperror_status_code" => config.temperror_status_code = Some(value.parse_param()?),
"timeout_secs" => config.timeout_secs = Some(value.parse_param()?),
"trust_authenticated_senders" => config.trust_authenticated_senders = Some(value.parse_param()?),
"trusted_networks" => config.trusted_networks = Some(value.parse_param()?),
"verify_helo" => config.verify_helo = Some(value.parse_param()?),
_ => {
return Err(ParseParamError::UnknownKey(key.into()));
}
}
Ok(())
}
trait ParseParam {
fn parse_param<F: FromParamStr>(&self) -> ParseParamResult<F>;
}
impl ParseParam for &str {
fn parse_param<F: FromParamStr>(&self) -> ParseParamResult<F> {
FromParamStr::from_param_str(self)
}
}
trait FromParamStr: Sized {
fn from_param_str(s: &str) -> ParseParamResult<Self>;
}
impl FromParamStr for i32 {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidI32(s.into()))
}
}
impl FromParamStr for u32 {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidU32(s.into()))
}
}
impl FromParamStr for bool {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
match s {
"yes" | "true" => Ok(true),
"no" | "false" => Ok(false),
_ => Err(ParseParamError::InvalidBoolean(s.into())),
}
}
}
impl FromParamStr for Socket {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidSocket(s.into()))
}
}
impl FromParamStr for ReplyCode {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidReplyCode(s.into()))
}
}
impl FromParamStr for EnhancedStatusCode {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidEnhancedStatusCode(s.into()))
}
}
impl FromParamStr for ExplainString {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidExplainString(s.into()))
}
}
impl FromParamStr for Header {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
let mut header = Vec::new();
for value in split_param_values(s) {
let value = value?;
let header_type = value
.parse()
.map_err(|_| ParseParamError::InvalidHeaderField(value.into()))?;
if header.contains(&header_type) {
return Err(ParseParamError::DuplicateHeaderField(header_type));
}
header.push(header_type);
}
Ok(Self::new(header).unwrap())
}
}
impl FromParamStr for ExpExplainString {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
parse_explain_string_mod(s, "%{exp}").map(From::from)
}
}
impl FromParamStr for ReasonExplainString {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
parse_explain_string_mod(s, "%{reason}").map(From::from)
}
}
fn parse_explain_string_mod(s: &str, token: &str) -> ParseParamResult<ExplainStringMod> {
fn split_once_token<'a>(s: &'a str, token: &str) -> Option<(&'a str, &'a str)> {
for (i, token) in s.match_indices(token) {
if s[..i].bytes().rev().take_while(|&c| c == b'%').count() % 2 == 0 {
return Some((&s[..i], &s[(i + token.len())..]));
}
}
None
}
match split_once_token(s, token) {
Some((prefix, suffix)) => {
let prefix = prefix
.parse()
.map_err(|_| ParseParamError::InvalidExplainString(s.into()))?;
let suffix = suffix
.parse()
.map_err(|_| ParseParamError::InvalidExplainString(s.into()))?;
Ok(ExplainStringMod::Decorate { prefix, suffix })
}
None => {
let es = s
.parse()
.map_err(|_| ParseParamError::InvalidExplainString(s.into()))?;
Ok(ExplainStringMod::Substitute(es))
}
}
}
impl FromParamStr for DefinitiveHeloResults {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
let mut results = HashSet::new();
for value in split_param_values(s) {
let value = value?;
let kind = value
.parse()
.map_err(|_| ParseParamError::InvalidDefinitiveHeloResult(value.into()))?;
results.insert(kind);
}
Ok(results.into())
}
}
impl FromParamStr for RejectResults {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
let mut results = HashSet::new();
for value in split_param_values(s) {
let value = value?;
let kind = value
.parse()
.map_err(|_| ParseParamError::InvalidRejectResult(value.into()))?;
results.insert(kind);
}
Ok(results.into())
}
}
impl FromParamStr for LogLevel {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidLogLevel(s.into()))
}
}
impl FromParamStr for LogDestination {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidLogDestination(s.into()))
}
}
impl FromParamStr for SyslogFacility {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
s.parse().map_err(|_| ParseParamError::InvalidSyslogFacility(s.into()))
}
}
impl FromParamStr for TrustedNetworks {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
let mut trusted_networks = TrustedNetworks {
trust_loopback: false,
networks: HashSet::new(),
};
for value in split_param_values(s) {
let value = value?;
if value == "loopback" {
trusted_networks.trust_loopback = true;
} else {
let net = value
.parse()
.or_else(|_| value.parse::<IpAddr>().map(From::from))
.map_err(|_| ParseParamError::InvalidNetworkAddress(value.into()))?;
trusted_networks.networks.insert(net);
}
}
Ok(trusted_networks)
}
}
impl FromParamStr for SkipSenders {
fn from_param_str(s: &str) -> ParseParamResult<Self> {
let mut entries = HashSet::new();
for value in split_param_values(s) {
let value = value?;
let entry = value
.parse()
.map_err(|_| ParseParamError::InvalidSkipEntry(value.into()))?;
entries.insert(entry);
}
Ok(entries.into())
}
}
async fn parse_skip_senders_file(s: &str) -> ParseParamResult<SkipSenders> {
let file_content = fs::read_to_string(s)
.await
.map_err(|e| ParseParamError::ReadIncludedFile(s.into(), e.to_string()))?;
parse_skip_senders_file_content(&file_content, s)
}
fn parse_skip_senders_file_content(s: &str, file_name: &str) -> ParseParamResult<SkipSenders> {
let mut entries = HashSet::new();
for line in s.lines() {
let line = line.trim();
if is_ignored_line(line) {
continue;
}
let entry = line
.parse()
.map_err(|_| ParseParamError::InvalidSkipEntryInFile(line.into(), file_name.into()))?;
entries.insert(entry);
}
Ok(entries.into())
}
fn split_param_values(value: &str) -> impl Iterator<Item = ParseParamResult<&str>> {
let value = value.trim();
let mut values = value.split(',');
if value.is_empty() {
values.next();
}
values.map(|s| {
let s = s.trim();
if s.is_empty() {
Err(ParseParamError::InvalidValue)
} else {
Ok(s)
}
})
}
fn is_ignored_line(line: &str) -> bool {
let line = line.trim_start();
line.is_empty() || line.starts_with('#')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::model::{DefinitiveHeloResultKind, RejectResultKind, SkipEntry};
use viaspf::DomainName;
#[test]
fn socket_with_cli_options_override() {
let config = RawConfig {
socket: Some(Socket::Unix("raw_config".into())),
..Default::default()
};
let opts = CliOptions::builder()
.socket(Socket::Unix("cli_opts".into()))
.build();
let config = config.into_builder(&opts).unwrap().build().unwrap();
assert_eq!(config.socket(), &Socket::Unix("cli_opts".into()));
}
#[tokio::test]
async fn parse_results_ok() {
let config = "
definitive_helo_results = pass, fail, temperror, permerror
reject_helo_results = fail, temperror
reject_results = fail, softfail, permerror
";
let raw_config = parse_raw_config(config).await.unwrap();
assert_eq!(
raw_config.definitive_helo_results,
Some(
HashSet::from([
DefinitiveHeloResultKind::Pass,
DefinitiveHeloResultKind::Fail,
DefinitiveHeloResultKind::Temperror,
DefinitiveHeloResultKind::Permerror,
])
.into()
)
);
assert_eq!(
raw_config.reject_helo_results,
Some(HashSet::from([RejectResultKind::Fail, RejectResultKind::Temperror]).into())
);
assert_eq!(
raw_config.reject_results,
Some(
HashSet::from([
RejectResultKind::Fail,
RejectResultKind::Softfail,
RejectResultKind::Permerror,
])
.into()
)
);
}
#[tokio::test]
async fn parse_results_invalid_result() {
let config = "reject_results = softfail, Failure";
let raw_config = parse_raw_config(config).await;
assert_eq!(
raw_config,
Err(ParseConfigError {
line: 1,
kind: ParseParamError::InvalidRejectResult("Failure".into()),
})
);
}
#[test]
fn parse_exp_explain_string_ok() {
let es = "domain %{o} %%{exp}".parse_param::<ExpExplainString>().unwrap();
assert_eq!(
es.as_ref(),
&ExplainStringMod::Substitute("domain %{o} %%{exp}".parse().unwrap())
);
let es = "domain %{o}: \"%{exp}\"".parse_param::<ExpExplainString>().unwrap();
assert_eq!(
es.as_ref(),
&ExplainStringMod::Decorate {
prefix: "domain %{o}: \"".parse().unwrap(),
suffix: "\"".parse().unwrap(),
}
);
let es = "domain %{o}: %{exp} %{exp}".parse_param::<ExpExplainString>();
assert_eq!(
es,
Err(ParseParamError::InvalidExplainString(
"domain %{o}: %{exp} %{exp}".into()
))
);
}
#[test]
fn parse_skip_senders_ok() {
let skip_senders = "Example.Com, me@Example.Com"
.parse_param::<SkipSenders>()
.unwrap();
assert_eq!(
skip_senders,
HashSet::from([
SkipEntry {
local_part: None,
domain: DomainName::new("example.com").unwrap(),
match_subdomains: false,
},
SkipEntry {
local_part: Some("me".into()),
domain: DomainName::new("example.com").unwrap(),
match_subdomains: false,
},
])
.into()
);
}
#[test]
fn parse_skip_senders_file_content_ok() {
let config = "
# Domains
Example.Com
.Example.Com
# Email addresses
me@Example.Com
";
let skip_senders = parse_skip_senders_file_content(config, "unused").unwrap();
assert_eq!(
skip_senders,
HashSet::from([
SkipEntry {
local_part: None,
domain: DomainName::new("example.com").unwrap(),
match_subdomains: false,
},
SkipEntry {
local_part: None,
domain: DomainName::new("example.com").unwrap(),
match_subdomains: true,
},
SkipEntry {
local_part: Some("me".into()),
domain: DomainName::new("example.com").unwrap(),
match_subdomains: false,
},
])
.into()
);
}
#[test]
fn split_param_values_ok() {
assert!(split_param_values(" ").eq([]));
assert!(split_param_values(" a").eq([Ok("a")]));
assert!(split_param_values(" a,").eq([Ok("a"), Err(ParseParamError::InvalidValue)]));
assert!(split_param_values(" ,a").eq([Err(ParseParamError::InvalidValue), Ok("a")]));
assert!(split_param_values(" ,").eq([
Err(ParseParamError::InvalidValue),
Err(ParseParamError::InvalidValue)
]));
assert!(split_param_values(" a,b").eq([Ok("a"), Ok("b")]));
}
}