Clean up config builder, add timeout to mock server

This commit is contained in:
David Bürgin 2020-02-22 08:18:53 +01:00
parent 02b7e2ad64
commit 4525df41cb
18 changed files with 146 additions and 116 deletions

12
Cargo.lock generated
View file

@ -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",

View file

@ -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 MTAs SMTP protocol
SpamAssassin Milter operates as a [milter] hooked into the MTAs 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, wont 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

View file

@ -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);

View file

@ -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`. Weve 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 weve
// 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)]

View file

@ -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")],
);
}
}

View file

@ -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;

View file

@ -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())

View file

@ -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)

View file

@ -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)

View file

@ -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!());

View 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)

View file

@ -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)

View file

@ -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
-- wont 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")

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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),

View file

@ -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!());