diff --git a/contrib/testing/queue-lifecycle-check.sh b/contrib/testing/queue-lifecycle-check.sh new file mode 100755 index 0000000..4d74aee --- /dev/null +++ b/contrib/testing/queue-lifecycle-check.sh @@ -0,0 +1,215 @@ +#!/bin/sh +# +# Exercise Queue administration, retry preservation, expiry, and DSN creation +# against a running local Bongo installation. + +set -eu + +user=${1:-stqqueue} +domain=${2:-bongo.test} +queue_tool=${BONGO_QUEUE_TOOL:-/usr/bin/bongo-queuetool} +store_tool=${BONGO_STORE_TOOL:-/usr/bin/bongo-storetool} +service=${BONGO_SERVICE:-bongo.service} +run_as=${BONGO_TEST_RUN_AS:-bongo} +as_root=${BONGO_TEST_AS_ROOT:-sudo -n} + +if [ "$(id -un)" = "$run_as" ]; then + as_bongo= +else + as_bongo="sudo -n -u $run_as" +fi + +work=$(mktemp -d "${TMPDIR:-/tmp}/bongo-stq05.XXXXXX") +chmod 0755 "$work" +managed_id= +retry_id= +expiry_id= + +cleanup() +{ + status=$? + for queue_id in "$managed_id" "$retry_id" "$expiry_id"; do + if [ -n "$queue_id" ]; then + $as_bongo "$queue_tool" delete "$queue_id" \ + >/dev/null 2>&1 || true + fi + done + rm -rf "$work" + exit "$status" +} +trap cleanup EXIT HUP INT TERM + +wait_queue() +{ + attempts=30 + while [ "$attempts" -gt 0 ]; do + if $as_bongo "$queue_tool" list >/dev/null 2>&1; then + return 0 + fi + attempts=$((attempts - 1)) + sleep 1 + done + echo "STQ-05: Queue did not become ready" >&2 + return 1 +} + +queue_contains() +{ + $as_bongo "$queue_tool" list | + grep -F "$1 " >/dev/null +} + +wait_message() +{ + queue_id=$1 + output=$2 + attempts=30 + while [ "$attempts" -gt 0 ]; do + if $as_bongo "$queue_tool" message "$queue_id" \ + >"$output" 2>/dev/null; then + return 0 + fi + attempts=$((attempts - 1)) + sleep 1 + done + echo "STQ-05: queue entry $queue_id did not become readable" >&2 + return 1 +} + +printf '%s\r\n' \ + "From: STQ-05 " \ + "To: $user@$domain" \ + 'Subject: STQ-05 queue lifecycle' \ + 'Date: Thu, 23 Jul 2026 12:34:56 +0200' \ + 'Message-ID: ' \ + '' \ + 'Queue listing, hold, retry, expiry, bounce, and deletion.' \ + >"$work/message.eml" +chmod 0644 "$work/message.eml" + +# A normal committed entry must be listable, movable to hold, preserved across +# restart, releasable to an explicit target, and deletable. +managed_id=$($as_bongo "$queue_tool" hold-local "$work/message.eml" 9) +case "$managed_id" in + 009-*) ;; + *) + echo "STQ-05: unexpected management fixture ID: $managed_id" >&2 + exit 1 + ;; +esac +queue_contains "$managed_id" + +held_id=$($as_bongo "$queue_tool" hold "$managed_id") +managed_id=$held_id +case "$managed_id" in + 999-*) ;; + *) + echo "STQ-05: hold did not return a queue-999 ID: $managed_id" >&2 + exit 1 + ;; +esac +queue_contains "$managed_id" + +$as_root /usr/bin/systemctl restart "$service" +wait_queue +queue_contains "$managed_id" + +released_id=$($as_bongo "$queue_tool" release "$managed_id" 9) +managed_id=$released_id +case "$managed_id" in + 009-*) ;; + *) + echo "STQ-05: release did not return a queue-9 ID: $managed_id" >&2 + exit 1 + ;; +esac +queue_contains "$managed_id" +$as_bongo "$queue_tool" delete "$managed_id" +if queue_contains "$managed_id"; then + echo "STQ-05: deleted entry remains in the listing" >&2 + exit 1 +fi +managed_id= + +# A failed remote attempt must remain queued without changing the message. +retry_id=$($as_bongo "$queue_tool" enqueue-remote \ + "$work/message.eml" "$user@$domain" nobody@retry.invalid 7) +case "$retry_id" in + 007-*) ;; + *) + echo "STQ-05: unexpected retry fixture ID: $retry_id" >&2 + exit 1 + ;; +esac +wait_message "$retry_id" "$work/retry-before.eml" +retry_before=$(sha256sum "$work/retry-before.eml" | awk '{ print $1 }') +$as_bongo "$queue_tool" flush +wait_message "$retry_id" "$work/retry-after.eml" +retry_after=$(sha256sum "$work/retry-after.eml" | awk '{ print $1 }') +if [ "$retry_before" != "$retry_after" ]; then + echo "STQ-05: retry changed the queued message" >&2 + exit 1 +fi +$as_bongo "$queue_tool" delete "$retry_id" +retry_id= + +# Deterministic expiry starts in hold so the normal remote worker cannot race +# the diagnostic command. Exactly one DSN must appear in the isolated inbox. +$as_bongo "$store_tool" -u "$user" \ + document-list /mail/INBOX >"$work/inbox-before" +expiry_id=$($as_bongo "$queue_tool" enqueue-remote \ + "$work/message.eml" "$user@$domain" nobody@retry.invalid 999) +case "$expiry_id" in + 999-*) ;; + *) + echo "STQ-05: unexpected expiry fixture ID: $expiry_id" >&2 + exit 1 + ;; +esac +$as_bongo "$queue_tool" expire "$expiry_id" >/dev/null +expiry_id= + +attempts=30 +while [ "$attempts" -gt 0 ]; do + $as_bongo "$store_tool" -u "$user" \ + document-list /mail/INBOX >"$work/inbox-after" + awk '{ print $1 }' "$work/inbox-before" | sort -u \ + >"$work/inbox-before.ids" + awk '{ print $1 }' "$work/inbox-after" | sort -u \ + >"$work/inbox-after.ids" + comm -13 "$work/inbox-before.ids" "$work/inbox-after.ids" \ + >"$work/new.ids" + if [ "$(wc -l <"$work/new.ids")" -eq 1 ]; then + break + fi + attempts=$((attempts - 1)) + sleep 1 +done + +if [ "$(wc -l <"$work/new.ids")" -ne 1 ]; then + echo "STQ-05: expiry did not create exactly one DSN" >&2 + diff -u "$work/inbox-before" "$work/inbox-after" >&2 || true + exit 1 +fi + +document=$(sed -n '1p' "$work/new.ids") +document_path=$(awk -v id="$document" '$1 == id { print $3 }' \ + "$work/inbox-after") +$as_bongo "$store_tool" -u "$user" \ + document-get "$document_path" - >"$work/dsn.eml" + +for expected in \ + 'Subject: Returned mail: Delivery time exceeded' \ + 'Original-recipient: rfc822;nobody@retry.invalid' \ + 'Final-recipient: rfc822;nobody@retry.invalid' \ + 'Action: failed' \ + 'Status: 5.4.7' +do + if ! grep -F "$expected" "$work/dsn.eml" >/dev/null; then + echo "STQ-05: generated DSN lacks '$expected'" >&2 + exit 1 + fi +done + +printf 'STQ-05 PASS management=hold/restart/release/delete retry-sha256=%s dsn=%s status=5.4.7\n' \ + "$retry_after" "$document" diff --git a/docs/release-testing-0.7.md b/docs/release-testing-0.7.md index ca195ac..b2afd87 100644 --- a/docs/release-testing-0.7.md +++ b/docs/release-testing-0.7.md @@ -110,7 +110,7 @@ inputs and outputs is absent from the ZIP. | STQ-02 | Local message survives Queue-to-Store delivery byte-for-byte | [PASS](test-evidence/0.7-r1.md#stq-02) | | | | STQ-03 | Queue free-space calculation is correct on 32/64-bit and reserves space | [PASS](test-evidence/0.7-r1.md#stq-03) | | | | STQ-04 | Full-disk and unreadable-spool failures defer safely without mail loss | [PASS](test-evidence/0.7-r1.md#stq-04) | | | -| STQ-05 | Retry, expiry, bounce, listing, hold/release, and deletion | | | | +| STQ-05 | Retry, expiry, bounce, listing, hold/release, and deletion | [PASS](test-evidence/0.7-r1.md#stq-05) | | | | STQ-06 | Concurrent delivery/read/move/delete/restart preserves integrity | | | | | STQ-07 | Backup/export and restore reproduce mail, metadata, contacts, calendars | | | | | STQ-08 | Invalid commands, oversized lines, and unauthorised access fail safely | | | | diff --git a/docs/test-evidence/0.7-r1.md b/docs/test-evidence/0.7-r1.md index d231e1f..2767428 100644 --- a/docs/test-evidence/0.7-r1.md +++ b/docs/test-evidence/0.7-r1.md @@ -1523,3 +1523,59 @@ Bongo finished active/running as manager PID 1225547 with systemd result `success`. The temporary fixture, fill allocation, and failed submission were removed; no Store object, external mail, SMTP relay, collector source, or LMTP endpoint was used. + +## STQ-05 + +Result: **PASS** + +Commit `efc5173d` added deterministic Queue listing, hold/release controls, +the reserved Queue 999 class, and recovery of held entries across manager +restarts. It also fixed `QMOVE` to use its parsed destination. Commit +`69ab7094` added complete remote-delivery fixtures and deterministic expiry +diagnostics. Commit `1441ab1a` fixed the expired-recipient envelope boundary +so the generated delivery-status recipient and enhanced status code are +parsed correctly instead of falling back to a generic internal error. Unit +tests cover the Queue protocol clients, movement, listing, recovery, expiry, +and exact bounce-recipient formatting. The complete rebuilt suite passed 86 +of 86 tests in 11.26 seconds. + +The commits were pushed to `master`, installed as +`mail-mta/bongo-9999::bongo` with GCC 15.3.0, and exercised on 2026-07-23: + +```sh +./contrib/testing/queue-lifecycle-check.sh +``` + +The repeatable live test first listed a Queue 009 entry, moved it to hold, +restarted Bongo, and proved that Queue 999 recovery retained it. It released +the message to Queue 009, listed it there, deleted it, and confirmed that it +was gone. A remote-delivery fixture for `nobody@retry.invalid` remained in +Queue 007 after a flush with identical message bytes: + +```text +retry SHA-256 39b785579f9bdc5914216aa84f83cd19ca6014eef95d49e388b1d52f209f26a6 +``` + +A separate held remote fixture was then expired without racing the normal +remote worker. It left the Queue and created exactly one new Store object, +`000000000000001b`, in the isolated `stqqueue` inbox. The multipart delivery +status notification contained: + +```text +Subject: Returned mail: Delivery time exceeded +Original-recipient: rfc822;nobody@retry.invalid +Final-recipient: rfc822;nobody@retry.invalid +Action: failed +Status: 5.4.7 +``` + +The script reported: + +```text +STQ-05 PASS management=hold/restart/release/delete retry-sha256=39b785579f9bdc5914216aa84f83cd19ca6014eef95d49e388b1d52f209f26a6 dsn=000000000000001b status=5.4.7 +``` + +All temporary Queue entries were removed. The intentionally retained DSN is +local release evidence; no external mail, SMTP relay, collector source, or +LMTP endpoint was used. Bongo finished active/running as manager PID 1253396 +with systemd result `success`.