Clean up config builder, add timeout to mock server
This commit is contained in:
parent
02b7e2ad64
commit
4525df41cb
18 changed files with 146 additions and 116 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -60,9 +60,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772"
|
||||
checksum = "e2c55f143919fbc0bc77e427fe2d74cf23786d7c1875666f2fde3ac3c659bb67"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -75,9 +75,9 @@ checksum = "a859057dc563d1388c1e816f98a1892629075fc046ed06e845b883bb8b2916fb"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.66"
|
||||
version = "0.2.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
|
||||
checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
|
||||
|
||||
[[package]]
|
||||
name = "milter"
|
||||
|
@ -188,9 +188,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5"
|
||||
checksum = "7a0294dc449adc58bb6592fff1a23d3e5e6e235afc6a0ffca2657d19e7bbffe5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
58
README.md
58
README.md
|
@ -1,11 +1,11 @@
|
|||
# SpamAssassin Milter
|
||||
|
||||
SpamAssassin Milter is a [milter] application that filters email through
|
||||
SpamAssassin Milter is a milter application that filters email through
|
||||
SpamAssassin server using the `spamc` client. It is a light-weight component
|
||||
that serves to integrate [Apache SpamAssassin] with a milter-capable MTA (mail
|
||||
server) such as [Postfix]. Its task is thus helping combat spam on email sites.
|
||||
|
||||
SpamAssassin Milter operates as a milter hooked into the MTA’s SMTP protocol
|
||||
SpamAssassin Milter operates as a [milter] hooked into the MTA’s SMTP protocol
|
||||
handler. It passes incoming messages to SpamAssassin for analysis, and then
|
||||
interprets the response from SpamAssassin and applies suggested changes to the
|
||||
message.
|
||||
|
@ -26,20 +26,20 @@ to alter the default behaviour. See below for a discussion of several
|
|||
configuration and integration approaches.
|
||||
|
||||
This application can be used as a replacement for [spamass-milt]; it has a
|
||||
reduced feature set, but it should be satisfactory for a personal mail server
|
||||
setup. SpamAssassin Milter has been used in such a setup together with Postfix,
|
||||
SpamAssassin, and for delivery [Dovecot] with LMTP and the [Sieve plugin].
|
||||
reduced feature set, but it should be sufficient for common mail server setups.
|
||||
SpamAssassin Milter has been used in such a setup together with Postfix, and for
|
||||
delivery [Dovecot] with LMTP and the [Sieve plugin].
|
||||
|
||||
[milter]: https://crates.io/crates/milter
|
||||
[Apache SpamAssassin]: https://spamassassin.apache.org
|
||||
[Postfix]: http://www.postfix.org
|
||||
[spamass-milt]: https://savannah.nongnu.org/projects/spamass-milt/
|
||||
[milter]: https://crates.io/crates/milter
|
||||
[spamass-milt]: https://savannah.nongnu.org/projects/spamass-milt
|
||||
[Dovecot]: https://dovecot.org
|
||||
[Sieve plugin]: https://doc.dovecot.org/configuration_manual/sieve/
|
||||
[Sieve plugin]: https://doc.dovecot.org/configuration_manual/sieve
|
||||
|
||||
## Building
|
||||
|
||||
This project is a Rust package. Build it with Cargo as usual.
|
||||
This project is a [Rust] package. Build it with Cargo as usual.
|
||||
|
||||
As a milter, this package requires the libmilter C library to be available. Be
|
||||
sure to install the libmilter shared library and header files.
|
||||
|
@ -59,6 +59,8 @@ Note that until recently `miltertest` had a serious bug that prevents most
|
|||
integration tests in this package from completing. Make sure you use an
|
||||
up-to-date version of `miltertest`.
|
||||
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, SpamAssassin Milter can be by invoked as `spamassassin-milter`.
|
||||
|
@ -74,7 +76,7 @@ For example, the following invocation starts SpamAssassin Milter on port 3000:
|
|||
spamassassin-milter inet:3000@localhost
|
||||
```
|
||||
|
||||
The available options and flags can be glimpsed by passing the `-h` flag:
|
||||
The available options can be glimpsed by passing the `-h` flag:
|
||||
|
||||
```
|
||||
spamassassin-milter -h
|
||||
|
@ -101,8 +103,8 @@ just picking a socket and things should just work. Some integration options are
|
|||
discussed in subsequent sections.
|
||||
|
||||
New users may wish to run SpamAssassin Milter with the `--dry-run` option before
|
||||
going live. Combined with the `--verbose` option, this gives accurate insight
|
||||
into the changes that SpamAssassin Milter would apply.
|
||||
going live. Combined with `--verbose`, this gives accurate insight into the
|
||||
changes that SpamAssassin Milter would apply.
|
||||
|
||||
```
|
||||
spamassassin-milter --dry-run --verbose inet:3000@localhost
|
||||
|
@ -143,7 +145,7 @@ report_safe 0
|
|||
```
|
||||
|
||||
In addition, body rewriting can also be suppressed on the SpamAssassin Milter
|
||||
side with the `--preserve-body` flag.
|
||||
side with the `--preserve-body` option.
|
||||
|
||||
SpamAssassin may also rewrite the Subject and other headers, for example, adding
|
||||
a prefix ‘\*\*\*Spam\*\*\*’. This is not done by default, but may be enabled
|
||||
|
@ -154,7 +156,7 @@ rewrite_header Subject ***SPAM***
|
|||
```
|
||||
|
||||
SpamAssassin Milter by default applies header rewriting. Header rewriting can be
|
||||
suppressed on the SpamAssassin Milter side with the `--preserve-headers` flag.
|
||||
suppressed on the SpamAssassin Milter side with the `--preserve-headers` option.
|
||||
|
||||
### `spamc` configuration
|
||||
|
||||
|
@ -167,7 +169,7 @@ By default, `spamc` will try to reach SpamAssassin server on the dedicated port
|
|||
Milter without further configuration.
|
||||
|
||||
If SpamAssassin server listens on a different port or on a UNIX domain socket
|
||||
instead, set the `--socket` option as appropriate in `spamc.conf`:
|
||||
instead, set the `--port` or `--socket` option as appropriate in `spamc.conf`:
|
||||
|
||||
```
|
||||
--socket=/run/spamassassin/spamd.sock
|
||||
|
@ -188,7 +190,7 @@ resist failure to an extent that it will not indicate failure even if it cannot
|
|||
connect to SpamAssassin server at all (apart from warnings logged to syslog)! If
|
||||
it cannot connect to the server, it simply echoes what it received, and so masks
|
||||
the error condition. This behaviour is labelled ‘safe fallback’ and is perhaps
|
||||
best disabled once the system is set up. Set the following flag:
|
||||
best disabled once the system is set up. Set the following option:
|
||||
|
||||
```
|
||||
--no-safe-fallback
|
||||
|
@ -196,9 +198,9 @@ best disabled once the system is set up. Set the following flag:
|
|||
|
||||
## Integration with Postfix
|
||||
|
||||
To integrate with Postfix, ensure the SpamAssassin Milter service is up and
|
||||
running, and then configure its listening socket in `/etc/postfix/main.cf` as
|
||||
follows:
|
||||
To integrate with Postfix, confirm that the SpamAssassin Milter service is up
|
||||
and running, and then configure its listening socket in `/etc/postfix/main.cf`
|
||||
as follows:
|
||||
|
||||
```
|
||||
smtpd_milters = inet:localhost:3000
|
||||
|
@ -209,33 +211,33 @@ After reloading the Postfix configuration, mail will be processed by
|
|||
SpamAssassin Milter.
|
||||
|
||||
By default, SpamAssassin Milter will accept, that is, won’t check messages
|
||||
coming from local connections (for example, mail sent locally from the
|
||||
command-line), and messages from authenticated senders (for example, mail
|
||||
coming from local connections (for example, mail sent from the command-line with
|
||||
`sendmail`), and messages from authenticated senders (for example, mail
|
||||
submitted via a SASL-authenticated channel).
|
||||
|
||||
## Integration with mail delivery
|
||||
|
||||
A further component that can be useful with SpamAssassin Milter is a
|
||||
[Sieve]-capable mail delivery agent. A Sieve script can for example look at the
|
||||
`X-Spam-` SpamAssassin headers of the incoming message, and take action based on
|
||||
those.
|
||||
[Sieve]-capable mail delivery service. A Sieve script can for example look at
|
||||
the `X-Spam-` SpamAssassin headers of the incoming message, and take action
|
||||
based on those.
|
||||
|
||||
As an example, in case [Dovecot] does mail delivery with [LMTP], enable the
|
||||
As an example, in case [Dovecot] does mail delivery using [LMTP], enable the
|
||||
Sieve plugin for the LMTP protocol, then set up a global Sieve script that files
|
||||
messages flagged as spam into the ‘Junk’ folder:
|
||||
|
||||
```
|
||||
require "fileinto";
|
||||
|
||||
if header :contains "X-Spam-Flag" "YES" {
|
||||
if header "X-Spam-Flag" "YES" {
|
||||
fileinto "Junk";
|
||||
stop;
|
||||
}
|
||||
```
|
||||
|
||||
[Dovecot]: https://dovecot.org
|
||||
[LMTP]: https://doc.dovecot.org/configuration_manual/protocols/lmtp_server/
|
||||
[Sieve]: http://sieve.info
|
||||
[Dovecot]: https://dovecot.org
|
||||
[LMTP]: https://doc.dovecot.org/configuration_manual/protocols/lmtp_server
|
||||
|
||||
## Licence
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@ struct Connection {
|
|||
}
|
||||
|
||||
impl Connection {
|
||||
fn new(ip: IpAddr) -> Self {
|
||||
fn new(client_ip: IpAddr) -> Self {
|
||||
Self {
|
||||
client_ip: ip,
|
||||
client_ip,
|
||||
helo_host: None,
|
||||
client: None,
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ fn handle_connect(
|
|||
) -> milter::Result<Status> {
|
||||
let ip = socket_addr.map_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), |a| a.ip());
|
||||
|
||||
if config::get().has_trusted_networks() {
|
||||
if config::get().use_trusted_networks() {
|
||||
if config::get().is_in_trusted_networks(&ip) {
|
||||
verbose!("accepted connection from trusted network address {}", ip);
|
||||
return Ok(Status::Accept);
|
||||
|
|
|
@ -2,9 +2,9 @@ use std::mem;
|
|||
|
||||
// Design note: Header handling requires ASCII-case-insensitive map keys. This
|
||||
// complexity could have been pushed out to the map key type (a newtype
|
||||
// implementing `Hash` and `Eq`) while using an ordinary `HashMap`. We’ve found
|
||||
// it simpler to extract the complexity into these custom collections instead,
|
||||
// and use plain strings in the application logic.
|
||||
// implementing `Hash` and `Eq` while using an ordinary `HashMap`), but we’ve
|
||||
// found it simpler to introduce these custom collections instead, and use plain
|
||||
// strings in the application logic.
|
||||
|
||||
/// A vector map with ASCII-case-insensitive `AsRef<str>` keys.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{collections::HashSet, net::IpAddr};
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigBuilder {
|
||||
milter_debug_level: i32,
|
||||
has_trusted_networks: bool,
|
||||
use_trusted_networks: bool,
|
||||
trusted_networks: HashSet<IpNet>,
|
||||
auth_untrusted: bool,
|
||||
spamc_args: Vec<String>,
|
||||
|
@ -24,14 +24,14 @@ impl ConfigBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn has_trusted_networks(&mut self, value: bool) -> &mut Self {
|
||||
self.has_trusted_networks = value;
|
||||
pub fn use_trusted_networks(&mut self, value: bool) -> &mut Self {
|
||||
self.use_trusted_networks = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_trusted_network(&mut self, value: IpNet) -> &mut Self {
|
||||
self.has_trusted_networks = true;
|
||||
self.trusted_networks.insert(value);
|
||||
pub fn trusted_network(&mut self, net: IpNet) -> &mut Self {
|
||||
self.use_trusted_networks = true;
|
||||
self.trusted_networks.insert(net);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -40,8 +40,12 @@ impl ConfigBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn spamc_args(&mut self, value: Vec<String>) -> &mut Self {
|
||||
self.spamc_args = value;
|
||||
pub fn spamc_args<I, S>(&mut self, args: I) -> &mut Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.spamc_args.extend(args.into_iter().map(|a| a.as_ref().to_owned()));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -76,9 +80,14 @@ impl ConfigBuilder {
|
|||
}
|
||||
|
||||
pub fn build(self) -> Config {
|
||||
assert!(
|
||||
self.use_trusted_networks || self.trusted_networks.is_empty(),
|
||||
"trusted networks present but not used"
|
||||
);
|
||||
|
||||
Config {
|
||||
milter_debug_level: self.milter_debug_level,
|
||||
has_trusted_networks: self.has_trusted_networks,
|
||||
use_trusted_networks: self.use_trusted_networks,
|
||||
trusted_networks: self.trusted_networks,
|
||||
auth_untrusted: self.auth_untrusted,
|
||||
spamc_args: self.spamc_args,
|
||||
|
@ -96,7 +105,7 @@ impl Default for ConfigBuilder {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
milter_debug_level: Default::default(),
|
||||
has_trusted_networks: Default::default(),
|
||||
use_trusted_networks: Default::default(),
|
||||
trusted_networks: Default::default(),
|
||||
auth_untrusted: Default::default(),
|
||||
spamc_args: Default::default(),
|
||||
|
@ -114,7 +123,7 @@ impl Default for ConfigBuilder {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
milter_debug_level: i32,
|
||||
has_trusted_networks: bool,
|
||||
use_trusted_networks: bool,
|
||||
trusted_networks: HashSet<IpNet>,
|
||||
auth_untrusted: bool,
|
||||
spamc_args: Vec<String>,
|
||||
|
@ -135,8 +144,8 @@ impl Config {
|
|||
self.milter_debug_level
|
||||
}
|
||||
|
||||
pub fn has_trusted_networks(&self) -> bool {
|
||||
self.has_trusted_networks
|
||||
pub fn use_trusted_networks(&self) -> bool {
|
||||
self.use_trusted_networks
|
||||
}
|
||||
|
||||
pub fn is_in_trusted_networks(&self, ip: &IpAddr) -> bool {
|
||||
|
@ -199,11 +208,24 @@ mod tests {
|
|||
#[test]
|
||||
fn trusted_networks_config() {
|
||||
let mut builder = Config::builder();
|
||||
builder.add_trusted_network("127.0.0.1/8".parse().unwrap());
|
||||
builder.trusted_network("127.0.0.1/8".parse().unwrap());
|
||||
let config = builder.build();
|
||||
|
||||
assert!(config.has_trusted_networks());
|
||||
assert!(config.use_trusted_networks());
|
||||
assert!(config.is_in_trusted_networks(&"127.0.0.1".parse().unwrap()));
|
||||
assert!(!config.is_in_trusted_networks(&"10.1.0.1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spamc_args_extends_args() {
|
||||
let mut builder = Config::builder();
|
||||
builder.spamc_args(&["-p", "3030"]);
|
||||
builder.spamc_args(&["-x"]);
|
||||
let config = builder.build();
|
||||
|
||||
assert_eq!(
|
||||
config.spamc_args(),
|
||||
&[String::from("-p"), String::from("3030"), String::from("-x")],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ fn header_lines(header: &[u8]) -> Vec<&[u8]> {
|
|||
let mut start = i;
|
||||
|
||||
while i < header.len() {
|
||||
// Assume line breaks are always encoded as b"\r\n".
|
||||
// Assume line endings are always encoded as b"\r\n".
|
||||
if header[i] == b'\r' && i + 1 < header.len() && header[i + 1] == b'\n' {
|
||||
if i + 2 < header.len() && (header[i + 2] == b' ' || header[i + 2] == b'\t') {
|
||||
i += 3;
|
||||
|
|
|
@ -110,13 +110,13 @@ fn build_config(matches: &ArgMatches<'_>) -> Result<Config> {
|
|||
}
|
||||
|
||||
if let Some(nets) = matches.values_of(ARG_TRUSTED_NETWORKS) {
|
||||
config.has_trusted_networks(true);
|
||||
config.use_trusted_networks(true);
|
||||
|
||||
for net in nets.filter(|n| !n.trim().is_empty()) {
|
||||
// Both `ipnet::IpNet` and `std::net::IpAddr` are supported.
|
||||
// Both `ipnet::IpNet` and `std::net::IpAddr` inputs are supported.
|
||||
match net.parse().or_else(|_| net.parse::<IpAddr>().map(From::from)) {
|
||||
Ok(net) => {
|
||||
config.add_trusted_network(net);
|
||||
config.trusted_network(net);
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(Error::with_description(
|
||||
|
@ -152,7 +152,7 @@ fn build_config(matches: &ArgMatches<'_>) -> Result<Config> {
|
|||
}
|
||||
|
||||
if let Some(spamc_args) = matches.values_of(ARG_SPAMC_ARGS) {
|
||||
config.spamc_args(spamc_args.map(|a| a.to_owned()).collect());
|
||||
config.spamc_args(spamc_args);
|
||||
};
|
||||
|
||||
Ok(config.build())
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use std::{
|
||||
ffi::OsString,
|
||||
io::{Read, Write},
|
||||
io::{ErrorKind, Read, Write},
|
||||
net::{Ipv4Addr, Shutdown, SocketAddrV4, TcpListener},
|
||||
path::PathBuf,
|
||||
process::{Command, ExitStatus},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub const SPAMD_PORT: u16 = 3783; // mock port
|
||||
|
||||
pub type HamOrSpam = Result<String, String>;
|
||||
|
||||
/// A mock `spamd` server that echoes what it is sent, after applying
|
||||
/// A mock `spamd` server that echoes what it is sent after applying
|
||||
/// transformation `f` to the message content and mock-classifying it as ham or
|
||||
/// spam.
|
||||
pub fn spawn_mock_spamd_server<F>(port: u16, f: F) -> JoinHandle<()>
|
||||
|
@ -20,33 +20,44 @@ where
|
|||
F: Fn(String) -> HamOrSpam + Send + 'static,
|
||||
{
|
||||
let socket_addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port);
|
||||
let timeout = Duration::from_secs(15);
|
||||
|
||||
thread::spawn(move || {
|
||||
let listener = TcpListener::bind(socket_addr).unwrap();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
|
||||
// This server handles only a single connection, so that we can `join`
|
||||
// this thread in the tests and detect panics. If the connection never
|
||||
// comes but `miltertest` still succeeds, this will cause the test to
|
||||
// hang.
|
||||
let now = Instant::now();
|
||||
|
||||
// This server expects and handles only a single connection, so that we
|
||||
// can `join` this thread in the tests and detect panics. A panic can be
|
||||
// triggered both in the handling code as well as due to the timeout.
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((mut stream, _)) => {
|
||||
let mut buf = Vec::new();
|
||||
stream.read_to_end(&mut buf).unwrap();
|
||||
stream.write_all(process_message(buf, &f).as_bytes()).unwrap();
|
||||
stream.shutdown(Shutdown::Write).unwrap();
|
||||
break;
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
if now.elapsed() > timeout {
|
||||
panic!("mock spamd server timed out waiting for a connection");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("mock spamd server could not open connection: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The client/server protocol is here ‘reverse-engineered’ in a very rudimentary
|
||||
// fashion, but hopefully sufficient for testing purposes. Both client and
|
||||
// server send a protocol header containing a content length indication and
|
||||
// terminated with "\r\n\r\n". The payload is the email message (with CRLF line
|
||||
// endings).
|
||||
// The SpamAssassin client/server protocol is here reverse-engineered in a very
|
||||
// rudimentary fashion: Both client and server send a protocol header containing
|
||||
// a content length indication and terminated with "\r\n\r\n". The payload is
|
||||
// the email message itself with CRLF line endings.
|
||||
|
||||
const SPAMD_PROTOCOL_OK: &str = "SPAMD/1.1 0 EX_OK";
|
||||
|
||||
|
@ -54,16 +65,17 @@ fn process_message<F>(buf: Vec<u8>, f: &F) -> String
|
|||
where
|
||||
F: Fn(String) -> HamOrSpam,
|
||||
{
|
||||
let mut s = String::from_utf8(buf).unwrap();
|
||||
let mut msg = String::from_utf8(buf).unwrap();
|
||||
|
||||
// Crude handling of the spamc client protocol: strip off everything before
|
||||
// and including the first "\r\n\r\n".
|
||||
let i = s.find("\r\n\r\n").expect("spamc protocol header missing");
|
||||
s.replace_range(..i + 4, "");
|
||||
// Crude handling of the `spamc` client protocol: strip off everything
|
||||
// before and including the first "\r\n\r\n".
|
||||
let i = msg.find("\r\n\r\n").expect("spamc protocol header missing");
|
||||
msg.replace_range(..i + 4, "");
|
||||
|
||||
match f(s) {
|
||||
// Again very basic handling of the spamd server protocol: add a forged
|
||||
// protocol header terminated with "\r\n\r\n". (Currently not used.)
|
||||
match f(msg) {
|
||||
// Again very basic handling of the `spamd` server protocol: add a
|
||||
// forged protocol header terminated with "\r\n\r\n". (This is currently
|
||||
// not used in tests.)
|
||||
Ok(ham) => format!(
|
||||
"{}\r\nContent-length: {}\r\nSpam: False ; 4.0 / 5.0\r\n\r\n{}",
|
||||
SPAMD_PROTOCOL_OK,
|
||||
|
@ -82,8 +94,10 @@ where
|
|||
const MILTERTEST: &str = "miltertest";
|
||||
|
||||
pub fn spawn_miltertest_runner(test_file_name: &str) -> JoinHandle<ExitStatus> {
|
||||
// This thread is just for safety, in case the miltertest runner thread
|
||||
// below never manages to shut down the milter.
|
||||
let _timeout_thread = thread::spawn(|| {
|
||||
thread::sleep(Duration::from_secs(15));
|
||||
thread::sleep(Duration::from_secs(20));
|
||||
|
||||
eprintln!("miltertest runner timed out");
|
||||
|
||||
|
@ -93,6 +107,7 @@ pub fn spawn_miltertest_runner(test_file_name: &str) -> JoinHandle<ExitStatus> {
|
|||
let file_name = to_miltertest_file_name(test_file_name);
|
||||
|
||||
thread::spawn(move || {
|
||||
// Wait just a little to give the milter time to start up.
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
let output = Command::new(MILTERTEST)
|
||||
|
|
|
@ -47,6 +47,7 @@ assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
|||
local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
-- Incoming foreign SpamAssassin headers, to be replaced or deleted.
|
||||
local err = mt.header(conn, "X-Spam-Checker-Version", "BogusChecker 1.0.0")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
|
|
@ -16,19 +16,14 @@ fn ham_flow() {
|
|||
"X-Spam-Checker-Version: MyChecker 1.0.0\r\n",
|
||||
1,
|
||||
)
|
||||
.replacen(
|
||||
"X-Spam-Report: Bogus report\r\n",
|
||||
"",
|
||||
1,
|
||||
)
|
||||
.replacen("X-Spam-Report: Bogus report\r\n", "", 1)
|
||||
.replacen(
|
||||
"\r\n\r\n",
|
||||
"\r\n\
|
||||
X-Spam-Custom: Custom-Value\r\n\
|
||||
\r\n",
|
||||
1,
|
||||
)
|
||||
)
|
||||
))
|
||||
});
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
-- Live test against an actual SpamAssassin server.
|
||||
-- Live test against a real SpamAssassin server.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
@ -48,7 +48,7 @@ local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
|||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- TODO Add headers here to experiment
|
||||
-- Add headers here to experiment.
|
||||
|
||||
local err = mt.eoh(conn)
|
||||
assert(err == nil, err)
|
||||
|
|
|
@ -56,7 +56,7 @@ local err = mt.bodystring(conn, "Test message body")
|
|||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- A `miltertest` (or milter protocol?) pitfall. Even though we return the
|
||||
-- A `miltertest` (or milter protocol?) pitfall: Even though we return the
|
||||
-- `SMFIR_REJECT` status in the application code, because we use a custom error
|
||||
-- reply, we must check for `SMFIR_REPLYCODE` instead.
|
||||
local err = mt.eom(conn)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
-- Transfer message body chunks until more than some max message size bytes
|
||||
-- have been written, then skip the rest of the message (oversized message
|
||||
-- won’t be processed by SpamAssassin anyway, so it is futile to send the whole
|
||||
-- message).
|
||||
-- Message body chunks are written to `spamc` until the maximum message size is
|
||||
-- reached, the rest is skipped (oversized messages are not processed by
|
||||
-- SpamAssassin, so it is futile to send the whole message in this case).
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
|
|
@ -47,6 +47,7 @@ assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
|||
local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
-- Incoming foreign SpamAssassin headers, to be replaced or deleted.
|
||||
local err = mt.header(conn, "X-Spam-Checker-Version", "BogusChecker 1.0.0")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
|
|
@ -17,11 +17,7 @@ fn spam_flow() {
|
|||
"X-Spam-Checker-Version: MyChecker 1.0.0\r\n",
|
||||
1,
|
||||
)
|
||||
.replacen(
|
||||
"X-Spam-Report: Bogus report\r\n",
|
||||
"",
|
||||
1,
|
||||
)
|
||||
.replacen("X-Spam-Report: Bogus report\r\n", "", 1)
|
||||
.replacen(
|
||||
"\r\n\r\n",
|
||||
"\r\n\
|
||||
|
@ -32,10 +28,10 @@ fn spam_flow() {
|
|||
1,
|
||||
);
|
||||
|
||||
// Replace the message body with the report …
|
||||
// Replace the message body with the SpamAssassin ‘report’.
|
||||
spam.replace_range(
|
||||
(spam.find("\r\n\r\n").unwrap() + 4)..,
|
||||
"Spam detection software has identified ..."
|
||||
"Spam detection software has identified ...",
|
||||
);
|
||||
|
||||
Err(spam)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
-- When no spamd server is available, spamc fails to connect.
|
||||
-- When no `spamd` server is available, `spamc` fails to connect.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
@ -28,8 +28,8 @@ local err = mt.macro(conn, SMFIC_DATA,
|
|||
"v", "Postfix 3.3.0")
|
||||
assert(err == nil, err)
|
||||
|
||||
-- If spamc cannot connect to spamd, it will retry several times. That is why
|
||||
-- this test can proceed all the way to the `eom` stage – where the failure
|
||||
-- When `spamc` cannot connect to `spamd`, it will retry several times. That is
|
||||
-- why this test can proceed all the way to the `eom` stage where the failure
|
||||
-- finally surfaces.
|
||||
|
||||
local err = mt.data(conn)
|
||||
|
|
|
@ -8,7 +8,6 @@ fn spamc_connection_error() {
|
|||
let mut builder = Config::builder();
|
||||
// `spamc` always ‘works’ even if it cannot actually reach `spamd`!
|
||||
// `--no-safe-fallback` prevents this masking of connection errors.
|
||||
// TODO Also try with non-existing flag, behaviour is different?
|
||||
builder.spamc_args(vec![
|
||||
String::from("--no-safe-fallback"),
|
||||
format!("--port={}", SPAMD_PORT),
|
||||
|
|
|
@ -6,7 +6,7 @@ use spamassassin_milter::*;
|
|||
#[test]
|
||||
fn trusted_network_connection() {
|
||||
let mut builder = Config::builder();
|
||||
builder.add_trusted_network("123.120.0.0/14".parse().unwrap());
|
||||
builder.trusted_network("123.120.0.0/14".parse().unwrap());
|
||||
let config = builder.build();
|
||||
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
|
Loading…
Reference in a new issue