From dd9df65fe603dff5151cfbe1547780435862bf7f Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 29 Jan 2026 07:52:11 +0000 Subject: [PATCH] http injection: fix auth_info regression for deferred_generation This commit fixes a regression introduced as part of the updated AAA/ACL code. Since that code has not shipped in a stable release, this is not being entered into the changelog. The issue was that when processing the deferred payloads, we were trying to parse the `auth_info` key from the metadata as though it were a json-serialized-string, but it was in fact already parsed and was actually a json object. This resulted in a runtime error that caused the message to be logged as a Bounce like: ``` expected 'auth_info' to be a string value, got Ok(Object (..)) ``` This commit adds an explicit integration test for deferred generation so that we can catch this sort of thing more easily moving forwards, as well as resolves this issue. --- .../src/test/http_inject_deferred.rs | 95 +++++++++++++++++++ crates/integration-tests/src/test/mod.rs | 1 + crates/kumod/src/http_server/inject_v1.rs | 10 +- 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 crates/integration-tests/src/test/http_inject_deferred.rs diff --git a/crates/integration-tests/src/test/http_inject_deferred.rs b/crates/integration-tests/src/test/http_inject_deferred.rs new file mode 100644 index 00000000..006b6fad --- /dev/null +++ b/crates/integration-tests/src/test/http_inject_deferred.rs @@ -0,0 +1,95 @@ +use crate::kumod::DaemonWithMaildir; +use anyhow::Context; +use k9::assert_equal; +use std::time::Duration; + +/// Test HTTP injection with gzip compressed request body +#[tokio::test] +async fn http_inject_deferred() -> anyhow::Result<()> { + let mut daemon = DaemonWithMaildir::start() + .await + .context("DaemonWithMaildir::start")?; + + let payload = serde_json::json!({ + "envelope_sender": "sender@example.com", + "recipients": [ + { + "email": "user@example.com", + "name": "Test User" + } + ], + "deferred_generation": true, + "content": { + "text_body": "Hello {{ name }}! This is a message.", + "subject": "Deferred Generation Test" + } + }); + + let json_data = serde_json::to_vec(&payload)?; + + let client = reqwest::Client::new(); + let response = client + .post(&format!( + "http://{}/api/inject/v1", + daemon.source.listener("http") + )) + .header("Content-Type", "application/json") + .body(json_data) + .send() + .await?; + + anyhow::ensure!( + response.status() == 200, + "Response status: {}", + response.status() + ); + let response_json: serde_json::Value = response.json().await?; + eprintln!("response: {response_json:?}"); + assert_equal!( + response_json["success_count"], + 0, + "deferred always shows zero" + ); + assert_equal!(response_json["fail_count"], 0); + + daemon + .wait_for_maildir_count(1, Duration::from_secs(10)) + .await; + + daemon.stop_both().await.context("stop_both")?; + println!("Stopped!"); + + let delivery_summary = daemon.dump_logs().await.context("dump_logs")?; + k9::snapshot!( + delivery_summary, + " +DeliverySummary { + source_counts: { + Reception: 2, + Delivery: 2, + }, + sink_counts: { + Reception: 1, + Delivery: 1, + }, +} +" + ); + + daemon.assert_no_acct_deny().await?; + let mut messages = daemon.extract_maildir_messages()?; + assert_equal!(messages.len(), 1); + let parsed = messages[0].parsed()?; + + // Verify the message content was properly expanded + let body = parsed.body().unwrap(); + match body { + mailparsing::DecodedBody::Text(text) => { + assert!(text.contains("Hello Test User!")); + assert!(text.contains("This is a message")); + } + _ => panic!("Expected text body"), + } + + Ok(()) +} diff --git a/crates/integration-tests/src/test/mod.rs b/crates/integration-tests/src/test/mod.rs index d8dc7217..574a2b66 100644 --- a/crates/integration-tests/src/test/mod.rs +++ b/crates/integration-tests/src/test/mod.rs @@ -15,6 +15,7 @@ mod end_to_end_webhook_batch; mod expires; mod http_auth; mod http_inject_compression; +mod http_inject_deferred; mod http_inject_size_limit; mod log_oob_arf; mod maildir_batch; diff --git a/crates/kumod/src/http_server/inject_v1.rs b/crates/kumod/src/http_server/inject_v1.rs index c17dc572..5c8aa460 100644 --- a/crates/kumod/src/http_server/inject_v1.rs +++ b/crates/kumod/src/http_server/inject_v1.rs @@ -979,9 +979,10 @@ impl HttpInjectionGeneratorDispatcher { .ok_or_else(|| anyhow::anyhow!("received_from metadata is missing!?"))? .parse()?; - let auth_info: AuthInfo = match msg.get_meta_string("auth_info").await? { - Some(info) => serde_json::from_str(&info)?, - None => { + let auth_info: AuthInfo = match msg.get_meta("auth_info").await? { + obj @ serde_json::Value::Object(_) => serde_json::from_value(obj) + .context("auth_info should be the json repr of AuthInfo")?, + serde_json::Value::Null => { // We might not have auth_info serialized in the metadata // if we are upgrading from a prior version that did not // support AuthInfo, so default it to something approximating @@ -990,6 +991,9 @@ impl HttpInjectionGeneratorDispatcher { auth_info.set_peer_address(Some(peer_address)); auth_info } + other => { + anyhow::bail!("expected optional `auth_info` object, but got {other:?}") + } }; let via_address = match msg.get_meta_string("received_via").await {