Update clap, MSRV, refactor Received header handling

This commit is contained in:
David Bürgin 2022-01-31 14:17:07 +01:00
parent 0fc96e078b
commit b6da8bbc54
10 changed files with 235 additions and 170 deletions

View file

@ -1,66 +1,77 @@
# SpamAssassin Milter changelog
## 0.3.0 (unreleased)
The minimum supported Rust version is now 1.56.1.
### Changed
* The minimum supported Rust version is now 1.56.1 (using Rust edition 2021).
* The command-line help information has changed its appearance slightly with the
update of the underlying Clap CLI library.
* The changelog is now maintained in a more structured format, similar to
https://keepachangelog.com.
## 0.2.1 (2021-12-23)
* Various cosmetic improvements in code and tests, and updates to
documentation.
* Update dependencies.
* Various cosmetic improvements in code and tests, and updates to documentation.
* Update dependencies.
## 0.2.0 (2021-08-26)
* Bump minimum supported Rust version to 1.46.0.
* (defaults change) Invoke `spamc` using the absolute path `/usr/bin/spamc`
(instead of any executable named `spamc` in the search path). To customise
this, set the environment variable `SPAMASSASSIN_MILTER_SPAMC` to the
desired path when building the application.
* Revise header rewriting logic. Handling and placement of `X-Spam-` headers
now more accurately mirrors that applied by SpamAssassin.
* Include authentication status in information passed on to SpamAssassin.
* Update dependencies.
* Bump minimum supported Rust version to 1.46.0.
* (defaults change) Invoke `spamc` using the absolute path `/usr/bin/spamc`
(instead of any executable named `spamc` in the search path). To customise
this, set the environment variable `SPAMASSASSIN_MILTER_SPAMC` to the desired
path when building the application.
* Revise header rewriting logic. Handling and placement of `X-Spam-` headers now
more accurately mirrors that applied by SpamAssassin.
* Include authentication status in information passed on to SpamAssassin.
* Update dependencies.
## 0.1.6 (2021-05-17)
* Improve processing of incoming `X-Spam-Flag` headers. Previously, in rare
circumstances a message flagged as spam would not be rejected as requested.
Reported by Petar Bogdanovic.
* Update dependencies.
* Improve processing of incoming `X-Spam-Flag` headers. Previously, in rare
circumstances a message flagged as spam would not be rejected as requested.
Reported by Petar Bogdanovic.
* Update dependencies.
## 0.1.5 (2021-03-16)
* Read output from `spamc` in a separate thread in order to avoid blocking
when processing large messages in certain configurations.
* Document requirement to keep `--max-message-size` setting in sync with
`spamc`s `--max-size` setting.
* Remove overly strict validation of command-line options.
* Properly specify minimal dependency versions in `Cargo.toml`.
* Document minimum supported Rust version 1.42.0.
* Read output from `spamc` in a separate thread in order to avoid blocking when
processing large messages in certain configurations.
* Document requirement to keep `--max-message-size` setting in sync with
`spamc`s `--max-size` setting.
* Remove overly strict validation of command-line options.
* Properly specify minimal dependency versions in `Cargo.toml`.
* Document minimum supported Rust version 1.42.0.
## 0.1.4 (2020-10-18)
* Correct a typo in log messages.
* Isolate integration tests from any existing `spamc` configuration present on
the host.
* Various à la mode style improvements in code and project metadata.
* Correct a typo in log messages.
* Isolate integration tests from any existing `spamc` configuration present on
the host.
* Various à la mode style improvements in code and project metadata.
## 0.1.3 (2020-07-04)
* Add `--reply-code`, `--reply-status-code`, and `--reply-text` options to
allow customising the SMTP reply when rejecting spam.
* Log a warning and do not truncate the message body when `--max-message-size`
is misconfigured (must be ≥ `spamc` max size as documented).
* Update dependencies in `Cargo.lock`.
* Add `--reply-code`, `--reply-status-code`, and `--reply-text` options to allow
customising the SMTP reply when rejecting spam.
* Log a warning and do not truncate the message body when `--max-message-size`
is misconfigured (must be ≥ `spamc` max size as documented).
* Update dependencies in `Cargo.lock`.
## 0.1.2 (2020-06-07)
* Bump milter dependency to version 0.2.1.
* Remove existing UNIX domain socket at target path during startup.
* Derive `Eq` and `PartialEq` for configuration structs.
* Bump milter dependency to version 0.2.1.
* Remove existing UNIX domain socket at target path during startup.
* Derive `Eq` and `PartialEq` for configuration structs.
## 0.1.1 (2020-04-13)
* Use `Write::write_all` instead of `Write::write` in `spamc` client, in order
to ensure buffers are written in their entirety.
* Do not include `.gitignore` file in published crate.
* Use `Write::write_all` instead of `Write::write` in `spamc` client, in order
to ensure buffers are written in their entirety.
* Do not include `.gitignore` file in published crate.
## 0.1.0 (2020-02-23)

101
Cargo.lock generated
View file

@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -49,19 +40,25 @@ dependencies = [
[[package]]
name = "clap"
version = "2.34.0"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
checksum = "08799f92c961c7a1cf0cc398a9073da99e21ce388b46372c37f3191f2f3eed3e"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"indexmap",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -71,6 +68,16 @@ dependencies = [
"libc",
]
[[package]]
name = "indexmap"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "ipnet"
version = "2.3.1"
@ -79,9 +86,15 @@ checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
[[package]]
name = "libc"
version = "0.2.112"
version = "0.2.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
checksum = "565dbd88872dbe4cc8a46e527f26483c1d1f7afa6b884a3bd6cd893d4f98da74"
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "milter"
@ -142,6 +155,15 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]]
name = "pkg-config"
version = "0.3.24"
@ -159,9 +181,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
@ -180,15 +202,15 @@ dependencies = [
[[package]]
name = "strsim"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.84"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
@ -196,14 +218,20 @@ dependencies = [
]
[[package]]
name = "textwrap"
version = "0.11.0"
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"unicode-width",
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
[[package]]
name = "time"
version = "0.1.44"
@ -215,24 +243,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -255,6 +271,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View file

@ -1,8 +1,8 @@
[package]
name = "spamassassin-milter"
version = "0.2.1"
edition = "2018"
rust-version = "1.46.0"
edition = "2021"
rust-version = "1.56.1"
description = "Milter for spam filtering with SpamAssassin"
license = "GPL-3.0-or-later"
categories = ["email"]
@ -12,7 +12,7 @@ exclude = ["/.gitignore", "/.gitlab-ci.yml"]
[dependencies]
chrono = "0.4.10"
clap = "2.33"
clap = "3"
ipnet = "2.2"
libc = "0.2.66"
milter = "0.2.3"

View file

@ -1,7 +1,8 @@
[Unit]
Description=SpamAssassin Milter
Documentation=man:spamassassin-milter(8)
After=network.target
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/sbin/spamassassin-milter inet:3000@localhost

View file

@ -1,5 +1,5 @@
use crate::{
client::{Client, Spamc},
client::{Client, ReceivedInfo, Spamc},
config,
};
use chrono::Local;
@ -76,6 +76,7 @@ fn handle_connect(
}
let conn = Connection::new(ip);
context.data.replace(conn)?;
Ok(Status::Continue)
@ -103,6 +104,7 @@ fn handle_mail(mut context: Context, smtp_args: Vec<&str>) -> milter::Result<Sta
let spamc = Spamc::new(config::get().spamc_args());
let sender = smtp_args[0].to_owned();
conn.client = Some(Client::new(spamc, sender));
Ok(Status::Continue)
@ -131,26 +133,26 @@ fn handle_data(mut context: Context) -> milter::Result<Status> {
return Ok(Status::Tempfail);
}
let client_ip = conn.client_ip.to_string();
// Note that when SpamAssassin reports are enabled (`report_safe 1`), the
// synthesised headers below are leaked to users in the sense that they
// are included inside the email MIME attachment in the new message body.
client.send_envelope_sender()?;
client.send_envelope_recipients()?;
client.send_synthesized_received_header(
conn.helo_host.as_ref().unwrap_or(&client_ip),
context.api.macro_value("_")?.unwrap_or(&client_ip),
context.api.macro_value("j")?.unwrap_or("localhost"),
context.api.macro_value("v")?
.and_then(|v| v.split_ascii_whitespace().next())
.unwrap_or("Postfix"),
context.api.macro_value("{tls_version}")?.is_some(),
context.api.macro_value("{auth_authen}")?.is_some(),
id,
&Local::now().to_rfc2822(),
)?;
let info = ReceivedInfo {
client_ip: conn.client_ip,
helo_host: conn.helo_host.as_deref(),
client_name_addr: context.api.macro_value("_")?,
my_hostname: context.api.macro_value("j")?,
mta: context.api.macro_value("v")?,
tls: context.api.macro_value("{tls_version}")?,
auth: context.api.macro_value("{auth_authen}")?,
queue_id: id,
date_time: Local::now().to_rfc2822(),
};
client.send_synthesized_received_header(info)?;
Ok(Status::Continue)
}

View file

@ -7,6 +7,7 @@ use milter::{ActionContext, SetErrorReply, Status};
use std::{
any::Any,
io::{self, Read, Write},
net::IpAddr,
os::unix::process::ExitStatusExt,
process::{Child, Command, Stdio},
thread::{self, JoinHandle},
@ -114,6 +115,18 @@ impl Drop for Spamc {
}
}
pub struct ReceivedInfo<'helo, 'macros> {
pub client_ip: IpAddr,
pub helo_host: Option<&'helo str>,
pub client_name_addr: Option<&'macros str>,
pub my_hostname: Option<&'macros str>,
pub mta: Option<&'macros str>,
pub tls: Option<&'macros str>,
pub auth: Option<&'macros str>,
pub queue_id: &'macros str,
pub date_time: String,
}
pub struct Client {
process: Box<dyn Process>,
sender: String,
@ -175,21 +188,21 @@ impl Client {
Ok(())
}
pub fn send_synthesized_received_header(
&mut self,
helo_host: &str,
client_name_addr: &str,
my_hostname: &str,
mta: &str,
tls: bool,
auth: bool,
queue_id: &str,
date_time: &str,
) -> Result<()> {
pub fn send_synthesized_received_header(&mut self, info: ReceivedInfo<'_, '_>) -> Result<()> {
// Sending this Received header is crucial: Milters dont see the
// MTAs own Received header. However, SpamAssassin draws a lot of
// information from that header. So we make one up and send it along.
let client_ip = info.client_ip.to_string();
let helo_host = info.helo_host.unwrap_or(&client_ip);
let client_name_addr = info.client_name_addr.unwrap_or(&client_ip);
let my_hostname = info.my_hostname.unwrap_or("localhost");
let mta = info
.mta
.and_then(|v| v.split_ascii_whitespace().next())
.unwrap_or("Postfix");
let buf = format!(
"Received: from {helo} ({client})\r\n\
\tby {hostname} ({mta}) with ESMTP{tls}{auth} id {id};\r\n\
@ -199,10 +212,10 @@ impl Client {
client = client_name_addr,
hostname = my_hostname,
mta = mta,
tls = if tls { "S" } else { "" },
auth = if auth { "A" } else { "" },
id = queue_id,
date_time = date_time,
tls = if info.tls.is_some() { "S" } else { "" },
auth = if info.auth.is_some() { "A" } else { "" },
id = info.queue_id,
date_time = info.date_time,
sender = self.sender
);

View file

@ -17,7 +17,9 @@ where
K: AsRef<str>,
{
pub fn new() -> Self {
Self { entries: Default::default() }
Self {
entries: Default::default(),
}
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
@ -74,7 +76,9 @@ where
E: AsRef<str>,
{
pub fn new() -> Self {
Self { map: StrVecMap::new() }
Self {
map: StrVecMap::new(),
}
}
pub fn contains<Q: AsRef<str>>(&self, key: Q) -> bool {

View file

@ -98,7 +98,7 @@ impl ConfigBuilder {
}
pub fn build(self) -> Config {
// TODO These invariants are enforced in `main`. In a future revision,
// These invariants are enforced in `main`. In a future revision,
// consider replacing the assertions with a `Result` return type.
assert!(
self.use_trusted_networks || self.trusted_networks.is_empty(),

View file

@ -246,7 +246,9 @@ impl<'a, 'c> HeaderRewriter<'a, 'c> {
// Delete all incoming X-Spam- headers not returned by SpamAssassin to
// get rid of foreign X-Spam-Flag etc. headers.
let deletions = self.original.keys()
let deletions = self
.original
.keys()
.filter(|n| is_spam_assassin_header(n) && !self.processed.contains(n))
.map(|name| HeaderMod::Delete { name })
.collect::<Vec<_>>();

View file

@ -1,5 +1,5 @@
use clap::{App, Arg, ArgMatches, Error, ErrorKind, Result};
use spamassassin_milter::Config;
use clap::{App, Arg, ErrorKind};
use spamassassin_milter::{Config, MILTER_NAME, VERSION};
use std::{net::IpAddr, process};
const ARG_AUTH_UNTRUSTED: &str = "AUTH_UNTRUSTED";
@ -18,78 +18,74 @@ const ARG_SOCKET: &str = "SOCKET";
const ARG_SPAMC_ARGS: &str = "SPAMC_ARGS";
fn main() {
use spamassassin_milter::{MILTER_NAME, VERSION};
let matches = App::new(MILTER_NAME)
let app = App::new(MILTER_NAME)
.version(VERSION)
.arg(Arg::with_name(ARG_AUTH_UNTRUSTED)
.short("a")
.arg(Arg::new(ARG_AUTH_UNTRUSTED)
.short('a')
.long("auth-untrusted")
.help("Treat authenticated senders as untrusted"))
.arg(Arg::with_name(ARG_DRY_RUN)
.short("n")
.arg(Arg::new(ARG_DRY_RUN)
.short('n')
.long("dry-run")
.help("Process messages without applying any changes"))
.arg(Arg::with_name(ARG_MAX_MESSAGE_SIZE)
.short("s")
.help("Process messages without applying changes"))
.arg(Arg::new(ARG_MAX_MESSAGE_SIZE)
.short('s')
.long("max-message-size")
.value_name("BYTES")
.help("Maximum message size to process"))
.arg(Arg::with_name(ARG_MILTER_DEBUG_LEVEL)
.arg(Arg::new(ARG_MILTER_DEBUG_LEVEL)
.long("milter-debug-level")
.value_name("LEVEL")
.possible_values(&["0", "1", "2", "3", "4", "5", "6"])
.possible_values(["0", "1", "2", "3", "4", "5", "6"])
.help("Set the milter library debug level")
.hidden(true) // not documented for now
.hide(true) // not documented for now
.hide_possible_values(true))
.arg(Arg::with_name(ARG_PRESERVE_BODY)
.short("B")
.arg(Arg::new(ARG_PRESERVE_BODY)
.short('B')
.long("preserve-body")
.help("Suppress rewriting of message body"))
.arg(Arg::with_name(ARG_PRESERVE_HEADERS)
.short("H")
.arg(Arg::new(ARG_PRESERVE_HEADERS)
.short('H')
.long("preserve-headers")
.help("Suppress rewriting of Subject/From/To headers"))
.arg(Arg::with_name(ARG_REJECT_SPAM)
.short("r")
.arg(Arg::new(ARG_REJECT_SPAM)
.short('r')
.long("reject-spam")
.help("Reject messages flagged as spam"))
.arg(Arg::with_name(ARG_REPLY_CODE)
.short("C")
.arg(Arg::new(ARG_REPLY_CODE)
.short('C')
.long("reply-code")
.value_name("CODE")
.help("Reply code when rejecting messages"))
.arg(Arg::with_name(ARG_REPLY_STATUS_CODE)
.short("S")
.arg(Arg::new(ARG_REPLY_STATUS_CODE)
.short('S')
.long("reply-status-code")
.value_name("CODE")
.help("Status code when rejecting messages"))
.arg(Arg::with_name(ARG_REPLY_TEXT)
.short("R")
.arg(Arg::new(ARG_REPLY_TEXT)
.short('R')
.long("reply-text")
.value_name("MSG")
.help("Reply text when rejecting messages"))
.arg(Arg::with_name(ARG_TRUSTED_NETWORKS)
.short("t")
.arg(Arg::new(ARG_TRUSTED_NETWORKS)
.short('t')
.long("trusted-networks")
.value_name("NETS")
.use_delimiter(true)
.help("Trust connections from these networks"))
.arg(Arg::with_name(ARG_VERBOSE)
.short("v")
.arg(Arg::new(ARG_VERBOSE)
.short('v')
.long("verbose")
.help("Enable verbose operation logging"))
.arg(Arg::with_name(ARG_SOCKET)
.arg(Arg::new(ARG_SOCKET)
.required(true)
.help("Listening socket of the milter"))
.arg(Arg::with_name(ARG_SPAMC_ARGS)
.arg(Arg::new(ARG_SPAMC_ARGS)
.last(true)
.multiple(true)
.help("Additional arguments to pass to spamc"))
.get_matches();
.multiple_occurrences(true)
.help("Additional arguments to pass to spamc"));
let socket = matches.value_of(ARG_SOCKET).unwrap();
let config = match build_config(&matches) {
let (socket, config) = match build_config(app) {
Ok(config) => config,
Err(e) => {
e.exit();
@ -98,8 +94,8 @@ fn main() {
eprintln!("{} {} starting", MILTER_NAME, VERSION);
match spamassassin_milter::run(socket, config) {
Ok(_) => {
match spamassassin_milter::run(&socket, config) {
Ok(()) => {
eprintln!("{} {} shut down", MILTER_NAME, VERSION);
}
Err(e) => {
@ -109,7 +105,11 @@ fn main() {
}
}
fn build_config(matches: &ArgMatches<'_>) -> Result<Config> {
fn build_config(mut app: App) -> clap::Result<(String, Config)> {
let matches = app.get_matches_mut();
let socket = matches.value_of(ARG_SOCKET).unwrap();
let mut config = Config::builder();
if let Some(bytes) = matches.value_of(ARG_MAX_MESSAGE_SIZE) {
@ -118,9 +118,9 @@ fn build_config(matches: &ArgMatches<'_>) -> Result<Config> {
config.max_message_size(bytes);
}
Err(_) => {
return Err(Error::with_description(
&format!("Invalid value for max message size: \"{}\"", bytes),
return Err(app.error(
ErrorKind::InvalidValue,
format!("Invalid value for max message size: \"{}\"", bytes),
));
}
}
@ -131,14 +131,17 @@ fn build_config(matches: &ArgMatches<'_>) -> Result<Config> {
for net in nets.filter(|n| !n.is_empty()) {
// Both `ipnet::IpNet` and `std::net::IpAddr` inputs are supported.
match net.parse().or_else(|_| net.parse::<IpAddr>().map(From::from)) {
match net
.parse()
.or_else(|_| net.parse::<IpAddr>().map(From::from))
{
Ok(net) => {
config.trusted_network(net);
}
Err(_) => {
return Err(Error::with_description(
&format!("Invalid value for trusted network address: \"{}\"", net),
return Err(app.error(
ErrorKind::InvalidValue,
format!("Invalid value for trusted network address: \"{}\"", net),
));
}
}
@ -147,7 +150,7 @@ fn build_config(matches: &ArgMatches<'_>) -> Result<Config> {
let reply_code = matches.value_of(ARG_REPLY_CODE);
let reply_status_code = matches.value_of(ARG_REPLY_STATUS_CODE);
validate_reply_codes(reply_code, reply_status_code)?;
validate_reply_codes(&mut app, reply_code, reply_status_code)?;
if matches.is_present(ARG_AUTH_UNTRUSTED) {
config.auth_untrusted(true);
@ -183,29 +186,33 @@ fn build_config(matches: &ArgMatches<'_>) -> Result<Config> {
config.spamc_args(spamc_args);
};
Ok(config.build())
Ok((socket.into(), config.build()))
}
fn validate_reply_codes(reply_code: Option<&str>, reply_status_code: Option<&str>) -> Result<()> {
fn validate_reply_codes(
app: &mut App,
reply_code: Option<&str>,
reply_status_code: Option<&str>,
) -> clap::Result<()> {
match (reply_code, reply_status_code) {
(Some(c1), Some(c2))
if !((c1.starts_with('4') || c1.starts_with('5')) && c2.starts_with(&c1[..1])) =>
{
Err(Error::with_description(
&format!(
Err(app.error(
ErrorKind::InvalidValue,
format!(
"Invalid or incompatible values for reply code and status code: \"{}\", \"{}\"",
c1, c2
),
ErrorKind::InvalidValue,
))
}
(Some(c), None) if !c.starts_with('5') => Err(Error::with_description(
&format!("Invalid value for reply code (5XX): \"{}\"", c),
(Some(c), None) if !c.starts_with('5') => Err(app.error(
ErrorKind::InvalidValue,
format!("Invalid value for reply code (5XX): \"{}\"", c),
)),
(None, Some(c)) if !c.starts_with('5') => Err(Error::with_description(
&format!("Invalid value for reply status code (5.X.X): \"{}\"", c),
(None, Some(c)) if !c.starts_with('5') => Err(app.error(
ErrorKind::InvalidValue,
format!("Invalid value for reply status code (5.X.X): \"{}\"", c),
)),
_ => Ok(()),
}