Replace miltertest tests with indymilter-test
This commit is contained in:
parent
1823f6a178
commit
b6b5d47b2a
24 changed files with 579 additions and 595 deletions
|
@ -1,7 +1,7 @@
|
||||||
image: rust
|
image: rust
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get --assume-yes update
|
- apt-get --assume-yes update
|
||||||
- apt-get --assume-yes install spamc miltertest
|
- apt-get --assume-yes install spamc
|
||||||
test:
|
test:
|
||||||
script:
|
script:
|
||||||
- cargo test --verbose
|
- cargo test --verbose
|
||||||
|
|
95
Cargo.lock
generated
95
Cargo.lock
generated
|
@ -13,9 +13,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.60"
|
version = "0.1.61"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
|
checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -111,9 +111,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxx"
|
name = "cxx"
|
||||||
version = "1.0.85"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
|
checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cxxbridge-flags",
|
"cxxbridge-flags",
|
||||||
|
@ -123,9 +123,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxx-build"
|
name = "cxx-build"
|
||||||
version = "1.0.85"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
|
checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
|
@ -138,15 +138,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxxbridge-flags"
|
name = "cxxbridge-flags"
|
||||||
version = "1.0.85"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
|
checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cxxbridge-macro"
|
name = "cxxbridge-macro"
|
||||||
version = "1.0.85"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
|
checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -242,6 +242,17 @@ dependencies = [
|
||||||
"slab",
|
"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]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
@ -277,9 +288,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indymilter"
|
name = "indymilter"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c9bde68534c1fb1c2d822f9e9de77afd47a9b587140ee60fe357e168d7742f6"
|
checksum = "23b2d41c863440ee78353a6607ff5bac3f5aa24c7c2ba735464e82a10fd4ec9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
@ -289,10 +300,21 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "indymilter-test"
|
||||||
version = "2.7.0"
|
version = "0.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
|
@ -392,6 +414,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.49"
|
version = "1.0.49"
|
||||||
|
@ -410,6 +438,36 @@ dependencies = [
|
||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "scratch"
|
name = "scratch"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
@ -472,11 +530,14 @@ version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"byte-strings",
|
"byte-strings",
|
||||||
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"indymilter",
|
"indymilter",
|
||||||
|
"indymilter-test",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rand",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-tokio",
|
"signal-hook-tokio",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -515,9 +576,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.23.0"
|
version = "1.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
|
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -11,16 +11,19 @@ repository = "https://gitlab.com/glts/spamassassin-milter"
|
||||||
exclude = ["/.gitignore", "/.gitlab-ci.yml"]
|
exclude = ["/.gitignore", "/.gitlab-ci.yml"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.60"
|
async-trait = "0.1.61"
|
||||||
byte-strings = "0.2.2"
|
byte-strings = "0.2.2"
|
||||||
|
bytes = "1.3.0"
|
||||||
chrono = "0.4.23"
|
chrono = "0.4.23"
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
indymilter = "0.1.1"
|
indymilter = "0.2.0"
|
||||||
ipnet = "2.7.0"
|
ipnet = "2.7.1"
|
||||||
once_cell = "1.17.0"
|
once_cell = "1.17.0"
|
||||||
signal-hook = "0.3.14"
|
signal-hook = "0.3.14"
|
||||||
signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] }
|
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]
|
[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"] }
|
||||||
|
|
11
README.md
11
README.md
|
@ -54,17 +54,6 @@ The minimum supported Rust version is 1.61.0.
|
||||||
|
|
||||||
[Rust]: https://www.rust-lang.org
|
[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
|
## Usage
|
||||||
|
|
||||||
Once installed, SpamAssassin Milter can be invoked as `spamassassin-milter`.
|
Once installed, SpamAssassin Milter can be invoked as `spamassassin-milter`.
|
||||||
|
|
|
@ -19,10 +19,11 @@ use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
};
|
};
|
||||||
use byte_strings::c_str;
|
use byte_strings::c_str;
|
||||||
|
use bytes::Bytes;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use indymilter::{
|
use indymilter::{
|
||||||
Actions, Callbacks, Context, EomContext, Macros, NegotiateContext, ProtoOpts, SocketInfo,
|
Actions, Callbacks, Context, EomContext, MacroStage, Macros, NegotiateContext, ProtoOpts,
|
||||||
Stage, Status,
|
SocketInfo, Status,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
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());
|
let macros = &mut context.requested_macros;
|
||||||
context.requested_macros.insert(Stage::Data, c_str!("i j _ {tls_version} v").into());
|
macros.insert(MacroStage::Mail, c_str!("{auth_authen}").into());
|
||||||
|
macros.insert(MacroStage::Data, c_str!("i j _ {tls_version} v").into());
|
||||||
|
|
||||||
Status::Continue
|
Status::Continue
|
||||||
}
|
}
|
||||||
|
@ -264,7 +266,7 @@ async fn handle_eoh(context: &mut Context<Connection>) -> Status {
|
||||||
async fn handle_body(
|
async fn handle_body(
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
context: &mut Context<Connection>,
|
context: &mut Context<Connection>,
|
||||||
chunk: Vec<u8>,
|
chunk: Bytes,
|
||||||
) -> Status {
|
) -> Status {
|
||||||
let conn = context.data.connection();
|
let conn = context.data.connection();
|
||||||
let client = conn.client.as_mut().unwrap();
|
let client = conn.client.as_mut().unwrap();
|
||||||
|
|
|
@ -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)
|
|
|
@ -2,15 +2,33 @@ mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter::MacroStage;
|
||||||
|
use indymilter_test::*;
|
||||||
|
|
||||||
|
/// An authenticated sender is accepted, the message is not processed.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn authenticated_sender() {
|
async fn authenticated_sender() {
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, Default::default())
|
let milter = SpamAssassinMilter::spawn(LOCALHOST, Default::default())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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();
|
milter.shutdown().await.unwrap();
|
||||||
|
|
||||||
assert!(exit_code.success());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
|
use chrono::Local;
|
||||||
|
use rand::Rng;
|
||||||
use spamassassin_milter::{Config, ConfigBuilder};
|
use spamassassin_milter::{Config, ConfigBuilder};
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsString,
|
|
||||||
io::{self, ErrorKind},
|
io::{self, ErrorKind},
|
||||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||||
path::PathBuf,
|
|
||||||
process::ExitStatus,
|
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
net::{TcpListener, ToSocketAddrs},
|
net::{TcpListener, ToSocketAddrs},
|
||||||
process::Command,
|
|
||||||
sync::oneshot,
|
sync::oneshot,
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
time,
|
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 {
|
pub struct SpamAssassinMilter {
|
||||||
milter_handle: JoinHandle<io::Result<()>>,
|
milter_handle: JoinHandle<io::Result<()>>,
|
||||||
shutdown: oneshot::Sender<()>,
|
shutdown: oneshot::Sender<()>,
|
||||||
|
@ -132,26 +139,3 @@ impl SpamAssassinMilter {
|
||||||
self.milter_handle.await?
|
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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -1,8 +1,12 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter::MacroStage;
|
||||||
|
use indymilter_test::*;
|
||||||
use spamassassin_milter::*;
|
use spamassassin_milter::*;
|
||||||
|
|
||||||
|
/// ‘Happy path’ processing of an ordinary ham (not spam) message.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ham_message() {
|
async fn ham_message() {
|
||||||
let config = configure_spamc(Config::builder())
|
let config = configure_spamc(Config::builder())
|
||||||
|
@ -28,10 +32,75 @@ async fn ham_message() {
|
||||||
|
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
|
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();
|
milter.shutdown().await.unwrap();
|
||||||
server.await.unwrap().unwrap();
|
|
||||||
|
|
||||||
assert!(exit_code.success());
|
server.await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -1,13 +1,16 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter::MacroStage;
|
||||||
|
use indymilter_test::*;
|
||||||
use spamassassin_milter::*;
|
use spamassassin_milter::*;
|
||||||
|
|
||||||
/// Runs a ‘live’ test against a real SpamAssassin server instance. This test is
|
/// 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
|
/// run on demand, as SpamAssassin will actually analyse the input, and do DNS
|
||||||
/// queries etc.
|
/// queries etc.
|
||||||
#[ignore]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "runs live test against SpamAssassin server"]
|
||||||
async fn live() {
|
async fn live() {
|
||||||
// When no port is specified, `spamc` will try to connect to the default
|
// When no port is specified, `spamc` will try to connect to the default
|
||||||
// `spamd` port 783 (see also `/etc/services`).
|
// `spamd` port 783 (see also `/etc/services`).
|
||||||
|
@ -15,9 +18,69 @@ async fn live() {
|
||||||
|
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
|
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();
|
milter.shutdown().await.unwrap();
|
||||||
|
|
||||||
assert!(exit_code.success());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -2,15 +2,34 @@ mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter::SocketInfo;
|
||||||
|
use indymilter_test::*;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn loopback_connection() {
|
async fn loopback_connection() {
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, Default::default())
|
let milter = SpamAssassinMilter::spawn(LOCALHOST, Default::default())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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();
|
milter.shutdown().await.unwrap();
|
||||||
|
|
||||||
assert!(exit_code.success());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -1,8 +1,13 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use byte_strings::c_str;
|
||||||
|
use indymilter::MacroStage;
|
||||||
|
use indymilter_test::*;
|
||||||
use spamassassin_milter::*;
|
use spamassassin_milter::*;
|
||||||
|
|
||||||
|
/// A spam message is rejected with an SMTP error reply.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn reject_spam() {
|
async fn reject_spam() {
|
||||||
let config = configure_spamc(Config::builder())
|
let config = configure_spamc(Config::builder())
|
||||||
|
@ -20,10 +25,68 @@ async fn reject_spam() {
|
||||||
|
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
|
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();
|
milter.shutdown().await.unwrap();
|
||||||
server.await.unwrap().unwrap();
|
|
||||||
|
|
||||||
assert!(exit_code.success());
|
server.await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -1,8 +1,14 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter::MacroStage;
|
||||||
|
use indymilter_test::*;
|
||||||
use spamassassin_milter::*;
|
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]
|
#[tokio::test]
|
||||||
async fn skip_oversized() {
|
async fn skip_oversized() {
|
||||||
let config = configure_spamc(Config::builder())
|
let config = configure_spamc(Config::builder())
|
||||||
|
@ -14,10 +20,78 @@ async fn skip_oversized() {
|
||||||
|
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
|
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, 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();
|
milter.shutdown().await.unwrap();
|
||||||
server.await.unwrap().unwrap();
|
|
||||||
|
|
||||||
assert!(exit_code.success());
|
server.await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -1,8 +1,12 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter::MacroStage;
|
||||||
|
use indymilter_test::*;
|
||||||
use spamassassin_milter::*;
|
use spamassassin_milter::*;
|
||||||
|
|
||||||
|
/// ‘Happy path’ processing of a spam message.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spam_message() {
|
async fn spam_message() {
|
||||||
let config = configure_spamc(Config::builder())
|
let config = configure_spamc(Config::builder())
|
||||||
|
@ -42,10 +46,79 @@ async fn spam_message() {
|
||||||
|
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
|
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();
|
milter.shutdown().await.unwrap();
|
||||||
server.await.unwrap().unwrap();
|
|
||||||
|
|
||||||
assert!(exit_code.success());
|
server.await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -1,8 +1,12 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter::MacroStage;
|
||||||
|
use indymilter_test::*;
|
||||||
use spamassassin_milter::*;
|
use spamassassin_milter::*;
|
||||||
|
|
||||||
|
/// When no `spamd` server is available, `spamc` fails to connect.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spamc_connection_error() {
|
async fn spamc_connection_error() {
|
||||||
let config = configure_spamc(Config::builder())
|
let config = configure_spamc(Config::builder())
|
||||||
|
@ -11,9 +15,65 @@ async fn spamc_connection_error() {
|
||||||
|
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
|
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();
|
milter.shutdown().await.unwrap();
|
||||||
|
|
||||||
assert!(exit_code.success());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -1,8 +1,11 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
use indymilter_test::*;
|
||||||
use spamassassin_milter::*;
|
use spamassassin_milter::*;
|
||||||
|
|
||||||
|
/// A connection from a trusted network is accepted.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn trusted_network_connection() {
|
async fn trusted_network_connection() {
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
|
@ -11,9 +14,12 @@ async fn trusted_network_connection() {
|
||||||
|
|
||||||
let milter = SpamAssassinMilter::spawn(LOCALHOST, config).await.unwrap();
|
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();
|
milter.shutdown().await.unwrap();
|
||||||
|
|
||||||
assert!(exit_code.success());
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue