Update clap, MSRV, refactor Received header handling
This commit is contained in:
parent
0fc96e078b
commit
b6da8bbc54
10 changed files with 235 additions and 170 deletions
87
CHANGELOG.md
87
CHANGELOG.md
|
@ -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
101
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 don’t see the
|
||||
// MTA’s 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
|
||||
);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<_>>();
|
||||
|
|
119
src/main.rs
119
src/main.rs
|
@ -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(()),
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue