The Delivery log box of every refused row said "This message never reached
the queue, so Postfix wrote no delivery lines for it" - untrue: Postfix
wrote a line for each. The owner, 2026-10-13: the box is required on every
message page. Pictured for him first (dev/shots, the refused variant of
out-message: wrong From with its queue id lines, rate-limited with the
NOQUEUE line); the variant has no mockup screen of its own - the guard ties
screens to pages, and its outline is out-message's.
- Wrong From: cleanup gives the message a queue id before the headers are
judged; the sender check reads {i} at end-of-headers and writes it on the
refused row. Its lines - stored under the id like any other's - are on the
page, but only those from 10 minutes before to 1 minute after the refusal
(store.SendLogLines for a rejected row): Postfix frees the id at once, and
a later message with it lends nothing. No mail.log fallback for a refused
row, for the same reason.
- Rate-limited and wrong client: refused at MAIL FROM, no queue id. Migration
0009 adds send_log.refusal_line; logtail.refusalLines matches smtpd's
"NOQUEUE: milter-reject: MAIL from ...; from=<...>" (not port 25's, not
CONNECT/EHLO) and store.AttachRefusalLine puts it on the newest refused row
of that sender (and login, when the line names it) within two minutes; a
line whose row is not written yet waits up to 30 s (retried at each pass).
- The page: the queue id fact says "given, the message was not queued" or
"none - refused at MAIL FROM, before one is given"; the box's caption is
"The line from mail.log for this refusal" for the latter; a row from
before says the line was not kept. LogLineLevel colours a refusal by its
reply: 5xx red, 4xx amber (IPv6 clients included). The id in the fact is
not muted as in the picture: the kit has no muted inline.
Tests: store (attach by sender, login, time; not a queued row, not one an
hour off; a refused row's lines are its own minutes); logtail (attach, wait
and retry, port 25 and other lines ignored); milter (the refused row carries
{i}); view (facts, caption, line levels); handlers (the line on the page,
the sentence without it). e2e refusals_show_postfixs_line: a client-address
refusal made there, and the pages of a wrong From, a rate-limited and a
wrong client refusal each show Postfix's line.
schema-migrations row 9, CHANGELOG [Unreleased], audit plan line ticked,
roadmap 25/62.
Evidence on the test server for this tree: make e2e exit 0 (the new case
in 5.4 s); go test ./... ok but for two schema tests, fixed after the run
(the schema at 0009) - store tests ok locally; the full run follows before
the push.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.3 KiB
Go
59 lines
2.3 KiB
Go
package logtail
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type refusalProbe struct {
|
|
rows int // rows waiting for a line
|
|
lines []string
|
|
who []string
|
|
}
|
|
|
|
func (r *refusalProbe) AttachRefusalLine(login, from string, _ time.Time, line string) (bool, error) {
|
|
if r.rows == 0 {
|
|
return false, nil
|
|
}
|
|
r.rows--
|
|
r.lines = append(r.lines, line)
|
|
r.who = append(r.who, login+"|"+from)
|
|
return true, nil
|
|
}
|
|
|
|
// The NOQUEUE line of a refusal at MAIL FROM goes on its row; a line whose
|
|
// row is not written yet waits and is tried again; port 25's and other
|
|
// lines are not refusals of an application.
|
|
func TestRefusalLines(t *testing.T) {
|
|
now := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)
|
|
st := &refusalProbe{}
|
|
r := &refusalLines{st: st}
|
|
limit := `2026-10-14T12:00:00+00:00 mail postfix/smtpd[1149]: NOQUEUE: milter-reject: MAIL from unknown[10.77.0.1]: 451 4.7.1 Service unavailable - try again later; from=<billing@sender.e2e.test> proto=ESMTP helo=<e2e-client.e2e.test> sasl_username=<l2app>`
|
|
for _, other := range []string{
|
|
`2026-10-14T12:00:00+00:00 mail postfix/inbound/smtpd[372]: NOQUEUE: milter-reject: MAIL from unknown[203.0.113.7]: 451 4.7.1 Service unavailable - try again later; from=<a@stranger.example> proto=ESMTP helo=<x>`,
|
|
`2026-10-14T12:00:00+00:00 mail postfix/smtpd[11]: NOQUEUE: milter-reject: CONNECT from unknown[10.77.0.1]: 451 4.7.1 Service unavailable - try again later; proto=SMTP`,
|
|
`2026-10-14T12:00:00+00:00 mail postfix/smtpd[11]: NOQUEUE: reject: RCPT from unknown[10.77.0.1]: 553 5.7.1 <a@x>: Sender address rejected: not owned by user app1; from=<a@x> to=<b@y> proto=ESMTP helo=<x>`,
|
|
} {
|
|
r.handle(other, now)
|
|
}
|
|
if len(r.pending) != 0 {
|
|
t.Fatalf("lines that are not an application's refusal at MAIL wait: %+v", r.pending)
|
|
}
|
|
|
|
r.handle(limit, now) // its row is not there yet
|
|
if len(r.pending) != 1 {
|
|
t.Fatalf("pending %d, want the line waiting", len(r.pending))
|
|
}
|
|
st.rows = 1 // the milter's row is written
|
|
r.retry(now.Add(time.Second))
|
|
if len(r.pending) != 0 || len(st.lines) != 1 || st.who[0] != "l2app|billing@sender.e2e.test" {
|
|
t.Fatalf("after the retry: pending %d, attached %v", len(r.pending), st.who)
|
|
}
|
|
|
|
r.handle(limit, now) // no row ever comes
|
|
r.retry(now.Add(refusalLineWait + time.Second))
|
|
if len(r.pending) != 0 {
|
|
t.Fatal("a line kept waiting past its time")
|
|
}
|
|
}
|