Replace miltertest tests with indymilter-test

This commit is contained in:
David Bürgin 2023-01-18 10:23:21 +01:00
parent 1823f6a178
commit b6b5d47b2a
24 changed files with 579 additions and 595 deletions

View file

@ -1,7 +1,7 @@
image: rust
before_script:
- apt-get --assume-yes update
- apt-get --assume-yes install spamc miltertest
- apt-get --assume-yes install spamc
test:
script:
- cargo test --verbose

95
Cargo.lock generated
View file

@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.60"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
dependencies = [
"proc-macro2",
"quote",
@ -111,9 +111,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cxx"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579"
dependencies = [
"cc",
"cxxbridge-flags",
@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70"
dependencies = [
"cc",
"codespan-reporting",
@ -138,15 +138,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c"
[[package]]
name = "cxxbridge-macro"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5"
dependencies = [
"proc-macro2",
"quote",
@ -242,6 +242,17 @@ dependencies = [
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
@ -277,9 +288,9 @@ dependencies = [
[[package]]
name = "indymilter"
version = "0.1.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9bde68534c1fb1c2d822f9e9de77afd47a9b587140ee60fe357e168d7742f6"
checksum = "23b2d41c863440ee78353a6607ff5bac3f5aa24c7c2ba735464e82a10fd4ec9e"
dependencies = [
"async-trait",
"bitflags",
@ -289,10 +300,21 @@ dependencies = [
]
[[package]]
name = "ipnet"
version = "2.7.0"
name = "indymilter-test"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
checksum = "dca0420749776b6105ad01a0f5cef9993798473e8b28f97dcbbe6bc4908ffff5"
dependencies = [
"bytes",
"indymilter",
"tokio",
]
[[package]]
name = "ipnet"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]]
name = "js-sys"
@ -392,6 +414,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.49"
@ -410,6 +438,36 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "scratch"
version = "1.0.3"
@ -472,11 +530,14 @@ version = "0.3.2"
dependencies = [
"async-trait",
"byte-strings",
"bytes",
"chrono",
"futures",
"indymilter",
"indymilter-test",
"ipnet",
"once_cell",
"rand",
"signal-hook",
"signal-hook-tokio",
"tokio",
@ -515,9 +576,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.23.0"
version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
dependencies = [
"autocfg",
"bytes",

View file

@ -11,16 +11,19 @@ repository = "https://gitlab.com/glts/spamassassin-milter"
exclude = ["/.gitignore", "/.gitlab-ci.yml"]
[dependencies]
async-trait = "0.1.60"
async-trait = "0.1.61"
byte-strings = "0.2.2"
bytes = "1.3.0"
chrono = "0.4.23"
futures = "0.3.25"
indymilter = "0.1.1"
ipnet = "2.7.0"
indymilter = "0.2.0"
ipnet = "2.7.1"
once_cell = "1.17.0"
signal-hook = "0.3.14"
signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] }
tokio = { version = "1.23.0", features = ["fs", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "sync"] }
tokio = { version = "1.24.1", features = ["fs", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "sync"] }
[dev-dependencies]
tokio = { version = "1.23.0", features = ["signal", "time"] }
indymilter-test = "0.0.3"
rand = "0.8.5"
tokio = { version = "1.24.1", features = ["signal", "time"] }

View file

@ -54,17 +54,6 @@ The minimum supported Rust version is 1.61.0.
[Rust]: https://www.rust-lang.org
### Building
The prerequisites mentioned above apply to building and testing the package,
too.
Additionally, the integration tests rely on the third-party `miltertest`
utility. Make sure `miltertest` is available and can be executed when running
the integration tests. (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`.)
## Usage
Once installed, SpamAssassin Milter can be invoked as `spamassassin-milter`.

View file

@ -19,10 +19,11 @@ use crate::{
config::Config,
};
use byte_strings::c_str;
use bytes::Bytes;
use chrono::Local;
use indymilter::{
Actions, Callbacks, Context, EomContext, Macros, NegotiateContext, ProtoOpts, SocketInfo,
Stage, Status,
Actions, Callbacks, Context, EomContext, MacroStage, Macros, NegotiateContext, ProtoOpts,
SocketInfo, Status,
};
use std::{
borrow::Cow,
@ -129,10 +130,11 @@ async fn handle_negotiate(
}
}
context.requested_opts |= ProtoOpts::SKIP | ProtoOpts::HEADER_LEADING_SPACE;
context.requested_opts |= ProtoOpts::SKIP | ProtoOpts::LEADING_SPACE;
context.requested_macros.insert(Stage::Mail, c_str!("{auth_authen}").into());
context.requested_macros.insert(Stage::Data, c_str!("i j _ {tls_version} v").into());
let macros = &mut context.requested_macros;
macros.insert(MacroStage::Mail, c_str!("{auth_authen}").into());
macros.insert(MacroStage::Data, c_str!("i j _ {tls_version} v").into());
Status::Continue
}
@ -264,7 +266,7 @@ async fn handle_eoh(context: &mut Context<Connection>) -> Status {
async fn handle_body(
config: Arc<Config>,
context: &mut Context<Connection>,
chunk: Vec<u8>,
chunk: Bytes,
) -> Status {
let conn = context.data.connection();
let client = conn.client.as_mut().unwrap();

View file

@ -1,23 +0,0 @@
-- An authenticated sender is accepted, the message is not processed.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.helo(conn, "mail.gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
-- `{auth_authen}` holds the SASL login name, if any.
local err = mt.macro(conn, SMFIC_MAIL, "{auth_authen}", "from@gluet.ch")
assert(err == nil, err)
local err = mt.mailfrom(conn, "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_ACCEPT)
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -2,15 +2,33 @@ mod common;
pub use common::*;
use indymilter::MacroStage;
use indymilter_test::*;
/// An authenticated sender is accepted, the message is not processed.
#[tokio::test]
async fn authenticated_sender() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, Default::default())
.await
.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.helo("mail.gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
// `{auth_authen}` holds the SASL login name, if any.
conn.macros(MacroStage::Mail, [("{auth_authen}", "from@gluet.ch")])
.await
.unwrap();
let status = conn.mail(["<from@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Accept);
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
assert!(exit_code.success());
}

View file

@ -1,16 +1,14 @@
use chrono::Local;
use rand::Rng;
use spamassassin_milter::{Config, ConfigBuilder};
use std::{
ffi::OsString,
io::{self, ErrorKind},
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
process::ExitStatus,
time::Duration,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, ToSocketAddrs},
process::Command,
sync::oneshot,
task::JoinHandle,
time,
@ -99,6 +97,15 @@ where
}
}
pub fn rand_msg_id() -> String {
let n = rand::thread_rng().gen_range(1..=999999);
format!("<{n:06}@gluet.ch>")
}
pub fn current_date() -> String {
Local::now().to_rfc2822()
}
pub struct SpamAssassinMilter {
milter_handle: JoinHandle<io::Result<()>>,
shutdown: oneshot::Sender<()>,
@ -132,26 +139,3 @@ impl SpamAssassinMilter {
self.milter_handle.await?
}
}
const MILTERTEST_PROGRAM: &str = "/usr/bin/miltertest";
pub async fn run_miltertest(test_file_name: &str, addr: SocketAddr) -> io::Result<ExitStatus> {
let file_name = to_miltertest_file_name(test_file_name);
let port = addr.port();
let mut miltertest = Command::new(MILTERTEST_PROGRAM)
// .arg("-vvv")
.arg("-D")
.arg(format!("port={}", port))
.arg("-s")
.arg(&file_name)
.spawn()?;
miltertest.wait().await
}
fn to_miltertest_file_name(file_name: &str) -> OsString {
let mut path = PathBuf::from(file_name);
path.set_extension("lua");
path.into_os_string()
}

View file

@ -1,76 +0,0 @@
-- Happy path processing of an ordinary ham (not spam) message.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.helo(conn, "mail.gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.mailfrom(conn, "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.rcptto(conn, "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
local err = mt.macro(conn, SMFIC_DATA,
"i", "1234567ABC",
"j", "localhost",
"_", "client.gluet.ch [123.123.123.123]",
"{tls_version}", "TLSv1.2",
"v", "Postfix 3.3.0")
assert(err == nil, err)
local err = mt.data(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "From", "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "To", "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Subject", "Test message")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
assert(err == nil, err)
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)
local err = mt.header(conn, "X-Spam-Report", "Bogus report")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.eoh(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.bodystring(conn, "Hello, we would like to invite you to ...")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.eom(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
assert(mt.eom_check(conn, MT_HDRINSERT, "X-Spam-Custom", " Custom-Value"))
assert(mt.eom_check(conn, MT_HDRDELETE, "X-Spam-Checker-Version"))
assert(mt.eom_check(conn, MT_HDRINSERT, "X-Spam-Checker-Version", " MyChecker 1.0.0"))
assert(mt.eom_check(conn, MT_HDRDELETE, "X-Spam-Report"))
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -1,8 +1,12 @@
mod common;
pub use common::*;
use indymilter::MacroStage;
use indymilter_test::*;
use spamassassin_milter::*;
/// Happy path processing of an ordinary ham (not spam) message.
#[tokio::test]
async fn ham_message() {
let config = configure_spamc(Config::builder())
@ -28,10 +32,75 @@ async fn ham_message() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.helo("mail.gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.mail(["<from@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.rcpt(["<to@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
conn.macros(
MacroStage::Data,
[
("i", "1234567ABC"),
("j", "localhost"),
("_", "client.gluet.ch [123.123.123.123]"),
("{tls_version}", "TLSv1.2"),
("v", "Postfix 3.3.0"),
],
)
.await
.unwrap();
let status = conn.data().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("From", "from@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("To", "to@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Subject", "Test message").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Message-ID", rand_msg_id()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Date", current_date()).await.unwrap();
assert_eq!(status, Status::Continue);
// Incoming foreign SpamAssassin headers, to be replaced or deleted.
let status = conn.header("X-Spam-Checker-Version", "BogusChecker 1.0.0").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("X-Spam-Report", "Bogus report").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.eoh().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.body(&b"Hello, we would like to invite you to ..."[..]).await.unwrap();
assert_eq!(status, Status::Continue);
let (actions, status) = conn.eom().await.unwrap();
assert_eq!(status, Status::Continue);
assert!(actions.has_insert_header(0, "X-Spam-Custom", " Custom-Value"));
assert!(actions.has_delete_header("X-Spam-Checker-Version", any()));
assert!(actions.has_insert_header(0, "X-Spam-Checker-Version", " MyChecker 1.0.0"));
assert!(actions.has_delete_header("X-Spam-Report", any()));
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
server.await.unwrap().unwrap();
assert!(exit_code.success());
server.await.unwrap().unwrap();
}

View file

@ -1,71 +0,0 @@
-- Live test against a real SpamAssassin server.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.helo(conn, "mail.gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.mailfrom(conn, "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.rcptto(conn, "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
local err = mt.macro(conn, SMFIC_DATA,
"i", "1234567ABC",
"j", "localhost",
"_", "client.gluet.ch [123.123.123.123]",
"{tls_version}", "TLSv1.2",
"v", "Postfix 3.3.0")
assert(err == nil, err)
local err = mt.data(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "From", "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "To", "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Subject", "Test message")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
assert(err == nil, err)
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)
-- Add headers here to experiment.
local err = mt.eoh(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
-- This is the magic GTUBE value, which makes this message certain spam.
-- See https://spamassassin.apache.org/gtube/.
local err = mt.bodystring(conn, "XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
-- TODO `miltertest` bug: Replacing the message body can crash `miltertest`. An
-- internal buffer size (1024) easily overflows when using SpamAssassin reports.
local err = mt.eom(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -1,13 +1,16 @@
mod common;
pub use common::*;
use indymilter::MacroStage;
use indymilter_test::*;
use spamassassin_milter::*;
/// Runs a live test against a real SpamAssassin server instance. This test is
/// run on demand, as SpamAssassin will actually analyse the input, and do DNS
/// queries etc.
#[ignore]
#[tokio::test]
#[ignore = "runs live test against SpamAssassin server"]
async fn live() {
// When no port is specified, `spamc` will try to connect to the default
// `spamd` port 783 (see also `/etc/services`).
@ -15,9 +18,69 @@ async fn live() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.helo("mail.gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.mail(["<from@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.rcpt(["<to@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
conn.macros(
MacroStage::Data,
[
("i", "1234567ABC"),
("j", "localhost"),
("_", "client.gluet.ch [123.123.123.123]"),
("{tls_version}", "TLSv1.2"),
("v", "Postfix 3.3.0"),
],
)
.await
.unwrap();
let status = conn.data().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("From", "from@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("To", "to@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Subject", "Test message").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Message-ID", rand_msg_id()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Date", current_date()).await.unwrap();
assert_eq!(status, Status::Continue);
// Add headers here to experiment.
let status = conn.eoh().await.unwrap();
assert_eq!(status, Status::Continue);
// This is the magic GTUBE value, which makes this message certain spam.
// See https://spamassassin.apache.org/gtube/.
let status = conn.body(
&b"XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X"[..],
)
.await
.unwrap();
assert_eq!(status, Status::Continue);
let (_, status) = conn.eom().await.unwrap();
assert_eq!(status, Status::Continue);
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
assert!(exit_code.success());
}

View file

@ -1,24 +0,0 @@
-- 1) A connection from the loopback IP address is accepted.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, nil, "127.0.0.1")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_ACCEPT)
local err = mt.disconnect(conn)
assert(err == nil, err)
-- 2) A connection from an unknown IP address (for example, from a UNIX
-- domain socket) is also accepted.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, nil, "unspec")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_ACCEPT)
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -2,15 +2,34 @@ mod common;
pub use common::*;
use indymilter::SocketInfo;
use indymilter_test::*;
use std::net::Ipv4Addr;
#[tokio::test]
async fn loopback_connection() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, Default::default())
.await
.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
// 1) A connection from the loopback IP address is accepted.
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("localhost", Ipv4Addr::LOCALHOST).await.unwrap();
assert_eq!(status, Status::Accept);
conn.close().await.unwrap();
// 2) A connection from an unknown IP address (for example, from a UNIX
// domain socket) is also accepted.
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("localhost", SocketInfo::Unknown).await.unwrap();
assert_eq!(status, Status::Accept);
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
assert!(exit_code.success());
}

View file

@ -1,69 +0,0 @@
-- A spam message is rejected with an SMTP error reply.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.helo(conn, "mail.gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.mailfrom(conn, "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.rcptto(conn, "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
local err = mt.macro(conn, SMFIC_DATA,
"i", "1234567ABC",
"j", "localhost",
"_", "client.gluet.ch [123.123.123.123]",
"{tls_version}", "TLSv1.2",
"v", "Postfix 3.3.0")
assert(err == nil, err)
local err = mt.data(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "From", "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "To", "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Subject", "Test message")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
assert(err == nil, err)
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)
local err = mt.eoh(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
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
-- `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)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_REPLYCODE)
assert(mt.eom_check(conn, MT_SMTPREPLY, "554", "5.7.1", "Not allowed!"))
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -1,8 +1,13 @@
mod common;
pub use common::*;
use byte_strings::c_str;
use indymilter::MacroStage;
use indymilter_test::*;
use spamassassin_milter::*;
/// A spam message is rejected with an SMTP error reply.
#[tokio::test]
async fn reject_spam() {
let config = configure_spamc(Config::builder())
@ -20,10 +25,68 @@ async fn reject_spam() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.helo("mail.gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.mail(["<from@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.rcpt(["<to@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
conn.macros(
MacroStage::Data,
[
("i", "1234567ABC"),
("j", "localhost"),
("_", "client.gluet.ch [123.123.123.123]"),
("{tls_version}", "TLSv1.2"),
("v", "Postfix 3.3.0"),
],
)
.await
.unwrap();
let status = conn.data().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("From", "from@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("To", "to@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Subject", "Test message").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Message-ID", rand_msg_id()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Date", current_date()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.eoh().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.body(&b"Test message body"[..]).await.unwrap();
assert_eq!(status, Status::Continue);
let (_, status) = conn.eom().await.unwrap();
assert_eq!(
status,
Status::Reject {
message: Some(c_str!("554 5.7.1 Not allowed!").into()),
}
);
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
server.await.unwrap().unwrap();
assert!(exit_code.success());
server.await.unwrap().unwrap();
}

View file

@ -1,78 +0,0 @@
-- 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).
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.helo(conn, "mail.gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.mailfrom(conn, "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.rcptto(conn, "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
local err = mt.macro(conn, SMFIC_DATA,
"i", "1234567ABC",
"j", "localhost",
"_", "client.gluet.ch [123.123.123.123]",
"{tls_version}", "TLSv1.2",
"v", "Postfix 3.3.0")
assert(err == nil, err)
local err = mt.data(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "From", "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "To", "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Subject", "Test message")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
assert(err == nil, err)
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)
local err = mt.eoh(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
-- At this point still below the size limit …
local err = mt.bodystring(conn, "Test message body")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
-- … after sending the following, were past the limit and skip.
local err = mt.bodystring(conn, [[
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
]])
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_SKIP)
local err = mt.eom(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -1,8 +1,14 @@
mod common;
pub use common::*;
use indymilter::MacroStage;
use indymilter_test::*;
use spamassassin_milter::*;
/// 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).
#[tokio::test]
async fn skip_oversized() {
let config = configure_spamc(Config::builder())
@ -14,10 +20,78 @@ async fn skip_oversized() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.helo("mail.gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.mail(["<from@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.rcpt(["<to@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
conn.macros(
MacroStage::Data,
[
("i", "1234567ABC"),
("j", "localhost"),
("_", "client.gluet.ch [123.123.123.123]"),
("{tls_version}", "TLSv1.2"),
("v", "Postfix 3.3.0"),
],
)
.await
.unwrap();
let status = conn.data().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("From", "from@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("To", "to@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Subject", "Test message").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Message-ID", rand_msg_id()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Date", current_date()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.eoh().await.unwrap();
assert_eq!(status, Status::Continue);
// At this point still below the size limit …
let status = conn.body(&b"Test message body"[..]).await.unwrap();
assert_eq!(status, Status::Continue);
// … after sending the following, were past the limit and skip.
let status = conn.body(
&b"\
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................"[..],
)
.await
.unwrap();
assert_eq!(status, Status::Skip);
let (_, status) = conn.eom().await.unwrap();
assert_eq!(status, Status::Continue);
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
server.await.unwrap().unwrap();
assert!(exit_code.success());
server.await.unwrap().unwrap();
}

View file

@ -1,80 +0,0 @@
-- Happy path processing of a spam message.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.helo(conn, "mail.gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.mailfrom(conn, "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.rcptto(conn, "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
local err = mt.macro(conn, SMFIC_DATA,
"i", "1234567ABC",
"j", "localhost",
"_", "client.gluet.ch [123.123.123.123]",
"{tls_version}", "TLSv1.2",
"v", "Postfix 3.3.0")
assert(err == nil, err)
local err = mt.data(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "From", "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "To", "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Subject", "Test message")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
assert(err == nil, err)
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)
local err = mt.header(conn, "X-Spam-Report", "Bogus report")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.eoh(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.bodystring(conn, "HI!!! You have won a BILLION dollars!!!!")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.eom(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
assert(mt.eom_check(conn, MT_HDRDELETE, "X-Spam-Checker-Version"))
assert(mt.eom_check(conn, MT_HDRADD, "X-Spam-Checker-Version", " MyChecker 1.0.0"))
assert(mt.eom_check(conn, MT_HDRADD, "X-Spam-Flag", " YES"))
assert(mt.eom_check(conn, MT_HDRADD, "X-Spam-Custom", " Custom-Value"))
assert(mt.eom_check(conn, MT_HDRDELETE, "X-Spam-Report"))
assert(mt.eom_check(conn, MT_HDRCHANGE, "Subject", " [SPAM] Test message"))
assert(mt.eom_check(conn, MT_HDRADD, "Content-Type", " multipart/mixed; ..."))
assert(mt.eom_check(conn, MT_BODYCHANGE, "Spam detection software has identified ..."))
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -1,8 +1,12 @@
mod common;
pub use common::*;
use indymilter::MacroStage;
use indymilter_test::*;
use spamassassin_milter::*;
/// Happy path processing of a spam message.
#[tokio::test]
async fn spam_message() {
let config = configure_spamc(Config::builder())
@ -42,10 +46,79 @@ async fn spam_message() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.helo("mail.gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.mail(["<from@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.rcpt(["<to@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
conn.macros(
MacroStage::Data,
[
("i", "1234567ABC"),
("j", "localhost"),
("_", "client.gluet.ch [123.123.123.123]"),
("{tls_version}", "TLSv1.2"),
("v", "Postfix 3.3.0"),
],
)
.await
.unwrap();
let status = conn.data().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("From", "from@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("To", "to@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Subject", "Test message").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Message-ID", rand_msg_id()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Date", current_date()).await.unwrap();
assert_eq!(status, Status::Continue);
// Incoming foreign SpamAssassin headers, to be replaced or deleted.
let status = conn.header("X-Spam-Checker-Version", "BogusChecker 1.0.0").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("X-Spam-Report", "Bogus report").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.eoh().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.body(&b"HI!!! You have won a BILLION dollars!!!!"[..]).await.unwrap();
assert_eq!(status, Status::Continue);
let (actions, status) = conn.eom().await.unwrap();
assert_eq!(status, Status::Continue);
assert!(actions.has_delete_header("X-Spam-Checker-Version", any()));
assert!(actions.has_add_header("X-Spam-Checker-Version", " MyChecker 1.0.0"));
assert!(actions.has_add_header("X-Spam-Flag", " YES"));
assert!(actions.has_add_header("X-Spam-Custom", " Custom-Value"));
assert!(actions.has_delete_header("X-Spam-Report", any()));
assert!(actions.has_change_header("Subject", 1, " [SPAM] Test message"));
assert!(actions.has_add_header("Content-Type", " multipart/mixed; ..."));
assert!(actions.has_replaced_body(b"Spam detection software has identified ..."));
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
server.await.unwrap().unwrap();
assert!(exit_code.success());
server.await.unwrap().unwrap();
}

View file

@ -1,68 +0,0 @@
-- When no `spamd` server is available, `spamc` fails to connect.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.helo(conn, "mail.gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.mailfrom(conn, "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.rcptto(conn, "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
local err = mt.macro(conn, SMFIC_DATA,
"i", "1234567ABC",
"j", "localhost",
"_", "client.gluet.ch [123.123.123.123]",
"{tls_version}", "TLSv1.2",
"v", "Postfix 3.3.0")
assert(err == nil, err)
-- 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)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "From", "from@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "To", "to@gluet.ch")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Subject", "Test message")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
assert(err == nil, err)
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)
local err = mt.eoh(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.bodystring(conn, "Test message body")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_CONTINUE)
local err = mt.eom(conn)
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_TEMPFAIL)
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -1,8 +1,12 @@
mod common;
pub use common::*;
use indymilter::MacroStage;
use indymilter_test::*;
use spamassassin_milter::*;
/// When no `spamd` server is available, `spamc` fails to connect.
#[tokio::test]
async fn spamc_connection_error() {
let config = configure_spamc(Config::builder())
@ -11,9 +15,65 @@ async fn spamc_connection_error() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.helo("mail.gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.mail(["<from@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.rcpt(["<to@gluet.ch>"]).await.unwrap();
assert_eq!(status, Status::Continue);
conn.macros(
MacroStage::Data,
[
("i", "1234567ABC"),
("j", "localhost"),
("_", "client.gluet.ch [123.123.123.123]"),
("{tls_version}", "TLSv1.2"),
("v", "Postfix 3.3.0"),
],
)
.await
.unwrap();
// 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.
let status = conn.data().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("From", "from@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("To", "to@gluet.ch").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Subject", "Test message").await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Message-ID", rand_msg_id()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.header("Date", current_date()).await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.eoh().await.unwrap();
assert_eq!(status, Status::Continue);
let status = conn.body(&b"Test message body"[..]).await.unwrap();
assert_eq!(status, Status::Continue);
let (_, status) = conn.eom().await.unwrap();
assert_eq!(status, Status::Tempfail { message: None });
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
assert!(exit_code.success());
}

View file

@ -1,11 +0,0 @@
-- A connection from a trusted network is accepted.
local conn = mt.connect("inet:" .. port .. "@127.0.0.1")
assert(conn, "could not open connection")
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
assert(err == nil, err)
assert(mt.getreply(conn) == SMFIR_ACCEPT)
local err = mt.disconnect(conn)
assert(err == nil, err)

View file

@ -1,8 +1,11 @@
mod common;
pub use common::*;
use indymilter_test::*;
use spamassassin_milter::*;
/// A connection from a trusted network is accepted.
#[tokio::test]
async fn trusted_network_connection() {
let config = Config::builder()
@ -11,9 +14,12 @@ async fn trusted_network_connection() {
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
let exit_code = run_miltertest(file!(), milter.addr()).await.unwrap();
let mut conn = TestConnection::open(milter.addr()).await.unwrap();
let status = conn.connect("client.gluet.ch", [123, 123, 123, 123]).await.unwrap();
assert_eq!(status, Status::Accept);
conn.close().await.unwrap();
milter.shutdown().await.unwrap();
assert!(exit_code.success());
}