logtail: deliveryRe anchored at the head of the line (audit M12)
The pattern that writes a row's status matched anywhere in a line: a port-25 reject line ending helo=<ID: to=<victim@...>, status=sent> parsed as (ID, victim, sent). parseDelivery now takes the stamp off with SplitTimestamp and matches `^host program[pid]: <id>: to=<...>, ... status=`, as queueLineRe and the inbound patterns do. Tests: the probed HELO line, the same text in a quoted sender on a qmgr line, the text after an smtpd line's queue id - none parse; a syslog-stamped line as Postfix 3.7 writes it parses. The existing cases unchanged. CHANGELOG [Unreleased], audit plan line ticked, roadmap 11/61. Evidence: go test ./internal/logtail ok but for TestFollowTailsAndRotates (Windows); the server run of the stage follows before the push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -88,17 +88,25 @@ type RetentionDays func() int
|
||||
// The "<queue-id>: to=<addr>, …, status=<word>" shape is specific to the
|
||||
// delivery agents; qmgr/smtpd/cleanup lines do not match.
|
||||
//
|
||||
// It is matched at the head of the line - the host name, "program[pid]:",
|
||||
// then the queue id - after SplitTimestamp has taken the stamp off, like
|
||||
// queueLineRe and the inbound patterns. Anywhere else the text is a
|
||||
// stranger's: a port-25 reject line ends with the client's HELO name, which
|
||||
// once parsed as a delivery result when it read "<id>: to=<…>, status=sent"
|
||||
// (audit 2026-10-12, M12).
|
||||
//
|
||||
// The run before status= is lazy on purpose. Postfix appends the remote
|
||||
// server's reply verbatim, so a greedy match would take the *last* status= on
|
||||
// the line — and that one can come from the reply text, which the far end
|
||||
// controls. A bounce whose reply quoted "status=sent" would then be filed as a
|
||||
// success. The real field is always the first one after to=<…>.
|
||||
var deliveryRe = regexp.MustCompile(`\b([0-9A-Za-z]+): to=<([^>]*)>,.*?\bstatus=(\w+)`)
|
||||
var deliveryRe = regexp.MustCompile(`^\S+ [^\s\[]+\[\d+\]: ([0-9A-Za-z]+): to=<([^>]*)>,.*?\bstatus=(\w+)`)
|
||||
|
||||
// parseDelivery extracts (queue-id, recipient, status) from a mail.log line.
|
||||
// ok is false for lines that are not recognised delivery results.
|
||||
func parseDelivery(line string) (queueID, recipient, status string, ok bool) {
|
||||
m := deliveryRe.FindStringSubmatch(line)
|
||||
_, rest := SplitTimestamp(line)
|
||||
m := deliveryRe.FindStringSubmatch(rest)
|
||||
if m == nil {
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
@@ -116,6 +116,34 @@ func TestParseDelivery(t *testing.T) {
|
||||
line: "host postfix/cleanup[12]: BEEF03: message-id=<x@example.com>",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
// The stamp Postfix 3.7's postlogd writes, as on the stand.
|
||||
name: "syslog stamp",
|
||||
line: "Oct 14 11:05:51 mail postfix/smtp[26]: 41E862C00D9E: to=<a@example.net>, relay=x, dsn=2.0.0, status=sent (250 OK)",
|
||||
wantOK: true,
|
||||
queueID: "41E862C00D9E",
|
||||
recipient: "a@example.net",
|
||||
status: store.StatusSent,
|
||||
},
|
||||
{
|
||||
// A stranger's HELO name at the end of a port-25 reject line, as
|
||||
// probed for the 2026-10-12 audit: it must not file a delivery.
|
||||
name: "a delivery result in a HELO name is not one",
|
||||
line: "Oct 14 11:05:51 mail postfix/inbound/smtpd[372]: NOQUEUE: reject: RCPT from unknown[203.0.113.7]: 554 5.7.1 <x@elsewhere.example>: Relay access denied; from=<a@stranger.example> to=<x@elsewhere.example> proto=ESMTP helo=<41E862C00D9E: to=<victim@example.net>, status=sent>",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
// The same text in a quoted sender address, on a line that does
|
||||
// have a queue id at its head.
|
||||
name: "a delivery result in a quoted sender is not one",
|
||||
line: `Oct 14 11:05:51 mail postfix/qmgr[10]: 5B4A2C1D3E: from=<"41E862C00D9E: to=<victim@example.net>, status=sent"@stranger.example>, size=500, nrcpt=1 (queue active)`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "an smtpd line with the text after its queue id is not one",
|
||||
line: "Oct 14 11:05:51 mail postfix/smtpd[10]: 5B4A2C1D3E: client=unknown[203.0.113.7], sasl_username=41E862C00D9E: to=<victim@example.net>, status=sent",
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user