test / test (push) Canceled after 0s
The runtime base is debian:trixie-slim, the build stage golang:1.26-trixie:
Postfix 3.10.13 with upstream's 2026 security fixes up to the August
batch, libmilter 8.18.1, OpenSSL 3.5. Every citation of another program in
code, scripts and documents was re-read against the trixie sources
(dev/upstream; bookworm's kept in dev/upstream-bookworm) and updated;
development.md's cited-package table holds the trixie versions.
What the re-reading changed in behaviour:
- logtail: reject lines with sasl_method/sasl_username/sasl_sender
(smtpd_check.c:1019-1026); cleanup's "removed (canceled)" forgets a
port-25 id (cleanup_api.c:363-364).
- internal/postfix/retry.go reads postconf's stdout only: since 3.9 a
deprecated-parameter warning on stderr made one value too many and the
panel fell back to the compiled-in retry policy.
- logrotate-loop.sh: logrotate 3.22 exits 1 when it ignores a config;
the loop refuses on that output whatever the status.
- postfix-config.sh: cyrus_sasl_config_path set explicitly (bookworm's
07_sasl_config.diff is gone); maillog_file_permissions=0640; mynetworks
explicit; Dockerfile: SYNC_CHROOT= in /etc/default/postfix,
opendkim-tools dropped, openssl named, systemd-standalone-sysusers so
cron does not bring the whole of systemd.
- The selfpost group is 10998, outside the system range 100-999: in
trixie a package took 998 and the build failed (owner's decision:
a conflict-free id, the distribution's own ids untouched). The
entrypoint hands /data back by name when the user or the group differs.
Guide, backup refusal text, help (design commit 68ec8b2), e2e.
- smtpd_hide_client_session=yes on smtps and submission (owner's
decision): Received: names no client, TLS, login or protocol; port 25
keeps the full header. e2e checks both.
- test/upstream/opendkim-probe.sh makes keys with openssl genrsa.
Tests: on the server, make e2e exit 0 (674 s, the image built on trixie),
go vet + go test 27 packages ok. CHANGELOG [Unreleased], guide,
architecture.md, security.md, roadmap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
4.2 KiB
Go
133 lines
4.2 KiB
Go
package milter
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/emersion/go-milter"
|
|
)
|
|
|
|
// The milter gets the address as go-milter v0.4.1 hands it: the command's
|
|
// first argument, the ESMTP parameters left out, "<>" trimmed (session.go,
|
|
// case CodeMail/CodeRcpt). Nothing more is cut: a quoted local part may hold
|
|
// a blank or a ">" (assumptions B-G5).
|
|
func TestCleanAddress(t *testing.T) {
|
|
for _, c := range []struct{ in, want string }{
|
|
{"a@example.net", "a@example.net"},
|
|
{"", ""},
|
|
{`"a b"@example.net`, `"a b"@example.net`},
|
|
{`"a>b"@example.net`, `"a>b"@example.net`},
|
|
{` a@example.net `, "a@example.net"},
|
|
} {
|
|
if got := cleanAddress(c.in); got != c.want {
|
|
t.Errorf("cleanAddress(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The journal row's recipient has to be the address Postfix later logs in
|
|
// to=<…>, or the delivery line matches no row and the sweep closes it as
|
|
// bounced: the local part unquoted, then quoted again only when it is not a
|
|
// dot-string (Postfix 3.10.13, src/global/tok822_parse.c and
|
|
// quote_822_local.c). Composed by hand after those functions.
|
|
func TestRecipientAsLogged(t *testing.T) {
|
|
for _, c := range []struct{ in, want string }{
|
|
{"a@example.net", "a@example.net"},
|
|
{"User@Example.NET", "User@Example.NET"}, // case kept
|
|
{`"ab"@example.net`, "ab@example.net"}, // quotes not needed
|
|
{`"a.b"@example.net`, "a.b@example.net"},
|
|
{`"a b"@example.net`, `"a b"@example.net`},
|
|
{`"a\b"@example.net`, "ab@example.net"}, // the backslash only escapes
|
|
{`"a\"b"@example.net`, `"a\"b"@example.net`},
|
|
{`"a..b"@example.net`, `"a..b"@example.net`},
|
|
{`".a"@example.net`, `".a"@example.net`},
|
|
{`"a(b)"@example.net`, `"a(b)"@example.net`},
|
|
{`"почта"@example.net`, "почта@example.net"}, // 8-bit clean
|
|
{"postmaster", "postmaster"},
|
|
} {
|
|
if got := recipientAsLogged(c.in); got != c.want {
|
|
t.Errorf("recipientAsLogged(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Through go-milter's own client, as Postfix sends it: the address and its
|
|
// ESMTP parameters as separate strings. A recipient given twice, in another
|
|
// case, is one row - cleanup keeps one (assumptions A-31).
|
|
func TestRecipientsThroughGoMilter(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := &fakeRecorder{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go Serve(ctx, ln, rec)
|
|
|
|
// The library's server speaks milter version 2, so the client asks for no
|
|
// action only version 6 has (go-milter v0.4.1, client.go, negotiate();
|
|
// its client_test.go does the same).
|
|
cl := milter.NewClientWithOptions("tcp", ln.Addr().String(), milter.ClientOptions{
|
|
ActionMask: milter.OptAddHeader | milter.OptChangeHeader,
|
|
})
|
|
defer cl.Close()
|
|
s, err := cl.Session()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
step := func(what string, act *milter.Action, err error) {
|
|
t.Helper()
|
|
if err != nil || act.Code != milter.ActContinue {
|
|
t.Fatalf("%s: %+v, %v", what, act, err)
|
|
}
|
|
}
|
|
act, err := s.Conn("client.example.org", milter.FamilyInet, 40000, "203.0.113.7")
|
|
step("connect", act, err)
|
|
if err := s.Macros(milter.CodeMail, "{auth_authen}", "app1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
act, err = s.Mail("<noreply@example.com>", []string{"SIZE=100", "BODY=8BITMIME"})
|
|
step("mail", act, err)
|
|
for _, r := range []struct {
|
|
rcpt string
|
|
args []string
|
|
}{
|
|
{`<"a b"@example.net>`, []string{"NOTIFY=SUCCESS"}},
|
|
{`<"ab"@example.net>`, nil},
|
|
{`<AB@example.net>`, nil}, // ab@example.net again, in capitals
|
|
} {
|
|
act, err = s.Rcpt(r.rcpt, r.args)
|
|
step("rcpt "+r.rcpt, act, err)
|
|
}
|
|
act, err = s.HeaderField("Subject", "hello")
|
|
step("header", act, err)
|
|
act, err = s.HeaderEnd()
|
|
step("end of headers", act, err)
|
|
if err := s.Macros(milter.CodeEOB, "i", "Q1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, act, err = s.End()
|
|
step("end", act, err)
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
rec.mu.Lock()
|
|
var got []string
|
|
for _, e := range rec.entries {
|
|
got = append(got, e.To)
|
|
}
|
|
rec.mu.Unlock()
|
|
if strings.Join(got, "; ") == `"a b"@example.net; ab@example.net` {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("rows for %v, want \"a b\"@example.net and ab@example.net", got)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|