Files
selfpost/internal/postfix/queue_test.go
T
mixandClaude Opus 5.5 b113e72c71 milter, postfix, app, domain: cite the source of what they rely on (assumptions part B)
Comments only, no behaviour changed. go-milter v0.4.1 (session.go: one
session per connection, replaced after a non-continue reply; macros per
stage), Postfix 3.7.11 (milter8.c, smtpd_check.c
reject_auth_sender_login_mismatch(), mail_addr_find.c, showq_compat.c,
mail_conf_time.c, mail_params.h), Cyrus SASL 2.1.28 (saslpasswd.c,
plugins/sasldb.c: deleting a missing login is not an error), OpenDKIM
2.11.0~beta2 (opendkim-db.c, opendkim-genkey), Berkeley DB 5.3 db_dump,
rspamd 3.11.0. Comments that claimed what the source denies are corrected
(sasldb keeps the password as given, not hashed; an embedded newline is
kept, not truncated; rspamd sorts symbols by absolute weight); fixtures say
seen or composed. The sender check's From: parsing gets no citations: it is
replaced by the port of OpenDKIM's dkim_mail_parse next.

Contradictions left for their own commits: B-G5 cleanAddress, B-PS1 first
key wins, B-PQ1 the forced-expire flag, B-PQ10/PQ11 Postfix's time units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:36:12 +03:00

148 lines
5.2 KiB
Go

package postfix
import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"
)
// TestFakePostqueue is not a test: it is the fake `postqueue -p` the tests
// below run in place of the real one, writing SP_FAKE_POSTQUEUE_BYTES bytes of
// queue entries composed by hand after the layout of Postfix 3.7.11's
// src/postqueue/showq_compat.c.
func TestFakePostqueue(t *testing.T) {
if os.Getenv("SP_FAKE_POSTQUEUE") != "1" {
t.Skip("helper process")
}
var size int
fmt.Sscan(os.Getenv("SP_FAKE_POSTQUEUE_BYTES"), &size)
out := bufio.NewWriter(os.Stdout)
fmt.Fprintln(out, "-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------")
for n, i := 0, 0; n < size; i++ {
entry := fmt.Sprintf("%010X 446 Fri Aug 8 10:13:31 app@example.com\n%41s rcpt@example.net\n\n", i, "")
out.WriteString(entry)
n += len(entry)
}
out.Flush()
os.Exit(0)
}
// fakePostqueue puts a postqueue that lists about size bytes of entries in
// place of the real one for the rest of the test.
func fakePostqueue(t *testing.T, size int) {
t.Helper()
old := postqueueP
postqueueP = func(ctx context.Context) *exec.Cmd {
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestFakePostqueue$")
cmd.Env = append(os.Environ(), "SP_FAKE_POSTQUEUE=1", fmt.Sprintf("SP_FAKE_POSTQUEUE_BYTES=%d", size))
return cmd
}
t.Cleanup(func() { postqueueP = old })
}
// A listing over the cap is no listing for the reconcile sweep: the messages
// past the cut are still queued, and taking the first 4 MiB of ids for the
// whole queue closed every row past it as bounced (audit 2026-10-12, H3).
// The queue page still shows the cut listing, with the note.
func TestQueueIDsRefusesATruncatedListing(t *testing.T) {
fakePostqueue(t, 5<<20)
if ids, err := QueueIDs(); err == nil || !strings.Contains(err.Error(), "listing is larger than") {
t.Fatalf("QueueIDs on a 5 MiB listing = %d ids, %v; want the listing refused for its size", len(ids), err)
}
text, err := Queue()
if err != nil {
t.Fatalf("Queue on a 5 MiB listing: %v", err)
}
if !strings.HasSuffix(text, fmt.Sprintf("[listing truncated at %d bytes]\n", maxQueueOutput)) {
t.Fatalf("Queue on a 5 MiB listing does not end with the truncation note: ...%q", text[len(text)-80:])
}
}
func TestQueueIDsReadsAListingUnderTheCap(t *testing.T) {
fakePostqueue(t, 1000)
ids, err := QueueIDs()
if err != nil {
t.Fatal(err)
}
if _, ok := ids["0000000000"]; !ok || len(ids) < 5 {
t.Fatalf("QueueIDs on a small listing = %v", ids)
}
}
// The reconcile sweep decides whether a message is still Postfix's problem, so
// the parser must pick queue ids out of a listing and nothing else out of it:
// not the header, not the byte-count trailer, and above all not a deferred
// entry's reason line. The listing is composed by hand. Postfix 3.7.11
// (src/postqueue/showq_compat.c, showq_message()) right-aligns a reason to
// column 76, so it reaches the left margin only when it is 76 characters or
// longer; the one here is put at the margin as the hard case.
func TestParseQueueIDs(t *testing.T) {
listing := `-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
3C5B04E6C1* 446 Fri Aug 8 10:12:31 app@example.com
rcpt@example.net
5B4A2C1D3E 446 Fri Aug 8 10:13:31 app@example.com
(connect to mx.example.net[203.0.113.9]:25: Connection timed out)
deferred@example.net
A1B2C3D4E5F! 891 Fri Aug 8 10:14:31 app@example.com
held@example.net
-- 1 Kbytes in 3 Requests.
`
ids := parseQueueIDs(listing)
want := []string{"3C5B04E6C1", "5B4A2C1D3E", "A1B2C3D4E5F"}
for _, id := range want {
if _, ok := ids[id]; !ok {
t.Errorf("queue id %s not found in %v", id, ids)
}
}
if len(ids) != len(want) {
t.Errorf("got %d ids %v, want exactly %v", len(ids), ids, want)
}
}
// An empty queue must come back as an empty set, not as a phantom id parsed out
// of Postfix's prose — every stale row would otherwise be compared against a
// listing that claims to hold a message called "Mail".
func TestParseQueueIDsOnAnEmptyQueue(t *testing.T) {
if ids := parseQueueIDs("Mail queue is empty\n"); len(ids) != 0 {
t.Errorf("got %v, want no ids", ids)
}
}
// The queue listing is held in memory and rendered into a page, so a very large
// queue must not size the panel's allocation: the writer keeps the cap and
// reports the rest as dropped, while still accepting every write so postqueue
// itself is never cut short.
func TestLimitWriterCapsOutput(t *testing.T) {
w := &limitWriter{remaining: 8}
n, err := w.Write([]byte("12345"))
if n != 5 || err != nil {
t.Fatalf("first write = %d, %v", n, err)
}
if w.truncated {
t.Fatal("truncated before the cap was reached")
}
n, err = w.Write([]byte("67890"))
if n != 5 || err != nil {
t.Fatalf("second write = %d, %v", n, err)
}
if !w.truncated {
t.Fatal("write past the cap was not recorded as truncated")
}
if got := w.buf.String(); got != "12345678" {
t.Fatalf("kept %q, want the first 8 bytes", got)
}
if n, err := w.Write([]byte("more")); n != 4 || err != nil {
t.Fatalf("write after the cap = %d, %v (must still accept)", n, err)
}
if got := w.buf.String(); got != "12345678" {
t.Fatalf("buffer grew past the cap: %q", got)
}
}