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 0000000..6deb9d2 Binary files /dev/null and b/tests/resources/smtp/antispam/spam-filter-rules.json.gz differ diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index c8411d8..d2cb876 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -81,9 +81,18 @@ async fn antispam() { admin .registry_create_object(SpamSettings { score_spam: Float::new(5.0), + // inbuxa: the rules carry the scores the expectations are written + // against, so they're pinned (spam-filter v3.0.2, beside the test + // cases) rather than read from a developer's own checkout, which + // left every score at zero. SPAM_RULES_URL still overrides. spam_filter_rules_url: std::env::var("SPAM_RULES_URL") .unwrap_or_else(|_| { - "file:///Users/me/code/spam-filter/spam-filter-rules.json.gz".to_string() + concat!( + "file://", + env!("CARGO_MANIFEST_DIR"), + "/resources/smtp/antispam/spam-filter-rules.json.gz" + ) + .to_string() }) .into(), ..Default::default() @@ -156,7 +165,7 @@ async fn antispam() { })) .await; // inbuxa: a rules file that can't be read is retried later; don't wait - // for that retry (the path above is a developer's own checkout) + // for that retry (SPAM_RULES_URL may name one that isn't there) test.wait_for_tasks_skip_not_due().await; admin.reload_settings().await; admin.reload_lookup_stores().await;