From f32992ca36ee38fe03b85a77a79234022d35db27 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 22 Sep 2026 20:22:29 -0700 Subject: [PATCH] Fix the antispam test: pin the rules it scores against, stop live Pyzor It failed everywhere but upstream's machines, for two reasons: - The spam rules, which carry every score, came from a path on an upstream developer's own disk. Without SPAM_RULES_URL none loaded, every score was 0.00 and the combined case came out ham instead of spam at 13.70. The published rules of spam-filter v3.0.2 are now pinned beside the test cases (Apache-2.0 or MIT, taken as MIT; in THIRD-PARTY.md). SPAM_RULES_URL still overrides. - The first combined case expects a Pyzor hit, and its digest (that of an empty body) wasn't among the three the test mode answers, so it went to a public Pyzor server: it failed offline and would drift with that server's counts. Test mode now answers every digest from a fixed table, with the empty body's added, and never reaches the network. The test passes online and offline, alone and with the rest of the SMTP tests. queue_retry, unrelated, still fails when it runs after the others in one process, though it passes alone every time. --- THIRD-PARTY.md | 1 + crates/spam-filter/src/modules/pyzor.rs | 61 ++++++++++-------- .../smtp/antispam/spam-filter-rules.json.gz | Bin 0 -> 21488 bytes tests/src/smtp/inbound/antispam.rs | 13 +++- 4 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 tests/resources/smtp/antispam/spam-filter-rules.json.gz diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md index efc418c..499061a 100644 --- a/THIRD-PARTY.md +++ b/THIRD-PARTY.md @@ -24,6 +24,7 @@ carry their own license files. | `crates/common/src/network/acme/directory.rs`, `crates/common/src/network/acme/jose.rs`, `crates/common/src/network/acme/order.rs` | [rustls-acme](https://github.com/FlorianUekermann/rustls-acme) (MIT or Apache-2.0) | Copyright (c) Florian Uekermann | | `crates/types/src/id.rs` | [crockford](https://github.com/archer884/crockford) (MIT or Apache-2.0) | Copyright (c) 2017 J/A | | `crates/nlp/src/tokenizers/types.rs` | test cases from [linkify](https://github.com/robinst/linkify) (MIT or Apache-2.0) | Copyright (c) 2017 Robin Stocker | +| `tests/resources/smtp/antispam/spam-filter-rules.json.gz` | the published rules of [spam-filter](https://github.com/stalwartlabs/spam-filter) v3.0.2, unmodified, for the spam filter's tests (MIT or Apache-2.0) | Copyright (C) 2024, Stalwart Labs LLC | Each notice above applies with this permission notice: diff --git a/crates/spam-filter/src/modules/pyzor.rs b/crates/spam-filter/src/modules/pyzor.rs index d24f0cd..bffdc6b 100644 --- a/crates/spam-filter/src/modules/pyzor.rs +++ b/crates/spam-filter/src/modules/pyzor.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use common::config::mailstore::spamfilter::PyzorConfig; @@ -43,35 +45,14 @@ pub(crate) async fn pyzor_check( // Hash message let request = message.pyzor_check_message(); + // Send message to address. inbuxa: in tests, a fixed table answers + // instead of a public server (test_response). + #[cfg(not(feature = "test_mode"))] + let response = pyzor_send_message(config.address, config.timeout, &request).await; #[cfg(feature = "test_mode")] - { - if request.contains("b5b476f0b5ba6e1c038361d3ded5818dd39c90a2") { - return Ok(PyzorResponse { - code: 200, - count: 1000, - wl_count: 0, - } - .into()); - } else if request.contains("d67d4b8bfc3860449e3418bb6017e2612f3e2a99") { - return Ok(PyzorResponse { - code: 200, - count: 60, - wl_count: 10, - } - .into()); - } else if request.contains("81763547012b75e57a20d18ce0b93014208cdfdb") { - return Ok(PyzorResponse { - code: 200, - count: 50, - wl_count: 20, - } - .into()); - } - } + let response = std::io::Result::Ok(test_response(&request)); - // Send message to address - pyzor_send_message(config.address, config.timeout, &request) - .await + response .map(Into::into) .map_err(|err| { trc::SpamEvent::PyzorError @@ -82,6 +63,32 @@ pub(crate) async fn pyzor_check( }) } +/// inbuxa: the answers tests get, by digest, instead of a public server's, +/// whose counts change and which a test may not be able to reach. Upstream +/// answered the first three here and sent every other digest to the network. +#[cfg(feature = "test_mode")] +fn test_response(request: &str) -> PyzorResponse { + let (count, wl_count) = if request.contains("b5b476f0b5ba6e1c038361d3ded5818dd39c90a2") + // The digest of an empty body, as an HTML-only message with no text + // to hash produces; public servers report it widely. + || request.contains("da39a3ee5e6b4b0d3255bfef95601890afd80709") + { + (1000, 0) + } else if request.contains("d67d4b8bfc3860449e3418bb6017e2612f3e2a99") { + (60, 10) + } else if request.contains("81763547012b75e57a20d18ce0b93014208cdfdb") { + (50, 20) + } else { + (0, 0) + }; + PyzorResponse { + code: 200, + count, + wl_count, + } +} + +#[cfg_attr(feature = "test_mode", allow(dead_code))] async fn pyzor_send_message( addr: SocketAddr, timeout: Duration, diff --git a/tests/resources/smtp/antispam/spam-filter-rules.json.gz b/tests/resources/smtp/antispam/spam-filter-rules.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..6deb9d2be3bf21945e22c69f7dd16f62a6002708 GIT binary patch literal 21488 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