Files
mixandClaude Opus 5.5 f1d90a2a1b
test / test (push) Waiting to run
go-milter: build it patched not to keep every header
The security documentation's last open item of panel-dkim-signing (the
owner's decision, 2026-10-16): go-milter v0.4.1 added every header of a
session to a map of its own, emptied only on ABORT, and handed it to
Headers - which no milter of the panel reads. Postfix bounds one header,
not their number, so a message of nothing but headers up to the message
size limit was held whole by the library in the sender check, the signer
and the journal milter on 465/587 and in the inbound journal milter on port
25, where strangers send; the signer's 1 MiB refusal came after it was all
in.

third_party/go-milter is v0.4.1 from the module cache (BSD 2-Clause,
LICENSE kept) with session.go changed: CodeHeader hands the header to
Header and keeps nothing, CodeEOH hands Headers an empty map. PATCHES.md
says what and why, and how to take a new upstream version. go.mod replaces
the module with it; the Dockerfile copies third_party before "go mod
download". TestTheLibraryKeepsNoHeaders holds it through the library's own
client over two messages of one connection. Comments, security.md (the
accepted risk becomes a note), development.md's library table, the plan and
CHANGELOG follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:19:57 +03:00

413 lines
9.5 KiB
Go

package milter
import (
"bytes"
"net"
nettextproto "net/textproto"
"reflect"
"testing"
"github.com/emersion/go-message/textproto"
)
type MockMilter struct {
ConnResp Response
ConnMod func(m *Modifier)
ConnErr error
HeloResp Response
HeloMod func(m *Modifier)
HeloErr error
MailResp Response
MailMod func(m *Modifier)
MailErr error
RcptResp Response
RcptMod func(m *Modifier)
RcptErr error
HdrResp Response
HdrMod func(m *Modifier)
HdrErr error
HdrsResp Response
HdrsMod func(m *Modifier)
HdrsErr error
BodyChunkResp Response
BodyChunkMod func(m *Modifier)
BodyChunkErr error
BodyResp Response
BodyMod func(m *Modifier)
BodyErr error
AbortMod func(m *Modifier)
AbortErr error
// Info collected during calls.
Host string
Family string
Port uint16
Addr net.IP
HeloValue string
From string
Rcpt []string
Hdr nettextproto.MIMEHeader
Chunks [][]byte
}
func (mm *MockMilter) Connect(host string, family string, port uint16, addr net.IP, m *Modifier) (Response, error) {
if mm.ConnMod != nil {
mm.ConnMod(m)
}
mm.Host = host
mm.Family = family
mm.Port = port
mm.Addr = addr
return mm.ConnResp, mm.ConnErr
}
func (mm *MockMilter) Helo(name string, m *Modifier) (Response, error) {
if mm.HeloMod != nil {
mm.HeloMod(m)
}
mm.HeloValue = name
return mm.HeloResp, mm.HeloErr
}
func (mm *MockMilter) MailFrom(from string, m *Modifier) (Response, error) {
if mm.MailMod != nil {
mm.MailMod(m)
}
mm.From = from
return mm.MailResp, mm.MailErr
}
func (mm *MockMilter) RcptTo(rcptTo string, m *Modifier) (Response, error) {
if mm.RcptMod != nil {
mm.RcptMod(m)
}
mm.Rcpt = append(mm.Rcpt, rcptTo)
return mm.RcptResp, mm.RcptErr
}
func (mm *MockMilter) Header(name string, value string, m *Modifier) (Response, error) {
if mm.HdrMod != nil {
mm.HdrMod(m)
}
return mm.HdrResp, mm.HdrErr
}
func (mm *MockMilter) Headers(h nettextproto.MIMEHeader, m *Modifier) (Response, error) {
if mm.HdrsMod != nil {
mm.HdrsMod(m)
}
mm.Hdr = h
return mm.HdrsResp, mm.HdrsErr
}
func (mm *MockMilter) BodyChunk(chunk []byte, m *Modifier) (Response, error) {
if mm.BodyChunkMod != nil {
mm.BodyChunkMod(m)
}
mm.Chunks = append(mm.Chunks, chunk)
return mm.BodyChunkResp, mm.BodyChunkErr
}
func (mm *MockMilter) Body(m *Modifier) (Response, error) {
if mm.BodyMod != nil {
mm.BodyMod(m)
}
return mm.BodyResp, mm.BodyErr
}
func (mm *MockMilter) Abort(m *Modifier) error {
if mm.AbortMod != nil {
mm.AbortMod(m)
}
return mm.AbortErr
}
func TestMilterClient_UsualFlow(t *testing.T) {
mm := MockMilter{
ConnResp: RespContinue,
HeloResp: RespContinue,
MailResp: RespContinue,
RcptResp: RespContinue,
HdrResp: RespContinue,
HdrsResp: RespContinue,
BodyChunkResp: RespContinue,
BodyResp: RespContinue,
BodyMod: func(m *Modifier) {
m.AddHeader("X-Bad", "very")
m.ChangeHeader(1, "Subject", "***SPAM***")
m.Quarantine("very bad message")
},
}
s := Server{
NewMilter: func() Milter {
return &mm
},
Actions: OptAddHeader | OptChangeHeader,
}
defer s.Close()
local, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go s.Serve(local)
cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{
ActionMask: OptAddHeader | OptChangeHeader | OptQuarantine,
})
defer cl.Close()
session, err := cl.Session()
if err != nil {
t.Fatal(err)
}
defer session.Close()
assertAction := func(act *Action, err error, expectCode ActionCode) {
t.Helper()
if err != nil {
t.Fatal(err)
}
if act.Code != expectCode {
t.Fatal("Unexpectedcode:", act.Code)
}
}
act, err := session.Conn("host", FamilyInet, 25565, "172.0.0.1")
assertAction(act, err, ActContinue)
if mm.Host != "host" {
t.Fatal("Wrong host:", mm.Host)
}
if mm.Family != "tcp4" {
t.Fatal("Wrong family:", mm.Family)
}
if mm.Port != 25565 {
t.Fatal("Wrong port:", mm.Port)
}
if mm.Addr.String() != "172.0.0.1" {
t.Fatal("Wrong IP:", mm.Addr)
}
if err := session.Macros(CodeHelo, "tls_version", "very old"); err != nil {
t.Fatal("Unexpected error", err)
}
act, err = session.Helo("helo_host")
assertAction(act, err, ActContinue)
if mm.HeloValue != "helo_host" {
t.Fatal("Wrong helo value:", mm.HeloValue)
}
act, err = session.Mail("from@example.org", []string{"A=B"})
assertAction(act, err, ActContinue)
if mm.From != "from@example.org" {
t.Fatal("Wrong MAIL FROM:", mm.From)
}
act, err = session.Rcpt("to1@example.org", []string{"A=B"})
assertAction(act, err, ActContinue)
act, err = session.Rcpt("to2@example.org", []string{"A=B"})
assertAction(act, err, ActContinue)
if !reflect.DeepEqual(mm.Rcpt, []string{"to1@example.org", "to2@example.org"}) {
t.Fatal("Wrong recipients:", mm.Rcpt)
}
hdr := textproto.Header{}
hdr.Add("From", "from@example.org")
hdr.Add("To", "to@example.org")
hdr.Add("x-empty-header", "")
act, err = session.Header(hdr)
assertAction(act, err, ActContinue)
if len(mm.Hdr) != 3 {
t.Fatal("Unexpected header length:", len(mm.Hdr))
}
if val := mm.Hdr.Get("From"); val != "from@example.org" {
t.Fatal("Wrong From header:", val)
}
if val := mm.Hdr.Get("To"); val != "to@example.org" {
t.Fatal("Wrong To header:", val)
}
if val := mm.Hdr.Get("x-empty-header"); val != "" {
t.Fatal("Wrong To header:", val)
}
modifyActs, act, err := session.BodyReadFrom(bytes.NewReader(bytes.Repeat([]byte{'A'}, 128000)))
assertAction(act, err, ActContinue)
if len(mm.Chunks) != 2 {
t.Fatal("Wrong amount of body chunks received")
}
if len(mm.Chunks[0]) > 65535 {
t.Fatal("Too big first chunk:", len(mm.Chunks[0]))
}
if totalLen := len(mm.Chunks[0]) + len(mm.Chunks[1]); totalLen < 128000 {
t.Fatal("Some body bytes lost:", totalLen)
}
expected := []ModifyAction{
{
Code: ActAddHeader,
HeaderName: "X-Bad",
HeaderValue: "very",
},
{
Code: ActChangeHeader,
HeaderIndex: 1,
HeaderName: "Subject",
HeaderValue: "***SPAM***",
},
{
Code: ActQuarantine,
Reason: "very bad message",
},
}
if !reflect.DeepEqual(modifyActs, expected) {
t.Fatalf("Wrong modify actions, got %+v", modifyActs)
}
}
func TestMilterClient_AbortFlow(t *testing.T) {
macros := make(map[string]string)
mm := MockMilter{
ConnResp: RespContinue,
HeloResp: RespContinue,
HeloMod: func(m *Modifier) {
macros = m.Macros
},
AbortMod: func(m *Modifier) {
macros = m.Macros
},
}
s := Server{
NewMilter: func() Milter {
return &mm
},
Actions: OptAddHeader | OptChangeHeader,
}
defer s.Close()
local, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go s.Serve(local)
cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{
ActionMask: OptAddHeader | OptChangeHeader | OptQuarantine,
})
defer cl.Close()
session, err := cl.Session()
if err != nil {
t.Fatal(err)
}
defer session.Close()
assertAction := func(act *Action, err error, expectCode ActionCode) {
t.Helper()
if err != nil {
t.Fatal(err)
}
if act.Code != expectCode {
t.Fatal("Unexpectedcode:", act.Code)
}
}
act, err := session.Conn("host", FamilyInet, 25565, "172.0.0.1")
assertAction(act, err, ActContinue)
if mm.Host != "host" {
t.Fatal("Wrong host:", mm.Host)
}
if mm.Family != "tcp4" {
t.Fatal("Wrong family:", mm.Family)
}
if mm.Port != 25565 {
t.Fatal("Wrong port:", mm.Port)
}
if mm.Addr.String() != "172.0.0.1" {
t.Fatal("Wrong IP:", mm.Addr)
}
if err := session.Macros(CodeHelo, "tls_version", "very old"); err != nil {
t.Fatal("Unexpected error", err)
}
act, err = session.Helo("helo_host")
assertAction(act, err, ActContinue)
if mm.HeloValue != "helo_host" {
t.Fatal("Wrong helo value:", mm.HeloValue)
}
if v, ok := macros["tls_version"]; !ok || v != "very old" {
t.Fatal("Wrong tls_version macro value:", v)
}
err = session.Abort()
if err != nil {
t.Fatal(err)
}
// Validate macro values are preserved for the abort callback
if v, ok := macros["tls_version"]; !ok || v != "very old" {
t.Fatal("Wrong tls_version macro value: ", v)
}
act, err = session.Helo("repeated_helo_host")
assertAction(act, err, ActContinue)
if mm.HeloValue != "repeated_helo_host" {
t.Fatal("Wrong helo value:", mm.HeloValue)
}
if len(macros["tls_version"]) != 0 {
t.Fatal("Unexpected macro data:", macros)
}
}
// TestMilterClient_ImpossibleClientDowngrade tests that the client does not downgrade to v2
// in case of a v6 bit set in the ActionMask.
func TestMilterClient_ImpossibleClientDowngrade(t *testing.T) {
mm := MockMilter{
ConnResp: RespContinue,
HeloResp: RespContinue,
MailResp: RespContinue,
RcptResp: RespContinue,
HdrResp: RespContinue,
HdrsResp: RespContinue,
BodyChunkResp: RespContinue,
BodyResp: RespContinue,
BodyMod: func(m *Modifier) {
m.AddHeader("X-Bad", "very")
m.ChangeHeader(1, "Subject", "***SPAM***")
m.Quarantine("very bad message")
},
}
s := Server{
NewMilter: func() Milter {
return &mm
},
Actions: OptAddHeader | OptChangeHeader,
}
defer s.Close()
local, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go s.Serve(local)
cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{
// OptChangeFrom is only supported by a server with v6, but this server only supports v2.
// This should provoke a downgrade error.
ActionMask: OptAddHeader | OptChangeFrom,
})
defer cl.Close()
_, err = cl.Session()
if err != ErrUnsupportedMilterVersion {
t.Fatalf("Expected ErrUnsupportedMilterVersion, got %v", err)
}
}