diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4035c30..20f29a1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 498344d..7a35831 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 87d6617..b2e3230 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md index b779ca7..2bcd0e6 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/src/callbacks.rs b/src/callbacks.rs index 5930308..411be3e 100644 --- a/src/callbacks.rs +++ b/src/callbacks.rs @@ -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) -> Status { async fn handle_body( config: Arc, context: &mut Context, - chunk: Vec, + chunk: Bytes, ) -> Status { let conn = context.data.connection(); let client = conn.client.as_mut().unwrap(); diff --git a/tests/authenticated_sender.lua b/tests/authenticated_sender.lua deleted file mode 100644 index 7382982..0000000 --- a/tests/authenticated_sender.lua +++ /dev/null @@ -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) diff --git a/tests/authenticated_sender.rs b/tests/authenticated_sender.rs index 5fb78bd..105d9fa 100644 --- a/tests/authenticated_sender.rs +++ b/tests/authenticated_sender.rs @@ -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([""]).await.unwrap(); + assert_eq!(status, Status::Accept); + + conn.close().await.unwrap(); milter.shutdown().await.unwrap(); - - assert!(exit_code.success()); } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b245a02..e2ca2f3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -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>, 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 { - 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() -} diff --git a/tests/ham_message.lua b/tests/ham_message.lua deleted file mode 100644 index a54a5a0..0000000 --- a/tests/ham_message.lua +++ /dev/null @@ -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) diff --git a/tests/ham_message.rs b/tests/ham_message.rs index 17e1ded..44f7fa6 100644 --- a/tests/ham_message.rs +++ b/tests/ham_message.rs @@ -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([""]).await.unwrap(); + assert_eq!(status, Status::Continue); + + let status = conn.rcpt([""]).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(); } diff --git a/tests/live.lua b/tests/live.lua deleted file mode 100644 index f18a905..0000000 --- a/tests/live.lua +++ /dev/null @@ -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) diff --git a/tests/live.rs b/tests/live.rs index 70fb40a..a4b25dc 100644 --- a/tests/live.rs +++ b/tests/live.rs @@ -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([""]).await.unwrap(); + assert_eq!(status, Status::Continue); + + let status = conn.rcpt([""]).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()); } diff --git a/tests/loopback_connection.lua b/tests/loopback_connection.lua deleted file mode 100644 index 60bcf46..0000000 --- a/tests/loopback_connection.lua +++ /dev/null @@ -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) diff --git a/tests/loopback_connection.rs b/tests/loopback_connection.rs index 66367f9..f227bbf 100644 --- a/tests/loopback_connection.rs +++ b/tests/loopback_connection.rs @@ -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()); } diff --git a/tests/reject_spam.lua b/tests/reject_spam.lua deleted file mode 100644 index 607f4ea..0000000 --- a/tests/reject_spam.lua +++ /dev/null @@ -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) diff --git a/tests/reject_spam.rs b/tests/reject_spam.rs index 36e1d1e..e6f59d6 100644 --- a/tests/reject_spam.rs +++ b/tests/reject_spam.rs @@ -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([""]).await.unwrap(); + assert_eq!(status, Status::Continue); + + let status = conn.rcpt([""]).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(); } diff --git a/tests/skip_oversized.lua b/tests/skip_oversized.lua deleted file mode 100644 index dbadd68..0000000 --- a/tests/skip_oversized.lua +++ /dev/null @@ -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, we’re 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) diff --git a/tests/skip_oversized.rs b/tests/skip_oversized.rs index 7b6ba32..445b9c3 100644 --- a/tests/skip_oversized.rs +++ b/tests/skip_oversized.rs @@ -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([""]).await.unwrap(); + assert_eq!(status, Status::Continue); + + let status = conn.rcpt([""]).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, we’re 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(); } diff --git a/tests/spam_message.lua b/tests/spam_message.lua deleted file mode 100644 index ac3b7c7..0000000 --- a/tests/spam_message.lua +++ /dev/null @@ -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) diff --git a/tests/spam_message.rs b/tests/spam_message.rs index d147328..f60d045 100644 --- a/tests/spam_message.rs +++ b/tests/spam_message.rs @@ -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([""]).await.unwrap(); + assert_eq!(status, Status::Continue); + + let status = conn.rcpt([""]).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(); } diff --git a/tests/spamc_connection_error.lua b/tests/spamc_connection_error.lua deleted file mode 100644 index ed89c43..0000000 --- a/tests/spamc_connection_error.lua +++ /dev/null @@ -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) diff --git a/tests/spamc_connection_error.rs b/tests/spamc_connection_error.rs index 0de2406..492d22b 100644 --- a/tests/spamc_connection_error.rs +++ b/tests/spamc_connection_error.rs @@ -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([""]).await.unwrap(); + assert_eq!(status, Status::Continue); + + let status = conn.rcpt([""]).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()); } diff --git a/tests/trusted_network_connection.lua b/tests/trusted_network_connection.lua deleted file mode 100644 index 515bf0b..0000000 --- a/tests/trusted_network_connection.lua +++ /dev/null @@ -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) diff --git a/tests/trusted_network_connection.rs b/tests/trusted_network_connection.rs index c1e575d..4acb31a 100644 --- a/tests/trusted_network_connection.rs +++ b/tests/trusted_network_connection.rs @@ -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()); }