diff --git a/client_test.go b/client_test.go index 2a16b70..404d6e3 100644 --- a/client_test.go +++ b/client_test.go @@ -78,6 +78,10 @@ type MockMilter struct { Chunks [][]byte Cmds []string + + ConnectionClosedCommand ClientCommand + ConnectionClosedResp *Response + ConnectionClosedErr error } func (mm *MockMilter) Connect(host string, family string, port uint16, addr string, m *Modifier) (*Response, error) { @@ -178,6 +182,12 @@ func (mm *MockMilter) Cleanup() { } } +func (mm *MockMilter) ConnectionClosed(lastCommand ClientCommand, resp *Response, err error) { + mm.ConnectionClosedCommand = lastCommand + mm.ConnectionClosedResp = resp + mm.ConnectionClosedErr = err +} + func assertAction(t *testing.T, act *Action, err error, expectCode ActionType) { t.Helper() if err != nil { @@ -193,6 +203,7 @@ type serverClientWrap struct { client *Client session *ClientSession local net.Listener + closed bool } func newServerClient(t *testing.T, macros Macros, serverOptions []Option, clientOptions []Option) serverClientWrap { @@ -216,8 +227,12 @@ func newServerClient(t *testing.T, macros Macros, serverOptions []Option, client } func (w *serverClientWrap) Cleanup() { - w.session.Close() - w.server.Close() + if !w.closed { + w.session.Close() + w.server.Close() + time.Sleep(time.Millisecond) + } + w.closed = true } func TestMilterClient_UsualFlow(t *testing.T) { @@ -352,6 +367,10 @@ func TestMilterClient_UsualFlow(t *testing.T) { if !reflect.DeepEqual(modifyActs, expected) { t.Fatalf("Wrong modify actions: got %+v", modifyActs) } + w.Cleanup() + if mm.ConnectionClosedCommand != CommandQuit { + t.Fatalf("ConnectionClosed() lastCommand got %q, expected %q", mm.ConnectionClosedCommand, CommandQuit) + } } func TestMilterClient_AbortFlow(t *testing.T) { @@ -463,29 +482,39 @@ func TestMilterClient_NoWorking(t *testing.T) { } } -func TestMilterClient_NegotiationMismatch(t *testing.T) { +func TestMilterClient_ClosePremature(t *testing.T) { t.Parallel() - mm := MockMilter{} - s := NewServer(WithMilter(func() Milter { + mm := MockMilter{ + ConnResp: RespContinue, + HeloResp: RespContinue, + MailResp: RespContinue, + RcptResp: RespContinue, + DataResp: RespReject, + } + macros := NewMacroBag() + w := newServerClient(t, macros, []Option{WithMilter(func() Milter { return &mm - }), WithActions(OptAddHeader|OptChangeHeader), WithProtocols(OptNoMailFrom)) - local, err := net.Listen("tcp", "127.0.0.1:0") + }), WithActions(OptAddHeader | OptChangeBody | OptAddRcpt | OptRemoveRcpt | OptChangeHeader | OptQuarantine | OptChangeFrom | OptAddRcptWithArgs)}, + []Option{WithActions(OptAddHeader | OptChangeBody | OptAddRcpt | OptRemoveRcpt | OptChangeHeader | OptQuarantine | OptChangeFrom | OptAddRcptWithArgs)}, + ) + defer w.Cleanup() + + macros.Set(MacroTlsVersion, "very old") + act, err := w.session.Conn("host", FamilyInet, 25565, "172.0.0.1") + assertAction(t, act, err, ActionContinue) + act, err = w.session.Helo("helo_host") + assertAction(t, act, err, ActionContinue) + act, err = w.session.Mail("from@example.org", "A=B") + assertAction(t, act, err, ActionContinue) + act, err = w.session.Rcpt("to1@example.org", "A=B") + assertAction(t, act, err, ActionContinue) + err = w.session.Close() if err != nil { t.Fatal(err) } - go s.Serve(local) - client := NewClient("tcp", local.Addr().String(), WithActions(OptAddHeader|OptChangeHeader|OptQuarantine), WithProtocols(OptNoEOH)) - session, err := client.Session(nil) - if err == nil { - session.Close() - t.Fatal("negotiation should fail") - } - - client2 := NewClient("tcp", local.Addr().String(), WithActions(OptAddHeader), WithProtocols(OptNoMailFrom)) - session2, err := client2.Session(nil) - if err == nil { - session2.Close() - t.Fatal("negotiation should fail") + w.Cleanup() + if mm.ConnectionClosedCommand != CommandQuit { + t.Fatalf("ConnectionClosed() lastCommand got %q, expected %q", mm.ConnectionClosedCommand, CommandQuit) } } diff --git a/cmd/log-milter/milter.go b/cmd/log-milter/milter.go index cdfdd46..61f93b5 100644 --- a/cmd/log-milter/milter.go +++ b/cmd/log-milter/milter.go @@ -87,6 +87,10 @@ func (l *LogMilter) Cleanup() { l.macroValues = nil } +func (l *LogMilter) ConnectionClosed(lastCommand milter.ClientCommand, resp *milter.Response, err error) { + l.log("CLOSED Last received command = %s, our response = %v, err = %v", lastCommand, resp, err) +} + func (l *LogMilter) outputChangedMacros(m *milter.Modifier) { if l.macroValues == nil { l.macroValues = make(map[milter.MacroName]string) diff --git a/go.mod b/go.mod index 492c4c4..e3e51a9 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/d--j/go-milter go 1.18 require ( - github.com/emersion/go-message v0.16.0 - golang.org/x/net v0.7.0 - golang.org/x/text v0.9.0 + github.com/emersion/go-message v0.17.0 + golang.org/x/net v0.16.0 + golang.org/x/text v0.13.0 ) require github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect diff --git a/go.sum b/go.sum index d0f805d..32dfca2 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,39 @@ -github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4= -github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ= +github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04= +github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/integration/go.mod b/integration/go.mod index 9ccc40f..05dbbfd 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -3,17 +3,17 @@ module github.com/d--j/go-milter/integration go 1.18 require ( - github.com/d--j/go-milter v0.8.2 - github.com/emersion/go-message v0.16.0 - github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 - github.com/emersion/go-smtp v0.16.0 - golang.org/x/text v0.9.0 - golang.org/x/tools v0.6.0 + github.com/d--j/go-milter v0.8.3 + github.com/emersion/go-message v0.17.0 + github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead + github.com/emersion/go-smtp v0.18.1 + golang.org/x/text v0.13.0 + golang.org/x/tools v0.13.0 ) require ( github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/net v0.16.0 // indirect ) replace github.com/d--j/go-milter => ../ diff --git a/integration/go.sum b/integration/go.sum index fddd1e0..ef8218c 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -1,18 +1,48 @@ -github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4= -github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ= -github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04= +github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8= -github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= +github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.18.1 h1:4DFV0jxKhq0Gqt/Br3BRHyKZy5TStk6NIMHAx6GE/LA= +github.com/emersion/go-smtp v0.18.1/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/integration/mta/mock/mta.go b/integration/mta/mock/mta.go index 6acfd86..c76f641 100644 --- a/integration/mta/mock/mta.go +++ b/integration/mta/mock/mta.go @@ -193,7 +193,7 @@ func toMailOptions(arg string) *smtp.MailOptions { opts.Body = smtp.BodyType(a[5:]) set = true } else if strings.HasPrefix(a, "SIZE=") { - opts.Size, err = strconv.Atoi(a[5:]) + opts.Size, err = strconv.ParseInt(a[5:], 10, 64) if err != nil { panic(err) } @@ -217,7 +217,7 @@ func (s *Session) Mail(from string, opts *smtp.MailOptions) error { return s.handleMilter(s.filter.Mail(s.MailFrom, s.MailFromArgs)) } -func (s *Session) Rcpt(to string) error { +func (s *Session) Rcpt(to string, _ *smtp.RcptOptions) error { log.Printf("[%s] Rcpt to: %s", s.queueId, to) if s.discarded { return nil @@ -483,7 +483,7 @@ queue: continue } for _, rcpt := range msg.Recipients { - if err := c.Rcpt(rcpt.Addr); err != nil { + if err := c.Rcpt(rcpt.Addr, nil); err != nil { log.Print(err) continue queue } diff --git a/integration/runner/receiver.go b/integration/runner/receiver.go index d34540e..7a2b773 100644 --- a/integration/runner/receiver.go +++ b/integration/runner/receiver.go @@ -53,7 +53,7 @@ func (rs *ReceiverSession) Mail(from string, opts *smtp.MailOptions) error { return nil } -func (rs *ReceiverSession) Rcpt(to string) error { +func (rs *ReceiverSession) Rcpt(to string, _ *smtp.RcptOptions) error { rs.Output.To = append(rs.Output.To, integration.ToAddrArg(to, nil)) return nil } diff --git a/integration/runner/test.go b/integration/runner/test.go index 0c7a0ce..6e5f231 100644 --- a/integration/runner/test.go +++ b/integration/runner/test.go @@ -179,7 +179,7 @@ func (t *TestCase) Send(steps []*integration.InputStep, port uint16) (uint16, st return smtpErr(err, integration.StepFrom) } case "TO": - if err := client.Rcpt(step.Addr); err != nil { + if err := client.Rcpt(step.Addr, nil); err != nil { return smtpErr(err, integration.StepTo) } case "RESET": diff --git a/milter.go b/milter.go index f85ec36..d6b9cee 100644 --- a/milter.go +++ b/milter.go @@ -62,13 +62,15 @@ const ( OptHeaderLeadingSpace OptProtocol = 1 << 20 ) +//goland:noinspection GoUnusedConst const ( // OptNoReplies combines all protocol flags that define that your milter does not send a reply // to the MTA. Use this if your [Milter] only decides at the [Milter.EndOfMessage] handler if the // email is acceptable or needs to be rejected. - OptNoReplies OptProtocol = OptNoHeaderReply | OptNoConnReply | OptNoHeloReply | OptNoMailReply | OptNoRcptReply | OptNoDataReply | OptNoUnknownReply | OptNoEOHReply | OptNoBodyReply + OptNoReplies = OptNoHeaderReply | OptNoConnReply | OptNoHeloReply | OptNoMailReply | OptNoRcptReply | OptNoDataReply | OptNoUnknownReply | OptNoEOHReply | OptNoBodyReply ) +//goland:noinspection GoUnusedConst const ( optMds256K uint32 = 1 << 28 // SMFIP_MDS_256K optMds1M uint32 = 1 << 29 // SMFIP_MDS_1M diff --git a/server.go b/server.go index e6d7296..14c35d0 100644 --- a/server.go +++ b/server.go @@ -12,6 +12,28 @@ const MaxServerProtocolVersion uint32 = 6 // ErrServerClosed is returned by the [Server]'s [Server.Serve] method after a call to [Server.Close]. var ErrServerClosed = errors.New("milter: server closed") +// ClientCommand represents a command that a milter client (MTA) sent to the milter server (filter program). +type ClientCommand string + +const ( + CommandNone ClientCommand = "" // The client did not send any commands + CommandNegotiate = "NEGOTIATE" // The client sent a SMFIC_OPTNEG command + CommandConnect = "CONNECT" // The client sent a SMFIC_CONNECT command + CommandHelo = "HELO" // The client sent a SMFIC_HELO command + CommandMail = "MAIL" // The client sent a SMFIC_MAIL command + CommandRcpt = "RCPT" // The client sent a SMFIC_RCPT command + CommandData = "DATA" // The client sent a SMFIC_DATA command + CommandHeader = "HEADER" // The client sent a SMFIC_HEADER command + CommandEOH = "END-OF-HEADER" // The client sent a SMFIC_EOH command + CommandBodyChunk = "BODY-CHUNK" // The client sent a SMFIC_BODY command + CommandEOM = "END-OF-MESSAGE" // The client sent a SMFIC_BODYEOB command + CommandUnknown = "UNKNOWN" // The client sent a SMFIC_UNKNOWN command + CommandMacro = "MACRO" // The client sent a SMFIC_MACRO command + CommandAbort = "ABORT" // The client sent a SMFIC_ABORT command + CommandQuit = "QUIT" // The client sent a SMFIC_QUIT command + CommandQuitNewConn = "QUIT-NEW-CONN" // The client sent a SMFIC_QUIT_NC command +) + // Milter is an interface for milter callback handlers. type Milter interface { // Connect is called to provide SMTP connection data for incoming message. @@ -89,6 +111,20 @@ type Milter interface { // E.g. because the MTA closed the connection, one SMTP message was successful or there was an error. // May be called more than once for a single [Milter]. Cleanup() + + // ConnectionClosed gets called after the milter connection was closed (by the client or server). + // It does not get called when client issues a SMFIC_QUIT_NC command or when one connection gets used for multiple messages. + // You should not use this function to clean up resources. Clients can use a single milter connection to process + // multiple SMTP messages and/or SMTP connections. Every message gets a new [Milter] backend but only the last + // [Milter] backend's ConnectionClosed function gets called (when the connection finally gets closed). + // + // The parameter lastCommand is the last command that the client passed to the [Milter]. + // + // The optional resp parameter is our response that we sent (err = nil) or would have sent (err != nil, if we already had one). + // + // The optional err parameter is the error that we might have gotten (the reason why the connection closed). + // It might be nil when we closed the connection. + ConnectionClosed(lastCommand ClientCommand, resp *Response, err error) } // NoOpMilter is a dummy [Milter] implementation that does nothing. @@ -96,46 +132,57 @@ type NoOpMilter struct{} var _ Milter = NoOpMilter{} +//goland:noinspection GoUnusedParameter func (NoOpMilter) Connect(host string, family string, port uint16, addr string, m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) Helo(name string, m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) MailFrom(from string, esmtpArgs string, m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) RcptTo(rcptTo string, esmtpArgs string, m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) Data(m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) Header(name string, value string, m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) Headers(m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) BodyChunk(chunk []byte, m *Modifier) (*Response, error) { return RespContinue, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) EndOfMessage(m *Modifier) (*Response, error) { return RespAccept, nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) Abort(_ *Modifier) error { return nil } +//goland:noinspection GoUnusedParameter func (NoOpMilter) Unknown(cmd string, m *Modifier) (*Response, error) { return RespContinue, nil } @@ -143,6 +190,10 @@ func (NoOpMilter) Unknown(cmd string, m *Modifier) (*Response, error) { func (NoOpMilter) Cleanup() { } +//goland:noinspection GoUnusedParameter +func (m NoOpMilter) ConnectionClosed(lastCommand ClientCommand, resp *Response, err error) { +} + // Server is a milter server. type Server struct { options options @@ -212,12 +263,13 @@ func (s *Server) Serve(ln net.Listener) error { } session := serverSession{ - server: s, - version: s.options.maxVersion, - actions: s.options.actions, - protocol: s.options.protocol, - conn: conn, - macros: newMacroStages(), + server: s, + version: s.options.maxVersion, + actions: s.options.actions, + protocol: s.options.protocol, + conn: conn, + macros: newMacroStages(), + lastCommand: CommandNone, } go session.HandleMilterCommands() } diff --git a/server_test.go b/server_test.go index baf0e69..3cdb519 100644 --- a/server_test.go +++ b/server_test.go @@ -2,10 +2,10 @@ package milter import ( "bytes" - "testing" - "github.com/d--j/go-milter/internal/wire" "github.com/emersion/go-message/textproto" + "net" + "testing" ) func TestNoOpMilter(t *testing.T) { @@ -113,3 +113,42 @@ func TestServer_NoOpMilter(t *testing.T) { t.Fatal(err) } } + +func TestNewServer(t *testing.T) { + dummyMilter := func(version uint32, action OptAction, protocol OptProtocol, maxData DataSize) Milter { + return &NoOpMilter{} + } + t.Parallel() + tests := []struct { + name string + opts []Option + check func(s *Server) bool + wantPanic bool + }{ + {"milter missing", []Option{WithMaximumVersion(6)}, nil, true}, + {"with invalid version 1", []Option{WithDynamicMilter(dummyMilter), WithMaximumVersion(1)}, nil, true}, + {"with invalid version 10", []Option{WithDynamicMilter(dummyMilter), WithMaximumVersion(10)}, nil, true}, + {"with dialer", []Option{WithDynamicMilter(dummyMilter), WithDialer(&net.Dialer{})}, nil, true}, + {"with offered max data", []Option{WithDynamicMilter(dummyMilter), WithOfferedMaxData(123)}, nil, true}, + {"with macros", []Option{WithDynamicMilter(dummyMilter), WithMacroRequest(StageConnect, []MacroName{MacroClientName})}, func(s *Server) bool { + return (s.options.actions & OptSetMacros) != 0 + }, false}, + } + for _, tt_ := range tests { + t.Run(tt_.name, func(t *testing.T) { + tt := tt_ + t.Parallel() + defer func() { _ = recover() }() + got := NewServer(tt.opts...) + if tt.check != nil { + if !tt.check(got) { + t.Errorf("check failed, got %+v", got) + } + } + if tt.wantPanic { + t.Errorf("did not panic") + } + + }) + } +} diff --git a/session.go b/session.go index f519471..a4e5de5 100644 --- a/session.go +++ b/session.go @@ -24,11 +24,49 @@ type serverSession struct { conn net.Conn macros *macrosStages backend Milter + lastCommand ClientCommand } // readPacket reads incoming milter packet func (m *serverSession) readPacket() (*wire.Message, error) { - return wire.ReadPacket(m.conn, 0) + msg, err := wire.ReadPacket(m.conn, 0) + if msg != nil { + switch msg.Code { + case wire.CodeOptNeg: + m.lastCommand = CommandNegotiate + case wire.CodeConn: + m.lastCommand = CommandConnect + case wire.CodeHelo: + m.lastCommand = CommandHelo + case wire.CodeMail: + m.lastCommand = CommandMail + case wire.CodeRcpt: + m.lastCommand = CommandRcpt + case wire.CodeData: + m.lastCommand = CommandData + case wire.CodeHeader: + m.lastCommand = CommandHeader + case wire.CodeEOH: + m.lastCommand = CommandEOH + case wire.CodeBody: + m.lastCommand = CommandBodyChunk + case wire.CodeEOB: + m.lastCommand = CommandEOM + case wire.CodeUnknown: + m.lastCommand = CommandUnknown + case wire.CodeMacro: + m.lastCommand = CommandMacro + case wire.CodeAbort: + m.lastCommand = CommandAbort + case wire.CodeQuitNewConn: + m.lastCommand = CommandQuitNewConn + case wire.CodeQuit: + m.lastCommand = CommandQuit + default: + m.lastCommand = ClientCommand(fmt.Sprintf("unknown-command-%c", msg.Code)) + } + } + return msg, err } // writePacket sends a milter response packet to socket stream @@ -350,16 +388,21 @@ func (m *serverSession) HandleMilterCommands() { if err != io.EOF { LogWarning("Error reading milter command: %v", err) } + // no backend yet + // m.backend.ConnectionClosed(m.lastCommand, nil, err) return } resp, err := m.negotiate(msg, m.server.options.maxVersion, m.server.options.actions, m.server.options.protocol, m.server.options.negotiationCallback, m.server.options.macrosByStage, 0) if err != nil { LogWarning("Error negotiating: %v", err) + // no backend yet + // m.backend.ConnectionClosed(m.lastCommand, resp, err) return } m.backend = m.newBackend() if err = m.writePacket(resp.Response()); err != nil { LogWarning("Error writing packet: %v", err) + m.backend.ConnectionClosed(m.lastCommand, resp, err) return } @@ -370,17 +413,21 @@ func (m *serverSession) HandleMilterCommands() { if err != io.EOF { LogWarning("Error reading milter command: %v", err) } + m.backend.ConnectionClosed(m.lastCommand, nil, err) return } resp, err := m.Process(msg) if err != nil { - if err != errCloseSession { + if !errors.Is(err, errCloseSession) { // log error condition LogWarning("Error performing milter command: %v", err) if resp != nil && !m.skipResponse(msg.Code) { _ = m.writePacket(resp.Response()) } + m.backend.ConnectionClosed(m.lastCommand, resp, err) + } else { + m.backend.ConnectionClosed(m.lastCommand, resp, nil) } return } @@ -393,6 +440,7 @@ func (m *serverSession) HandleMilterCommands() { // send back response message if err = m.writePacket(resp.Response()); err != nil { LogWarning("Error writing packet: %v", err) + m.backend.ConnectionClosed(m.lastCommand, resp, err) return } diff --git a/session_test.go b/session_test.go index 5493d3b..faa5f3d 100644 --- a/session_test.go +++ b/session_test.go @@ -11,24 +11,27 @@ import ( ) type processTestMilter struct { - cleanupCalled int - host string - family string - port uint16 - addr string - name string - from string - fromEsmtp string - rcptTo string - rcptEsmtp string - dataCalled bool - hdrName, hdrValue string - headers textproto.MIMEHeader - headersCalled bool - chunk []byte - eomCalled bool - abortCalled bool - cmd string + cleanupCalled int + host string + family string + port uint16 + addr string + name string + from string + fromEsmtp string + rcptTo string + rcptEsmtp string + dataCalled bool + hdrName, hdrValue string + headers textproto.MIMEHeader + headersCalled bool + chunk []byte + eomCalled bool + abortCalled bool + cmd string + connectionClosedCmd ClientCommand + connectionClosedResp *Response + connectionClosedErr error } func (p *processTestMilter) Connect(host string, family string, port uint16, addr string, m *Modifier) (*Response, error) { @@ -96,6 +99,12 @@ func (p *processTestMilter) Cleanup() { p.cleanupCalled++ } +func (p *processTestMilter) ConnectionClosed(lastCommand ClientCommand, resp *Response, err error) { + p.connectionClosedCmd = lastCommand + p.connectionClosedResp = resp + p.connectionClosedErr = err +} + var _ Milter = &processTestMilter{} func Test_milterSession_negotiate(t *testing.T) {