├── go.mod ├── README.md ├── .gitignore ├── .build.yml ├── cstrings.go ├── LICENSE ├── response.go ├── message.go ├── milter.go ├── go.sum ├── modifier.go ├── server.go ├── milter-protocol-extras.txt ├── cmd └── milter-check │ └── main.go ├── session.go ├── client_test.go ├── milter-protocol.txt └── client.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emersion/go-milter 2 | 3 | go 1.12 4 | 5 | require github.com/emersion/go-message v0.18.1 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-milter 2 | 3 | [![GoDoc](https://godoc.org/github.com/emersion/go-milter?status.svg)](https://godoc.org/github.com/emersion/go-milter) 4 | [![builds.sr.ht status](https://builds.sr.ht/~emersion/go-milter/commits.svg)](https://builds.sr.ht/~emersion/go-milter/commits?) 5 | 6 | A Go library to write mail filters. 7 | 8 | ## License 9 | 10 | BSD 2-Clause 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.build.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - go 4 | # Required by codecov 5 | - bash 6 | - findutils 7 | sources: 8 | - https://github.com/emersion/go-milter 9 | tasks: 10 | - build: | 11 | cd go-milter 12 | go build -v ./... 13 | - test: | 14 | cd go-milter 15 | go test -coverprofile=coverage.txt -covermode=atomic ./... 16 | - upload-coverage: | 17 | cd go-milter 18 | export CODECOV_TOKEN=8c0f7014-fcfa-4ed9-8972-542eb5958fb3 19 | curl -s https://codecov.io/bash | bash 20 | -------------------------------------------------------------------------------- /cstrings.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // NULL terminator 9 | const null = "\x00" 10 | 11 | // DecodeCStrings splits a C style strings into a Go slice 12 | func decodeCStrings(data []byte) []string { 13 | if len(data) == 0 { 14 | return nil 15 | } 16 | return strings.Split(strings.Trim(string(data), null), null) 17 | } 18 | 19 | // ReadCString reads and returns a C style string from []byte 20 | func readCString(data []byte) string { 21 | pos := bytes.IndexByte(data, 0) 22 | if pos == -1 { 23 | return string(data) 24 | } 25 | return string(data[0:pos]) 26 | } 27 | 28 | // appendCString appends a C style string to the buffer and returns it (like append does). 29 | func appendCString(dest []byte, s string) []byte { 30 | dest = append(dest, []byte(s)...) 31 | dest = append(dest, 0x00) 32 | return dest 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017 Bozhin Zafirov 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | // Response represents a response structure returned by callback 4 | // handlers to indicate how the milter server should proceed 5 | type Response interface { 6 | Response() *Message 7 | Continue() bool 8 | } 9 | 10 | // SimpleResponse type to define list of pre-defined responses 11 | type SimpleResponse byte 12 | 13 | // Response returns a Message object reference 14 | func (r SimpleResponse) Response() *Message { 15 | return &Message{byte(r), nil} 16 | } 17 | 18 | // Continue to process milter messages only if current code is Continue 19 | func (r SimpleResponse) Continue() bool { 20 | return ActionCode(r) == ActContinue 21 | } 22 | 23 | // Define standard responses with no data 24 | const ( 25 | RespAccept = SimpleResponse(ActAccept) 26 | RespContinue = SimpleResponse(ActContinue) 27 | RespDiscard = SimpleResponse(ActDiscard) 28 | RespReject = SimpleResponse(ActReject) 29 | RespTempFail = SimpleResponse(ActTempFail) 30 | ) 31 | 32 | // CustomResponse is a response instance used by callback handlers to indicate 33 | // how the milter should continue processing of current message 34 | type CustomResponse struct { 35 | code byte 36 | data []byte 37 | } 38 | 39 | // Response returns message instance with data 40 | func (c *CustomResponse) Response() *Message { 41 | return &Message{c.code, c.data} 42 | } 43 | 44 | // Continue returns false if milter chain should be stopped, true otherwise 45 | func (c *CustomResponse) Continue() bool { 46 | for _, q := range []ActionCode{ActAccept, ActDiscard, ActReject, ActTempFail} { 47 | if c.code == byte(q) { 48 | return false 49 | } 50 | } 51 | return true 52 | } 53 | 54 | // NewResponse generates a new CustomResponse suitable for WritePacket 55 | func NewResponse(code byte, data []byte) *CustomResponse { 56 | return &CustomResponse{code, data} 57 | } 58 | 59 | // NewResponseStr generates a new CustomResponse with string payload 60 | func NewResponseStr(code byte, data string) *CustomResponse { 61 | return NewResponse(code, []byte(data+null)) 62 | } 63 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | // Message represents a command sent from milter client 4 | type Message struct { 5 | Code byte 6 | Data []byte 7 | } 8 | 9 | type ActionCode byte 10 | 11 | const ( 12 | ActAccept ActionCode = 'a' // SMFIR_ACCEPT 13 | ActContinue ActionCode = 'c' // SMFIR_CONTINUE 14 | ActDiscard ActionCode = 'd' // SMFIR_DISCARD 15 | ActReject ActionCode = 'r' // SMFIR_REJECT 16 | ActTempFail ActionCode = 't' // SMFIR_TEMPFAIL 17 | ActReplyCode ActionCode = 'y' // SMFIR_REPLYCODE 18 | 19 | // [v6] 20 | ActSkip ActionCode = 's' // SMFIR_SKIP 21 | ) 22 | 23 | type ModifyActCode byte 24 | 25 | const ( 26 | ActAddRcpt ModifyActCode = '+' // SMFIR_ADDRCPT 27 | ActDelRcpt ModifyActCode = '-' // SMFIR_DELRCPT 28 | ActReplBody ModifyActCode = 'b' // SMFIR_ACCEPT 29 | ActAddHeader ModifyActCode = 'h' // SMFIR_ADDHEADER 30 | ActChangeHeader ModifyActCode = 'm' // SMFIR_CHGHEADER 31 | ActInsertHeader ModifyActCode = 'i' // SMFIR_INSHEADER 32 | ActQuarantine ModifyActCode = 'q' // SMFIR_QUARANTINE 33 | 34 | // [v6] 35 | ActChangeFrom ModifyActCode = 'e' // SMFIR_CHGFROM 36 | ) 37 | 38 | type Code byte 39 | 40 | const ( 41 | CodeOptNeg Code = 'O' // SMFIC_OPTNEG 42 | CodeMacro Code = 'D' // SMFIC_MACRO 43 | CodeConn Code = 'C' // SMFIC_CONNECT 44 | CodeQuit Code = 'Q' // SMFIC_QUIT 45 | CodeHelo Code = 'H' // SMFIC_HELO 46 | CodeMail Code = 'M' // SMFIC_MAIL 47 | CodeRcpt Code = 'R' // SMFIC_RCPT 48 | CodeHeader Code = 'L' // SMFIC_HEADER 49 | CodeEOH Code = 'N' // SMFIC_EOH 50 | CodeBody Code = 'B' // SMFIC_BODY 51 | CodeEOB Code = 'E' // SMFIC_BODYEOB 52 | CodeAbort Code = 'A' // SMFIC_ABORT 53 | CodeData Code = 'T' // SMFIC_DATA 54 | 55 | // [v6] 56 | CodeQuitNewConn Code = 'K' // SMFIC_QUIT_NC 57 | ) 58 | 59 | const MaxBodyChunk = 65535 60 | 61 | type ProtoFamily byte 62 | 63 | const ( 64 | FamilyUnknown ProtoFamily = 'U' // SMFIA_UNKNOWN 65 | FamilyUnix ProtoFamily = 'L' // SMFIA_UNIX 66 | FamilyInet ProtoFamily = '4' // SMFIA_INET 67 | FamilyInet6 ProtoFamily = '6' // SMFIA_INET6 68 | ) 69 | -------------------------------------------------------------------------------- /milter.go: -------------------------------------------------------------------------------- 1 | // Package milter provides an interface to implement milter mail filters 2 | package milter 3 | 4 | // OptAction sets which actions the milter wants to perform. 5 | // Multiple options can be set using a bitmask. 6 | type OptAction uint32 7 | 8 | // Set which actions the milter wants to perform. 9 | const ( 10 | OptAddHeader OptAction = 1 << 0 // SMFIF_ADDHDRS 11 | OptChangeBody OptAction = 1 << 1 // SMFIF_CHGBODY 12 | OptAddRcpt OptAction = 1 << 2 // SMFIF_ADDRCPT 13 | OptRemoveRcpt OptAction = 1 << 3 // SMFIF_DELRCPT 14 | OptChangeHeader OptAction = 1 << 4 // SMFIF_CHGHDRS 15 | OptQuarantine OptAction = 1 << 5 // SMFIF_QUARANTINE 16 | 17 | // [v6] 18 | OptChangeFrom OptAction = 1 << 6 // SMFIF_CHGFROM 19 | OptAddRcptWithArgs OptAction = 1 << 7 // SMFIF_ADDRCPT_PAR 20 | OptSetSymList OptAction = 1 << 8 // SMFIF_SETSYMLIST 21 | ) 22 | 23 | // OptProtocol masks out unwanted parts of the SMTP transaction. 24 | // Multiple options can be set using a bitmask. 25 | type OptProtocol uint32 26 | 27 | const ( 28 | OptNoConnect OptProtocol = 1 << 0 // SMFIP_NOCONNECT 29 | OptNoHelo OptProtocol = 1 << 1 // SMFIP_NOHELO 30 | OptNoMailFrom OptProtocol = 1 << 2 // SMFIP_NOMAIL 31 | OptNoRcptTo OptProtocol = 1 << 3 // SMFIP_NORCPT 32 | OptNoBody OptProtocol = 1 << 4 // SMFIP_NOBODY 33 | OptNoHeaders OptProtocol = 1 << 5 // SMFIP_NOHDRS 34 | OptNoEOH OptProtocol = 1 << 6 // SMFIP_NOEOH 35 | OptNoUnknown OptProtocol = 1 << 8 // SMFIP_NOUNKNOWN 36 | OptNoData OptProtocol = 1 << 9 // SMFIP_NODATA 37 | 38 | // [v6] MTA supports ActSkip 39 | OptSkip OptProtocol = 1 << 10 // SMFIP_SKIP 40 | // [v6] Filter wants rejected RCPTs 41 | OptRcptRej OptProtocol = 1 << 11 // SMFIP_RCPT_REJ 42 | 43 | // Milter will not send action response for the following MTA messages 44 | OptNoHeaderReply OptProtocol = 1 << 7 // SMFIP_NR_HDR, SMFIP_NOHREPL 45 | // [v6] 46 | OptNoConnReply OptProtocol = 1 << 12 // SMFIP_NR_CONN 47 | OptNoHeloReply OptProtocol = 1 << 13 // SMFIP_NR_HELO 48 | OptNoMailReply OptProtocol = 1 << 14 // SMFIP_NR_MAIL 49 | OptNoRcptReply OptProtocol = 1 << 15 // SMFIP_NR_RCPT 50 | OptNoDataReply OptProtocol = 1 << 16 // SMFIP_NR_DATA 51 | OptNoUnknownReply OptProtocol = 1 << 17 // SMFIP_NR_UNKN 52 | OptNoEOHReply OptProtocol = 1 << 18 // SMFIP_NR_EOH 53 | OptNoBodyReply OptProtocol = 1 << 19 // SMFIP_NR_BODY 54 | 55 | // [v6] 56 | OptHeaderLeadingSpace OptProtocol = 1 << 20 // SMFIP_HDR_LEADSPC 57 | ) 58 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= 2 | github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= 3 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 6 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 7 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 8 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 9 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 10 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 11 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 12 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 14 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 22 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 23 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 24 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 25 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 26 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 27 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 28 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 29 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 30 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 31 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 32 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 33 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | -------------------------------------------------------------------------------- /modifier.go: -------------------------------------------------------------------------------- 1 | // Modifier instance is provided to milter handlers to modify email messages 2 | 3 | package milter 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "fmt" 9 | "net/textproto" 10 | ) 11 | 12 | // postfix wants LF lines endings. Using CRLF results in double CR sequences. 13 | func crlfToLF(b []byte) []byte { 14 | return bytes.ReplaceAll(b, []byte{'\r', '\n'}, []byte{'\n'}) 15 | } 16 | 17 | // Modifier provides access to Macros, Headers and Body data to callback handlers. It also defines a 18 | // number of functions that can be used by callback handlers to modify processing of the email message 19 | type Modifier struct { 20 | Macros map[string]string 21 | Headers textproto.MIMEHeader 22 | 23 | writePacket func(*Message) error 24 | } 25 | 26 | // AddRecipient appends a new envelope recipient for current message 27 | func (m *Modifier) AddRecipient(r string) error { 28 | data := []byte(fmt.Sprintf("<%s>", r) + null) 29 | return m.writePacket(NewResponse('+', data).Response()) 30 | } 31 | 32 | // DeleteRecipient removes an envelope recipient address from message 33 | func (m *Modifier) DeleteRecipient(r string) error { 34 | data := []byte(fmt.Sprintf("<%s>", r) + null) 35 | return m.writePacket(NewResponse('-', data).Response()) 36 | } 37 | 38 | // ReplaceBody substitutes message body with provided body 39 | func (m *Modifier) ReplaceBody(body []byte) error { 40 | body = crlfToLF(body) 41 | return m.writePacket(NewResponse('b', body).Response()) 42 | } 43 | 44 | // AddHeader appends a new email message header the message 45 | func (m *Modifier) AddHeader(name, value string) error { 46 | var buffer bytes.Buffer 47 | buffer.WriteString(name + null) 48 | buffer.Write(crlfToLF([]byte(value))) 49 | buffer.WriteString(null) 50 | return m.writePacket(NewResponse('h', buffer.Bytes()).Response()) 51 | } 52 | 53 | // Quarantine a message by giving a reason to hold it 54 | func (m *Modifier) Quarantine(reason string) error { 55 | return m.writePacket(NewResponse('q', []byte(reason+null)).Response()) 56 | } 57 | 58 | // ChangeHeader replaces the header at the specified position with a new one. 59 | // The index is per name. 60 | func (m *Modifier) ChangeHeader(index int, name, value string) error { 61 | var buffer bytes.Buffer 62 | if err := binary.Write(&buffer, binary.BigEndian, uint32(index)); err != nil { 63 | return err 64 | } 65 | buffer.WriteString(name + null) 66 | buffer.Write(crlfToLF([]byte(value))) 67 | buffer.WriteString(null) 68 | return m.writePacket(NewResponse('m', buffer.Bytes()).Response()) 69 | } 70 | 71 | // InsertHeader inserts the header at the specified position 72 | func (m *Modifier) InsertHeader(index int, name, value string) error { 73 | var buffer bytes.Buffer 74 | if err := binary.Write(&buffer, binary.BigEndian, uint32(index)); err != nil { 75 | return err 76 | } 77 | buffer.WriteString(name + null) 78 | buffer.Write(crlfToLF([]byte(value))) 79 | buffer.WriteString(null) 80 | return m.writePacket(NewResponse('i', buffer.Bytes()).Response()) 81 | } 82 | 83 | // ChangeFrom replaces the FROM envelope header with a new one 84 | func (m *Modifier) ChangeFrom(value string) error { 85 | data := []byte(value + null) 86 | return m.writePacket(NewResponse('e', data).Response()) 87 | } 88 | 89 | // newModifier creates a new Modifier instance from milterSession 90 | func newModifier(s *milterSession) *Modifier { 91 | return &Modifier{ 92 | Macros: s.macros, 93 | Headers: s.headers, 94 | writePacket: s.WritePacket, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/textproto" 7 | ) 8 | 9 | // Milter protocol version implemented by the server. 10 | // 11 | // Note: Not exported as we might want to support multiple versions 12 | // transparently in the future. 13 | var serverProtocolVersion uint32 = 2 14 | 15 | // ErrServerClosed is returned by the Server's Serve method after a call to 16 | // Close. 17 | var ErrServerClosed = errors.New("milter: server closed") 18 | 19 | // Milter is an interface for milter callback handlers. 20 | type Milter interface { 21 | // Connect is called to provide SMTP connection data for incoming message. 22 | // Suppress with OptNoConnect. 23 | Connect(host string, family string, port uint16, addr net.IP, m *Modifier) (Response, error) 24 | 25 | // Helo is called to process any HELO/EHLO related filters. Suppress with 26 | // OptNoHelo. 27 | Helo(name string, m *Modifier) (Response, error) 28 | 29 | // MailFrom is called to process filters on envelope FROM address. Suppress 30 | // with OptNoMailFrom. 31 | MailFrom(from string, m *Modifier) (Response, error) 32 | 33 | // RcptTo is called to process filters on envelope TO address. Suppress with 34 | // OptNoRcptTo. 35 | RcptTo(rcptTo string, m *Modifier) (Response, error) 36 | 37 | // Header is called once for each header in incoming message. Suppress with 38 | // OptNoHeaders. 39 | Header(name string, value string, m *Modifier) (Response, error) 40 | 41 | // Headers is called when all message headers have been processed. Suppress 42 | // with OptNoEOH. 43 | Headers(h textproto.MIMEHeader, m *Modifier) (Response, error) 44 | 45 | // BodyChunk is called to process next message body chunk data (up to 64KB 46 | // in size). Suppress with OptNoBody. 47 | BodyChunk(chunk []byte, m *Modifier) (Response, error) 48 | 49 | // Body is called at the end of each message. All changes to message's 50 | // content & attributes must be done here. 51 | Body(m *Modifier) (Response, error) 52 | 53 | // Abort is called is the current message has been aborted. All message data 54 | // should be reset to prior to the Helo callback. Connection data should be 55 | // preserved. 56 | Abort(m *Modifier) error 57 | } 58 | 59 | // NoOpMilter is a dummy Milter implementation that does nothing. 60 | type NoOpMilter struct{} 61 | 62 | var _ Milter = NoOpMilter{} 63 | 64 | func (NoOpMilter) Connect(host string, family string, port uint16, addr net.IP, m *Modifier) (Response, error) { 65 | return RespContinue, nil 66 | } 67 | 68 | func (NoOpMilter) Helo(name string, m *Modifier) (Response, error) { 69 | return RespContinue, nil 70 | } 71 | 72 | func (NoOpMilter) MailFrom(from string, m *Modifier) (Response, error) { 73 | return RespContinue, nil 74 | } 75 | 76 | func (NoOpMilter) RcptTo(rcptTo string, m *Modifier) (Response, error) { 77 | return RespContinue, nil 78 | } 79 | 80 | func (NoOpMilter) Header(name string, value string, m *Modifier) (Response, error) { 81 | return RespContinue, nil 82 | } 83 | 84 | func (NoOpMilter) Headers(h textproto.MIMEHeader, m *Modifier) (Response, error) { 85 | return RespContinue, nil 86 | } 87 | 88 | func (NoOpMilter) BodyChunk(chunk []byte, m *Modifier) (Response, error) { 89 | return RespContinue, nil 90 | } 91 | 92 | func (NoOpMilter) Body(m *Modifier) (Response, error) { 93 | return RespAccept, nil 94 | } 95 | 96 | func (NoOpMilter) Abort(m *Modifier) error { 97 | return nil 98 | } 99 | 100 | // Server is a milter server. 101 | type Server struct { 102 | NewMilter func() Milter 103 | Actions OptAction 104 | Protocol OptProtocol 105 | 106 | listeners []net.Listener 107 | closed bool 108 | } 109 | 110 | // Serve starts the server. 111 | func (s *Server) Serve(ln net.Listener) error { 112 | defer ln.Close() 113 | 114 | s.listeners = append(s.listeners, ln) 115 | 116 | for { 117 | conn, err := ln.Accept() 118 | if err != nil { 119 | if s.closed { 120 | return ErrServerClosed 121 | } 122 | return err 123 | } 124 | 125 | session := milterSession{ 126 | server: s, 127 | actions: s.Actions, 128 | protocol: s.Protocol, 129 | conn: conn, 130 | backend: s.NewMilter(), 131 | } 132 | go session.HandleMilterCommands() 133 | } 134 | } 135 | 136 | func (s *Server) Close() error { 137 | s.closed = true 138 | for _, ln := range s.listeners { 139 | if err := ln.Close(); err != nil { 140 | return err 141 | } 142 | } 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /milter-protocol-extras.txt: -------------------------------------------------------------------------------- 1 | This is an extension of milter-protocol.txt document which attempts to document 2 | changes made to the milter protocol. 3 | 4 | What is described here together with milter-protocol.txt is believed to be 5 | milter protocol version 6 as understood by sendmail 8.15.2 and libmilter 1.0.1. 6 | 7 | ---------------------- 8 | Protocol negotiation 9 | 10 | Actions: 11 | 12 | 0x00000001 SMFIF_ADDHDRS Add headers (SMFIR_ADDHEADER) [v1] 13 | *and insert headers (SMFIR_INSHEADER)* 14 | 0x00000002 SMFIF_CHGBODY Change body chunks (SMFIR_REPLBODY) [v1] 15 | 0x00000004 SMFIF_ADDRCPT Add recipients (SMFIR_ADDRCPT) [v1] 16 | 0x00000008 SMFIF_DELRCPT Remove recipients (SMFIR_DELRCPT) [v1] 17 | 0x00000010 SMFIF_CHGHDRS Change or delete headers (SMFIR_CHGHEADER) [v2] 18 | 0x00000020 SMFIF_QUARANTINE Quarantine message (SMFIR_QUARANTINE) [v2] 19 | 20 | 0x00000040 SMFIF_CHGFROM Change envelope sender (SMFIR_CHGFROM) 21 | 0x00000080 SMFIF_ADDRCPT_PAR Add recipient with ESMTP args 22 | 23 | 0x00000100 SMFIF_SETSYMLIST Send set of macros needed 24 | 25 | Protocol flags: 26 | 27 | 0x00000001 SMFIP_NOCONNECT Skip SMFIC_CONNECT [v1] 28 | 0x00000002 SMFIP_NOHELO Skip SMFIC_HELO [v1] 29 | 0x00000004 SMFIP_NOMAIL Skip SMFIC_MAIL [v1] 30 | 0x00000008 SMFIP_NORCPT Skip SMFIC_RCPT [v1] 31 | 0x00000010 SMFIP_NOBODY Skip SMFIC_BODY [v1] 32 | 0x00000020 SMFIP_NOHDRS Skip SMFIC_HEADER [v1] 33 | 0x00000040 SMFIP_NOEOH Skip SMFIC_EOH [v2] 34 | 35 | 0x00000080 SMFIP_NR_HDR No reply for SMFIC_HEADER 36 | 0x00000100 SMFIP_NOUNKNOWN MTA should not send unknown commands 37 | 0x00000200 SMFIP_NODATA MTA should not send DATA 38 | 0x00000400 SMFIP_SKIP MTA understands SMFIS_SKIP 39 | 0x00000800 SMFIP_RCPT_REJ MTA should also send rejected RCPTs 40 | 0x00001000 SMFIP_NR_CONN No reply for SMFIC_CONNECT 41 | 0x00002000 SMFIP_NR_HELO No reply for SMFIC_HELO 42 | 0x00004000 SMFIP_NR_MAIL No reply for SMFIC_MAIL 43 | 0x00008000 SMFIP_NR_RCPT No reply for SMFIC_RCPT 44 | 0x00010000 SMFIP_NR_DATA No reply for SMFIC_DATA 45 | 0x00020000 SMFIP_NR_UNKN No reply for SMFIC_UNKN 46 | 0x00040000 SMFIP_NR_EOH No reply for SMFIC_EOH 47 | 0x00080000 SMFIP_NR_BODY No reply for body chunk 48 | 0x00100000 SMFIP_HDR_LEADSPC Header value leading space ** 49 | 0x10000000 SMFIP_MDS_256K Max DATA size = 256K (?) 50 | 0x20000000 SMFIP_MDS_1M Max DATA size = 1M (?) 51 | 52 | [**] From libmilter docs: Indicates that the MTA can send header values with 53 | leading space intact. If this protocol step is requested, then the MTA will 54 | also not add a leading space to headers when they are added, inserted, or 55 | changed. 56 | 57 | ---------------- 58 | Command codes 59 | 60 | ** 61 | 62 | 'K' SMFIC_QUIC_NC QUIT but new connection follows 63 | 64 | ??? 65 | 66 | ** 67 | 68 | 'T' SMFIC_DATA DATA 69 | 70 | Called when the client uses the DATA command. 71 | 72 | ** 73 | 74 | 'U' SMFIC_UNKNOWN Any unknown command (?) 75 | 76 | ??? 77 | 78 | ---------------- 79 | Response codes 80 | 81 | ** 82 | 83 | '2' SMFIR_ADDRCPT_PAR Add recipient with ESMTP args 84 | 85 | char args[][] Array of strings, NUL terminated (address at index 0) 86 | args[0] is recipient, with <> qualification. 87 | args[1] and beyond are ESMTP arguments, if any 88 | 89 | ** 90 | 91 | 'e' SMFIR_CHGFROM Replace envelope from address 92 | 93 | char args[][] Array of strings, NUL terminated (address at index 0) 94 | args[0] is sender, with <> qualification. 95 | args[1] and beyond are ESMTP arguments, if any 96 | 97 | ** 98 | 99 | 'f' SMFIR_CONN_FAIL Cause a connection failure 100 | 101 | ??? 102 | 103 | ** 104 | 105 | 'i' SMFIR_INSHEADER Insert header at a specified position (modification action) 106 | 107 | uint32 index Index into header list where insertion should occur 108 | char name[] Name of header, NUL terminated 109 | char value[] Value of header, NUL terminated 110 | 111 | ** 112 | 113 | 'l' SMFIR_SETSYMLIST Set list of symbols (macros) 114 | 115 | ??? 116 | 117 | ** 118 | 119 | 's' SMFIR_SKIP Do not send more body chunks 120 | 121 | A milter has received sufficiently many body chunks to make a decision, but still 122 | wants to perform message modification functions that are only allowed to be 123 | returned in response to SMFIC_BODYEOB. 124 | 125 | 126 | -------------------------------------------------------------------------------- /cmd/milter-check/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "log" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/emersion/go-message/textproto" 12 | "github.com/emersion/go-milter" 13 | ) 14 | 15 | func printAction(prefix string, act *milter.Action) { 16 | switch act.Code { 17 | case milter.ActAccept: 18 | log.Println(prefix, "accept") 19 | case milter.ActReject: 20 | log.Println(prefix, "reject") 21 | case milter.ActDiscard: 22 | log.Println(prefix, "discard") 23 | case milter.ActTempFail: 24 | log.Println(prefix, "temp. fail") 25 | case milter.ActReplyCode: 26 | log.Println(prefix, "reply code:", act.SMTPCode, act.SMTPText) 27 | case milter.ActContinue: 28 | log.Println(prefix, "continue") 29 | } 30 | } 31 | 32 | func printModifyAction(act milter.ModifyAction) { 33 | switch act.Code { 34 | case milter.ActAddHeader: 35 | log.Printf("add header: name %s, value %s", act.HeaderName, act.HeaderValue) 36 | case milter.ActInsertHeader: 37 | log.Printf("insert header: at %d, name %s, value %s", act.HeaderIndex, act.HeaderName, act.HeaderValue) 38 | case milter.ActChangeFrom: 39 | log.Printf("change from: %s %v", act.From, act.FromArgs) 40 | case milter.ActChangeHeader: 41 | log.Printf("change header: at %d, name %s, value %s", act.HeaderIndex, act.HeaderName, act.HeaderValue) 42 | case milter.ActReplBody: 43 | log.Println("replace body:", string(act.Body)) 44 | case milter.ActAddRcpt: 45 | log.Println("add rcpt:", act.Rcpt) 46 | case milter.ActDelRcpt: 47 | log.Println("del rcpt:", act.Rcpt) 48 | case milter.ActQuarantine: 49 | log.Println("quarantine:", act.Reason) 50 | } 51 | } 52 | 53 | func main() { 54 | transport := flag.String("transport", "unix", "Transport to use for milter connection, One of 'tcp', 'unix', 'tcp4' or 'tcp6'") 55 | address := flag.String("address", "", "Transport address, path for 'unix', address:port for 'tcp'") 56 | hostname := flag.String("hostname", "localhost", "Value to send in CONNECT message") 57 | family := flag.String("family", string(milter.FamilyInet), "Protocol family to send in CONNECT message") 58 | port := flag.Uint("port", 2525, "Port to send in CONNECT message") 59 | connAddr := flag.String("conn-addr", "127.0.0.1", "Connection address to send in CONNECT message") 60 | helo := flag.String("helo", "localhost", "Value to send in HELO message") 61 | mailFrom := flag.String("from", "foxcpp@example.org", "Value to send in MAIL message") 62 | rcptTo := flag.String("rcpt", "foxcpp@example.com", "Comma-separated list of values for RCPT messages") 63 | actionMask := flag.Uint("actions", 64 | uint(milter.OptChangeBody|milter.OptChangeFrom|milter.OptChangeHeader| 65 | milter.OptAddHeader|milter.OptAddRcpt|milter.OptChangeFrom), 66 | "Bitmask value of actions we allow") 67 | disabledMsgs := flag.Uint("disabled-msgs", 0, "Bitmask of disabled protocol messages") 68 | flag.Parse() 69 | 70 | c := milter.NewClientWithOptions(*transport, *address, milter.ClientOptions{ 71 | ActionMask: milter.OptAction(*actionMask), 72 | ProtocolMask: milter.OptProtocol(*disabledMsgs), 73 | ReadTimeout: 10 * time.Second, 74 | WriteTimeout: 10 * time.Second, 75 | }) 76 | defer c.Close() 77 | 78 | s, err := c.Session() 79 | if err != nil { 80 | log.Println(err) 81 | return 82 | } 83 | defer s.Close() 84 | 85 | act, err := s.Conn(*hostname, milter.ProtoFamily((*family)[0]), uint16(*port), *connAddr) 86 | if err != nil { 87 | log.Println(err) 88 | return 89 | } 90 | printAction("CONNECT:", act) 91 | if act.Code != milter.ActContinue { 92 | return 93 | } 94 | 95 | act, err = s.Helo(*helo) 96 | if err != nil { 97 | log.Println(err) 98 | return 99 | } 100 | printAction("HELO:", act) 101 | if act.Code != milter.ActContinue { 102 | return 103 | } 104 | 105 | act, err = s.Mail(*mailFrom, nil) 106 | if err != nil { 107 | log.Println(err) 108 | return 109 | } 110 | printAction("MAIL:", act) 111 | if act.Code != milter.ActContinue { 112 | return 113 | } 114 | 115 | for _, rcpt := range strings.Split(*rcptTo, ",") { 116 | act, err = s.Rcpt(rcpt, nil) 117 | if err != nil { 118 | log.Println(err) 119 | return 120 | } 121 | printAction("RCPT:", act) 122 | if act.Code != milter.ActContinue { 123 | return 124 | } 125 | } 126 | 127 | bufR := bufio.NewReader(os.Stdin) 128 | hdr, err := textproto.ReadHeader(bufR) 129 | if err != nil { 130 | log.Println("header parse:", err) 131 | return 132 | } 133 | 134 | act, err = s.Header(hdr) 135 | if err != nil { 136 | log.Println(err) 137 | return 138 | } 139 | printAction("HEADER:", act) 140 | if act.Code != milter.ActContinue { 141 | return 142 | } 143 | 144 | modifyActs, act, err := s.BodyReadFrom(bufR) 145 | if err != nil { 146 | log.Println(err) 147 | return 148 | } 149 | for _, act := range modifyActs { 150 | printModifyAction(act) 151 | } 152 | printAction("EOB:", act) 153 | } 154 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | "log" 10 | "net" 11 | "net/textproto" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var errCloseSession = errors.New("Stop current milter processing") 17 | 18 | // milterSession keeps session state during MTA communication 19 | type milterSession struct { 20 | server *Server 21 | actions OptAction 22 | protocol OptProtocol 23 | conn net.Conn 24 | headers textproto.MIMEHeader 25 | macros map[string]string 26 | backend Milter 27 | } 28 | 29 | // ReadPacket reads incoming milter packet 30 | func (c *milterSession) ReadPacket() (*Message, error) { 31 | return readPacket(c.conn, 0) 32 | } 33 | 34 | func readPacket(conn net.Conn, timeout time.Duration) (*Message, error) { 35 | if timeout != 0 { 36 | conn.SetReadDeadline(time.Now().Add(timeout)) 37 | defer conn.SetReadDeadline(time.Time{}) 38 | } 39 | 40 | // read packet length 41 | var length uint32 42 | if err := binary.Read(conn, binary.BigEndian, &length); err != nil { 43 | return nil, err 44 | } 45 | 46 | // read packet data 47 | data := make([]byte, length) 48 | if _, err := io.ReadFull(conn, data); err != nil { 49 | return nil, err 50 | } 51 | 52 | // prepare response data 53 | message := Message{ 54 | Code: data[0], 55 | Data: data[1:], 56 | } 57 | 58 | return &message, nil 59 | } 60 | 61 | // WritePacket sends a milter response packet to socket stream 62 | func (m *milterSession) WritePacket(msg *Message) error { 63 | return writePacket(m.conn, msg, 0) 64 | } 65 | 66 | func writePacket(conn net.Conn, msg *Message, timeout time.Duration) error { 67 | if timeout != 0 { 68 | conn.SetWriteDeadline(time.Now().Add(timeout)) 69 | defer conn.SetWriteDeadline(time.Time{}) 70 | } 71 | 72 | buffer := bufio.NewWriter(conn) 73 | 74 | // calculate and write response length 75 | length := uint32(len(msg.Data) + 1) 76 | if err := binary.Write(buffer, binary.BigEndian, length); err != nil { 77 | return err 78 | } 79 | 80 | // write response code 81 | if err := buffer.WriteByte(msg.Code); err != nil { 82 | return err 83 | } 84 | 85 | // write response data 86 | if _, err := buffer.Write(msg.Data); err != nil { 87 | return err 88 | } 89 | 90 | // flush data to network socket stream 91 | if err := buffer.Flush(); err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // Process processes incoming milter commands 99 | func (m *milterSession) Process(msg *Message) (Response, error) { 100 | switch Code(msg.Code) { 101 | case CodeAbort: 102 | // abort current message and start over 103 | defer func() { 104 | m.headers = nil 105 | m.macros = nil 106 | }() 107 | return nil, m.backend.Abort(newModifier(m)) 108 | 109 | case CodeBody: 110 | // body chunk 111 | return m.backend.BodyChunk(msg.Data, newModifier(m)) 112 | 113 | case CodeConn: 114 | // new connection, get hostname 115 | hostname := readCString(msg.Data) 116 | msg.Data = msg.Data[len(hostname)+1:] 117 | // get protocol family 118 | protocolFamily := msg.Data[0] 119 | msg.Data = msg.Data[1:] 120 | // get port 121 | var port uint16 122 | if protocolFamily == '4' || protocolFamily == '6' { 123 | if len(msg.Data) < 2 { 124 | return RespTempFail, nil 125 | } 126 | port = binary.BigEndian.Uint16(msg.Data) 127 | msg.Data = msg.Data[2:] 128 | } 129 | // get address 130 | address := readCString(msg.Data) 131 | // convert address and port to human readable string 132 | family := map[byte]string{ 133 | 'U': "unknown", 134 | 'L': "unix", 135 | '4': "tcp4", 136 | '6': "tcp6", 137 | } 138 | // run handler and return 139 | return m.backend.Connect( 140 | hostname, 141 | family[protocolFamily], 142 | port, 143 | net.ParseIP(address), 144 | newModifier(m)) 145 | 146 | case CodeMacro: 147 | // define macros 148 | m.macros = make(map[string]string) 149 | // convert data to Go strings 150 | data := decodeCStrings(msg.Data[1:]) 151 | if len(data) != 0 { 152 | if len(data)%2 == 1 { 153 | data = append(data, "") 154 | } 155 | 156 | // store data in a map 157 | for i := 0; i < len(data); i += 2 { 158 | m.macros[data[i]] = data[i+1] 159 | } 160 | } 161 | // do not send response 162 | return nil, nil 163 | 164 | case CodeEOB: 165 | // call and return milter handler 166 | return m.backend.Body(newModifier(m)) 167 | 168 | case CodeHelo: 169 | // helo command 170 | name := strings.TrimSuffix(string(msg.Data), null) 171 | return m.backend.Helo(name, newModifier(m)) 172 | 173 | case CodeHeader: 174 | // make sure headers is initialized 175 | if m.headers == nil { 176 | m.headers = make(textproto.MIMEHeader) 177 | } 178 | // add new header to headers map 179 | headerData := decodeCStrings(msg.Data) 180 | // headers with an empty body appear as `text\x00\x00`, decodeCStrings will drop the empty body 181 | if len(headerData) == 1 { 182 | headerData = append(headerData, "") 183 | } 184 | if len(headerData) == 2 { 185 | m.headers.Add(headerData[0], headerData[1]) 186 | // call and return milter handler 187 | return m.backend.Header(headerData[0], headerData[1], newModifier(m)) 188 | } 189 | 190 | case CodeMail: 191 | // envelope from address 192 | from := readCString(msg.Data) 193 | return m.backend.MailFrom(strings.Trim(from, "<>"), newModifier(m)) 194 | 195 | case CodeEOH: 196 | // end of headers 197 | return m.backend.Headers(m.headers, newModifier(m)) 198 | 199 | case CodeOptNeg: 200 | // ignore request and prepare response buffer 201 | var buffer bytes.Buffer 202 | // prepare response data 203 | for _, value := range []uint32{serverProtocolVersion, uint32(m.actions), uint32(m.protocol)} { 204 | if err := binary.Write(&buffer, binary.BigEndian, value); err != nil { 205 | return nil, err 206 | } 207 | } 208 | // build and send packet 209 | return NewResponse('O', buffer.Bytes()), nil 210 | 211 | case CodeQuit: 212 | // client requested session close 213 | return nil, errCloseSession 214 | 215 | case CodeRcpt: 216 | // envelope to address 217 | to := readCString(msg.Data) 218 | return m.backend.RcptTo(strings.Trim(to, "<>"), newModifier(m)) 219 | 220 | case CodeData: 221 | // data, ignore 222 | 223 | default: 224 | // print error and close session 225 | log.Printf("Unrecognized command code: %c", msg.Code) 226 | return nil, errCloseSession 227 | } 228 | 229 | // by default continue with next milter message 230 | return RespContinue, nil 231 | } 232 | 233 | // HandleMilterComands processes all milter commands in the same connection 234 | func (m *milterSession) HandleMilterCommands() { 235 | defer m.conn.Close() 236 | 237 | for { 238 | msg, err := m.ReadPacket() 239 | if err != nil { 240 | if err != io.EOF { 241 | log.Printf("Error reading milter command: %v", err) 242 | } 243 | return 244 | } 245 | 246 | resp, err := m.Process(msg) 247 | if err != nil { 248 | if err != errCloseSession { 249 | // log error condition 250 | log.Printf("Error performing milter command: %v", err) 251 | } 252 | return 253 | } 254 | 255 | // ignore empty responses 256 | if resp != nil { 257 | // send back response message 258 | if err = m.WritePacket(resp.Response()); err != nil { 259 | log.Printf("Error writing packet: %v", err) 260 | return 261 | } 262 | 263 | if !resp.Continue() { 264 | // prepare backend for next message 265 | m.backend = m.server.NewMilter() 266 | } 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | nettextproto "net/textproto" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/emersion/go-message/textproto" 11 | ) 12 | 13 | type MockMilter struct { 14 | ConnResp Response 15 | ConnMod func(m *Modifier) 16 | ConnErr error 17 | 18 | HeloResp Response 19 | HeloMod func(m *Modifier) 20 | HeloErr error 21 | 22 | MailResp Response 23 | MailMod func(m *Modifier) 24 | MailErr error 25 | 26 | RcptResp Response 27 | RcptMod func(m *Modifier) 28 | RcptErr error 29 | 30 | HdrResp Response 31 | HdrMod func(m *Modifier) 32 | HdrErr error 33 | 34 | HdrsResp Response 35 | HdrsMod func(m *Modifier) 36 | HdrsErr error 37 | 38 | BodyChunkResp Response 39 | BodyChunkMod func(m *Modifier) 40 | BodyChunkErr error 41 | 42 | BodyResp Response 43 | BodyMod func(m *Modifier) 44 | BodyErr error 45 | 46 | AbortMod func(m *Modifier) 47 | AbortErr error 48 | 49 | // Info collected during calls. 50 | Host string 51 | Family string 52 | Port uint16 53 | Addr net.IP 54 | 55 | HeloValue string 56 | From string 57 | Rcpt []string 58 | Hdr nettextproto.MIMEHeader 59 | 60 | Chunks [][]byte 61 | } 62 | 63 | func (mm *MockMilter) Connect(host string, family string, port uint16, addr net.IP, m *Modifier) (Response, error) { 64 | if mm.ConnMod != nil { 65 | mm.ConnMod(m) 66 | } 67 | mm.Host = host 68 | mm.Family = family 69 | mm.Port = port 70 | mm.Addr = addr 71 | return mm.ConnResp, mm.ConnErr 72 | } 73 | 74 | func (mm *MockMilter) Helo(name string, m *Modifier) (Response, error) { 75 | if mm.HeloMod != nil { 76 | mm.HeloMod(m) 77 | } 78 | mm.HeloValue = name 79 | return mm.HeloResp, mm.HeloErr 80 | } 81 | 82 | func (mm *MockMilter) MailFrom(from string, m *Modifier) (Response, error) { 83 | if mm.MailMod != nil { 84 | mm.MailMod(m) 85 | } 86 | mm.From = from 87 | return mm.MailResp, mm.MailErr 88 | } 89 | 90 | func (mm *MockMilter) RcptTo(rcptTo string, m *Modifier) (Response, error) { 91 | if mm.RcptMod != nil { 92 | mm.RcptMod(m) 93 | } 94 | mm.Rcpt = append(mm.Rcpt, rcptTo) 95 | return mm.RcptResp, mm.RcptErr 96 | } 97 | 98 | func (mm *MockMilter) Header(name string, value string, m *Modifier) (Response, error) { 99 | if mm.HdrMod != nil { 100 | mm.HdrMod(m) 101 | } 102 | return mm.HdrResp, mm.HdrErr 103 | } 104 | 105 | func (mm *MockMilter) Headers(h nettextproto.MIMEHeader, m *Modifier) (Response, error) { 106 | if mm.HdrsMod != nil { 107 | mm.HdrsMod(m) 108 | } 109 | mm.Hdr = h 110 | return mm.HdrsResp, mm.HdrsErr 111 | } 112 | 113 | func (mm *MockMilter) BodyChunk(chunk []byte, m *Modifier) (Response, error) { 114 | if mm.BodyChunkMod != nil { 115 | mm.BodyChunkMod(m) 116 | } 117 | mm.Chunks = append(mm.Chunks, chunk) 118 | return mm.BodyChunkResp, mm.BodyChunkErr 119 | } 120 | 121 | func (mm *MockMilter) Body(m *Modifier) (Response, error) { 122 | if mm.BodyMod != nil { 123 | mm.BodyMod(m) 124 | } 125 | return mm.BodyResp, mm.BodyErr 126 | } 127 | 128 | func (mm *MockMilter) Abort(m *Modifier) error { 129 | if mm.AbortMod != nil { 130 | mm.AbortMod(m) 131 | } 132 | return mm.AbortErr 133 | } 134 | 135 | func TestMilterClient_UsualFlow(t *testing.T) { 136 | mm := MockMilter{ 137 | ConnResp: RespContinue, 138 | HeloResp: RespContinue, 139 | MailResp: RespContinue, 140 | RcptResp: RespContinue, 141 | HdrResp: RespContinue, 142 | HdrsResp: RespContinue, 143 | BodyChunkResp: RespContinue, 144 | BodyResp: RespContinue, 145 | BodyMod: func(m *Modifier) { 146 | m.AddHeader("X-Bad", "very") 147 | m.ChangeHeader(1, "Subject", "***SPAM***") 148 | m.Quarantine("very bad message") 149 | }, 150 | } 151 | s := Server{ 152 | NewMilter: func() Milter { 153 | return &mm 154 | }, 155 | Actions: OptAddHeader | OptChangeHeader, 156 | } 157 | defer s.Close() 158 | local, err := net.Listen("tcp", "127.0.0.1:0") 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | go s.Serve(local) 163 | 164 | cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{ 165 | ActionMask: OptAddHeader | OptChangeHeader | OptQuarantine, 166 | }) 167 | defer cl.Close() 168 | session, err := cl.Session() 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | defer session.Close() 173 | 174 | assertAction := func(act *Action, err error, expectCode ActionCode) { 175 | t.Helper() 176 | if err != nil { 177 | t.Fatal(err) 178 | } 179 | if act.Code != expectCode { 180 | t.Fatal("Unexpectedcode:", act.Code) 181 | } 182 | } 183 | 184 | act, err := session.Conn("host", FamilyInet, 25565, "172.0.0.1") 185 | assertAction(act, err, ActContinue) 186 | if mm.Host != "host" { 187 | t.Fatal("Wrong host:", mm.Host) 188 | } 189 | if mm.Family != "tcp4" { 190 | t.Fatal("Wrong family:", mm.Family) 191 | } 192 | if mm.Port != 25565 { 193 | t.Fatal("Wrong port:", mm.Port) 194 | } 195 | if mm.Addr.String() != "172.0.0.1" { 196 | t.Fatal("Wrong IP:", mm.Addr) 197 | } 198 | 199 | if err := session.Macros(CodeHelo, "tls_version", "very old"); err != nil { 200 | t.Fatal("Unexpected error", err) 201 | } 202 | 203 | act, err = session.Helo("helo_host") 204 | assertAction(act, err, ActContinue) 205 | if mm.HeloValue != "helo_host" { 206 | t.Fatal("Wrong helo value:", mm.HeloValue) 207 | } 208 | 209 | act, err = session.Mail("from@example.org", []string{"A=B"}) 210 | assertAction(act, err, ActContinue) 211 | if mm.From != "from@example.org" { 212 | t.Fatal("Wrong MAIL FROM:", mm.From) 213 | } 214 | 215 | act, err = session.Rcpt("to1@example.org", []string{"A=B"}) 216 | assertAction(act, err, ActContinue) 217 | act, err = session.Rcpt("to2@example.org", []string{"A=B"}) 218 | assertAction(act, err, ActContinue) 219 | if !reflect.DeepEqual(mm.Rcpt, []string{"to1@example.org", "to2@example.org"}) { 220 | t.Fatal("Wrong recipients:", mm.Rcpt) 221 | } 222 | 223 | hdr := textproto.Header{} 224 | hdr.Add("From", "from@example.org") 225 | hdr.Add("To", "to@example.org") 226 | hdr.Add("x-empty-header", "") 227 | act, err = session.Header(hdr) 228 | assertAction(act, err, ActContinue) 229 | if len(mm.Hdr) != 3 { 230 | t.Fatal("Unexpected header length:", len(mm.Hdr)) 231 | } 232 | if val := mm.Hdr.Get("From"); val != "from@example.org" { 233 | t.Fatal("Wrong From header:", val) 234 | } 235 | if val := mm.Hdr.Get("To"); val != "to@example.org" { 236 | t.Fatal("Wrong To header:", val) 237 | } 238 | if val := mm.Hdr.Get("x-empty-header"); val != "" { 239 | t.Fatal("Wrong To header:", val) 240 | } 241 | 242 | modifyActs, act, err := session.BodyReadFrom(bytes.NewReader(bytes.Repeat([]byte{'A'}, 128000))) 243 | assertAction(act, err, ActContinue) 244 | 245 | if len(mm.Chunks) != 2 { 246 | t.Fatal("Wrong amount of body chunks received") 247 | } 248 | if len(mm.Chunks[0]) > 65535 { 249 | t.Fatal("Too big first chunk:", len(mm.Chunks[0])) 250 | } 251 | if totalLen := len(mm.Chunks[0]) + len(mm.Chunks[1]); totalLen < 128000 { 252 | t.Fatal("Some body bytes lost:", totalLen) 253 | } 254 | 255 | expected := []ModifyAction{ 256 | { 257 | Code: ActAddHeader, 258 | HeaderName: "X-Bad", 259 | HeaderValue: "very", 260 | }, 261 | { 262 | Code: ActChangeHeader, 263 | HeaderIndex: 1, 264 | HeaderName: "Subject", 265 | HeaderValue: "***SPAM***", 266 | }, 267 | { 268 | Code: ActQuarantine, 269 | Reason: "very bad message", 270 | }, 271 | } 272 | 273 | if !reflect.DeepEqual(modifyActs, expected) { 274 | t.Fatalf("Wrong modify actions, got %+v", modifyActs) 275 | } 276 | } 277 | 278 | func TestMilterClient_AbortFlow(t *testing.T) { 279 | macros := make(map[string]string) 280 | mm := MockMilter{ 281 | ConnResp: RespContinue, 282 | HeloResp: RespContinue, 283 | HeloMod: func(m *Modifier) { 284 | macros = m.Macros 285 | }, 286 | AbortMod: func(m *Modifier) { 287 | macros = m.Macros 288 | }, 289 | } 290 | s := Server{ 291 | NewMilter: func() Milter { 292 | return &mm 293 | }, 294 | Actions: OptAddHeader | OptChangeHeader, 295 | } 296 | defer s.Close() 297 | local, err := net.Listen("tcp", "127.0.0.1:0") 298 | if err != nil { 299 | t.Fatal(err) 300 | } 301 | go s.Serve(local) 302 | 303 | cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{ 304 | ActionMask: OptAddHeader | OptChangeHeader | OptQuarantine, 305 | }) 306 | defer cl.Close() 307 | session, err := cl.Session() 308 | if err != nil { 309 | t.Fatal(err) 310 | } 311 | defer session.Close() 312 | 313 | assertAction := func(act *Action, err error, expectCode ActionCode) { 314 | t.Helper() 315 | if err != nil { 316 | t.Fatal(err) 317 | } 318 | if act.Code != expectCode { 319 | t.Fatal("Unexpectedcode:", act.Code) 320 | } 321 | } 322 | 323 | act, err := session.Conn("host", FamilyInet, 25565, "172.0.0.1") 324 | assertAction(act, err, ActContinue) 325 | if mm.Host != "host" { 326 | t.Fatal("Wrong host:", mm.Host) 327 | } 328 | if mm.Family != "tcp4" { 329 | t.Fatal("Wrong family:", mm.Family) 330 | } 331 | if mm.Port != 25565 { 332 | t.Fatal("Wrong port:", mm.Port) 333 | } 334 | if mm.Addr.String() != "172.0.0.1" { 335 | t.Fatal("Wrong IP:", mm.Addr) 336 | } 337 | 338 | if err := session.Macros(CodeHelo, "tls_version", "very old"); err != nil { 339 | t.Fatal("Unexpected error", err) 340 | } 341 | 342 | act, err = session.Helo("helo_host") 343 | assertAction(act, err, ActContinue) 344 | if mm.HeloValue != "helo_host" { 345 | t.Fatal("Wrong helo value:", mm.HeloValue) 346 | } 347 | if v, ok := macros["tls_version"]; !ok || v != "very old" { 348 | t.Fatal("Wrong tls_version macro value:", v) 349 | } 350 | 351 | err = session.Abort() 352 | if err != nil { 353 | t.Fatal(err) 354 | } 355 | 356 | // Validate macro values are preserved for the abort callback 357 | if v, ok := macros["tls_version"]; !ok || v != "very old" { 358 | t.Fatal("Wrong tls_version macro value: ", v) 359 | } 360 | 361 | act, err = session.Helo("repeated_helo_host") 362 | assertAction(act, err, ActContinue) 363 | if mm.HeloValue != "repeated_helo_host" { 364 | t.Fatal("Wrong helo value:", mm.HeloValue) 365 | } 366 | if len(macros["tls_version"]) != 0 { 367 | t.Fatal("Unexpected macro data:", macros) 368 | } 369 | } 370 | 371 | // TestMilterClient_ImpossibleClientDowngrade tests that the client does not downgrade to v2 372 | // in case of a v6 bit set in the ActionMask. 373 | func TestMilterClient_ImpossibleClientDowngrade(t *testing.T) { 374 | mm := MockMilter{ 375 | ConnResp: RespContinue, 376 | HeloResp: RespContinue, 377 | MailResp: RespContinue, 378 | RcptResp: RespContinue, 379 | HdrResp: RespContinue, 380 | HdrsResp: RespContinue, 381 | BodyChunkResp: RespContinue, 382 | BodyResp: RespContinue, 383 | BodyMod: func(m *Modifier) { 384 | m.AddHeader("X-Bad", "very") 385 | m.ChangeHeader(1, "Subject", "***SPAM***") 386 | m.Quarantine("very bad message") 387 | }, 388 | } 389 | s := Server{ 390 | NewMilter: func() Milter { 391 | return &mm 392 | }, 393 | Actions: OptAddHeader | OptChangeHeader, 394 | } 395 | defer s.Close() 396 | local, err := net.Listen("tcp", "127.0.0.1:0") 397 | if err != nil { 398 | t.Fatal(err) 399 | } 400 | go s.Serve(local) 401 | 402 | cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{ 403 | // OptChangeFrom is only supported by a server with v6, but this server only supports v2. 404 | // This should provoke a downgrade error. 405 | ActionMask: OptAddHeader | OptChangeFrom, 406 | }) 407 | defer cl.Close() 408 | _, err = cl.Session() 409 | if err != ErrUnsupportedMilterVersion { 410 | t.Fatalf("Expected ErrUnsupportedMilterVersion, got %v", err) 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /milter-protocol.txt: -------------------------------------------------------------------------------- 1 | $Id: milter-protocol.txt,v 1.6 2004/08/04 16:27:50 tvierling Exp $ 2 | _______________________________________ 3 | THE SENDMAIL MILTER PROTOCOL, VERSION 2 4 | 5 | ** 6 | 7 | The Sendmail and "libmilter" implementations of the protocol described 8 | herein are: 9 | 10 | Copyright (c) 1999-2002 Sendmail, Inc. and its suppliers. 11 | All rights reserved. 12 | 13 | This document is: 14 | 15 | Copyright (c) 2002-2003, Todd Vierling 16 | All rights reserved. 17 | 18 | Permission is granted to copy or reproduce this document in its entirety 19 | in any medium without charge, provided that the copy or reproduction is 20 | without modification and includes the above copyright notice(s). 21 | 22 | ________ 23 | OVERVIEW 24 | 25 | The date of this document is contained within the "Id" symbolic CVS/RCS 26 | tag present at the top of this document. 27 | 28 | This document describes the Sendmail "milter" mail filtering and 29 | MTA-level mail manipulation protocol, version 2, based on the publicly 30 | available C-language source code to Sendmail, version 8.11.6. 31 | 32 | As of this writing, this protocol document is based on the 33 | implementation of milter in Sendmail 8.11, but has been verified 34 | compatible with Sendmail 8.12. Some Sendmail 8.12 extensions, 35 | determined by flags sent with the SMFIC_OPTNEG command, are not yet 36 | described here. 37 | 38 | Technical terms describing mail transport are used throughout. A reader 39 | should have ample understanding of RFCs 821, 822, 2821, and their 40 | successors, and (for Sendmail MTAs) a cursory understanding of Sendmail 41 | configuration procedures. 42 | 43 | ______ 44 | LEGEND 45 | 46 | All integers are assumed to be in network (big-endian) byte order. 47 | Data items are aligned to a byte boundary, and are not forced to any 48 | larger alignment. 49 | 50 | This document makes use of a mnemonic representation of data structures 51 | as transmitted over a communications endpoint to and from a milter 52 | program. A structure may be represented like the following: 53 | 54 | 'W' SMFIC_HWORLD Hello world packet 55 | uint16 len Length of string 56 | char str[len] Text value 57 | 58 | This structure contains a single byte with the ASCII representation 'W', 59 | a 16-bit network byte order integer, and a character array with the 60 | length given by the "len" integer. Character arrays described in this 61 | fashion are an exact number of bytes, and are not assumed to be NUL 62 | terminated. 63 | 64 | A special data type representation is used here to indicate strings and 65 | arrays of strings using C-language semantics of NUL termination. 66 | 67 | char str[] String, NUL terminated 68 | char array[][] Array of strings, NUL terminated 69 | 70 | Here, "str" is a NUL-terminated string, and subsequent data items are 71 | assumed to be located immediately following the NUL byte. "array" is a 72 | stream of NUL-terminated strings, located immediately following each 73 | other in the stream, leading up to the end of the data structure 74 | (determined by the data packet's size). 75 | 76 | ____________________ 77 | LINK/PACKET PROTOCOL 78 | 79 | The MTA makes a connection to a milter by connecting to an IPC endpoint 80 | (socket), via a stream-based protocol. TCPv4, TCPv6, and "Unix 81 | filesystem" sockets can be used for connection to a milter. 82 | (Configuration of Sendmail to make use of these different endpoint 83 | addressing methods is not described here.) 84 | 85 | Data is transmitted in both directions using a structured packet 86 | protocol. Each packets is comprised of: 87 | 88 | uint32 len Size of data to follow 89 | char cmd Command/response code 90 | char data[len-1] Code-specific data (may be empty) 91 | 92 | The connection can be closed at any time by either side. If closed by 93 | the MTA, the milter program should release all state information for the 94 | previously established connection. If closed by the milter program 95 | without first sending an accept or reject action message, the MTA will 96 | take the default action for any message in progress (configurable to 97 | ignore the milter program, or reject with a 4xx or 5xx error). 98 | 99 | _____________________________ 100 | A TYPICAL MILTER CONVERSATION 101 | 102 | The MTA drives the milter conversation. The milter program sends 103 | responses when (and only when) specified by the particular command code 104 | sent by the MTA. It is an error for a milter either to send a response 105 | packet when not requested, or fail to send a response packet when 106 | requested. The MTA may have limits on the time allowed for a response 107 | packet to be sent. 108 | 109 | The typical lifetime of a milter connection can be viewed as follows: 110 | 111 | MTA Milter 112 | 113 | SMFIC_OPTNEG 114 | SMFIC_OPTNEG 115 | SMFIC_MACRO:'C' 116 | SMFIC_CONNECT 117 | Accept/reject action 118 | SMFIC_MACRO:'H' 119 | SMFIC_HELO 120 | Accept/reject action 121 | SMFIC_MACRO:'M' 122 | SMFIC_MAIL 123 | Accept/reject action 124 | SMFIC_MACRO:'R' 125 | SMFIC_RCPT 126 | Accept/reject action 127 | SMFIC_HEADER (multiple) 128 | Accept/reject action (per SMFIC_HEADER) 129 | SMFIC_EOH 130 | Accept/reject action 131 | SMFIC_BODY (multiple) 132 | Accept/reject action (per SMFIC_BODY) 133 | SMFIC_BODYEOB 134 | Modification action (multiple, may be none) 135 | Accept/reject action 136 | 137 | (Reset state to before SMFIC_MAIL and continue, 138 | unless connection is dropped by MTA) 139 | 140 | Several of these MTA/milter steps can be skipped if requested by the 141 | SMFIC_OPTNEG response packet; see below. 142 | 143 | ____________________ 144 | PROTOCOL NEGOTIATION 145 | 146 | Milters can perform several actions on a SMTP transaction. The following is 147 | a bitmask of possible actions, which may be set by the milter in the 148 | "actions" field of the SMFIC_OPTNEG response packet. (Any action which MAY 149 | be performed by the milter MUST be included in this field.) 150 | 151 | 0x01 SMFIF_ADDHDRS Add headers (SMFIR_ADDHEADER) 152 | 0x02 SMFIF_CHGBODY Change body chunks (SMFIR_REPLBODY) 153 | 0x04 SMFIF_ADDRCPT Add recipients (SMFIR_ADDRCPT) 154 | 0x08 SMFIF_DELRCPT Remove recipients (SMFIR_DELRCPT) 155 | 0x10 SMFIF_CHGHDRS Change or delete headers (SMFIR_CHGHEADER) 156 | 0x20 SMFIF_QUARANTINE Quarantine message (SMFIR_QUARANTINE) 157 | 158 | (XXX: SMFIF_DELRCPT has an impact on how address rewriting affects 159 | addresses sent in the SMFIC_RCPT phase. This will be described in a 160 | future revision of this document.) 161 | 162 | Protocol content can contain only selected parts of the SMTP 163 | transaction. To mask out unwanted parts (saving on "over-the-wire" data 164 | churn), the following can be set in the "protocol" field of the 165 | SMFIC_OPTNEG response packet. 166 | 167 | 0x01 SMFIP_NOCONNECT Skip SMFIC_CONNECT 168 | 0x02 SMFIP_NOHELO Skip SMFIC_HELO 169 | 0x04 SMFIP_NOMAIL Skip SMFIC_MAIL 170 | 0x08 SMFIP_NORCPT Skip SMFIC_RCPT 171 | 0x10 SMFIP_NOBODY Skip SMFIC_BODY 172 | 0x20 SMFIP_NOHDRS Skip SMFIC_HEADER 173 | 0x40 SMFIP_NOEOH Skip SMFIC_EOH 174 | 175 | For backwards-compatible milters, the milter should pay attention to the 176 | "actions" and "protocol" fields of the SMFIC_OPTNEG packet, and mask out 177 | any bits that are not part of the offered protocol content. The MTA may 178 | reject the milter program if any action or protocol bit appears outside 179 | the MTA's offered bitmask. 180 | 181 | _____________ 182 | COMMAND CODES 183 | 184 | The following are commands transmitted from the MTA to the milter 185 | program. The data structures represented occupy the "cmd" and "data" 186 | fields of the packets described above in LINK/PACKET PROTOCOL. (In 187 | other words, the data structures below take up exactly "len" bytes, 188 | including the "cmd" byte.) 189 | 190 | ** 191 | 192 | 'A' SMFIC_ABORT Abort current filter checks 193 | Expected response: NONE 194 | 195 | (Resets internal state of milter program to before SMFIC_HELO, but keeps 196 | the connection open.) 197 | 198 | ** 199 | 200 | 'B' SMFIC_BODY Body chunk 201 | Expected response: Accept/reject action 202 | 203 | char buf[] Up to MILTER_CHUNK_SIZE (65535) bytes 204 | 205 | The buffer is not NUL-terminated. 206 | 207 | The body SHOULD be encoded with CRLF line endings, as if it was being 208 | transmitted over SMTP. In practice existing MTAs and milter clients 209 | will probably accept bare LFs, although at least some will convert CRLF 210 | sequences to LFs. 211 | 212 | (These body chunks can be buffered by the milter for later replacement 213 | via SMFIR_REPLBODY during the SMFIC_BODYEOB phase.) 214 | 215 | ** 216 | 217 | 'C' SMFIC_CONNECT SMTP connection information 218 | Expected response: Accept/reject action 219 | 220 | char hostname[] Hostname, NUL terminated 221 | char family Protocol family (see below) 222 | uint16 port Port number (SMFIA_INET or SMFIA_INET6 only) 223 | char address[] IP address (ASCII) or unix socket path, NUL terminated 224 | 225 | (Sendmail invoked via the command line or via "-bs" will report the 226 | connection as the "Unknown" protocol family.) 227 | 228 | Protocol families used with SMFIC_CONNECT in the "family" field: 229 | 230 | 'U' SMFIA_UNKNOWN Unknown (NOTE: Omits "port" and "host" fields entirely) 231 | 'L' SMFIA_UNIX Unix (AF_UNIX/AF_LOCAL) socket ("port" is 0) 232 | '4' SMFIA_INET TCPv4 connection 233 | '6' SMFIA_INET6 TCPv6 connection 234 | 235 | ** 236 | 237 | 'D' SMFIC_MACRO Define macros 238 | Expected response: NONE 239 | 240 | char cmdcode Command for which these macros apply 241 | char nameval[][] Array of NUL-terminated strings, alternating 242 | between name of macro and value of macro. 243 | 244 | SMFIC_MACRO appears as a packet just before the corresponding "cmdcode" 245 | (here), which is the same identifier as the following command. The 246 | names correspond to Sendmail macros, omitting the "$" identifier 247 | character. 248 | 249 | Types of macros, and some commonly supplied macro names, used with 250 | SMFIC_MACRO are as follows, organized by "cmdcode" value. 251 | Implementations SHOULD NOT assume that any of these macros will be 252 | present on a given connection. In particular, communications protocol 253 | information may not be present on the "Unknown" protocol type. 254 | 255 | 'C' SMFIC_CONNECT $_ $j ${daemon_name} ${if_name} ${if_addr} 256 | 257 | 'H' SMFIC_HELO ${tls_version} ${cipher} ${cipher_bits} 258 | ${cert_subject} ${cert_issuer} 259 | 260 | 'M' SMFIC_MAIL $i ${auth_type} ${auth_authen} ${auth_ssf} 261 | ${auth_author} ${mail_mailer} ${mail_host} 262 | ${mail_addr} 263 | 264 | 'R' SMFIC_RCPT ${rcpt_mailer} ${rcpt_host} ${rcpt_addr} 265 | 266 | For future compatibility, implementations MUST allow SMFIC_MACRO at any 267 | time, but the handling of unspecified command codes, or SMFIC_MACRO not 268 | appearing before its specified command, is currently undefined. 269 | 270 | ** 271 | 272 | 'E' SMFIC_BODYEOB End of body marker 273 | Expected response: Zero or more modification 274 | actions, then accept/reject action 275 | 276 | ** 277 | 278 | 'H' SMFIC_HELO HELO/EHLO name 279 | Expected response: Accept/reject action 280 | 281 | char helo[] HELO string, NUL terminated 282 | 283 | ** 284 | 285 | 'L' SMFIC_HEADER Mail header 286 | Expected response: Accept/reject action 287 | 288 | char name[] Name of header, NUL terminated 289 | char value[] Value of header, NUL terminated 290 | 291 | ** 292 | 293 | 'M' SMFIC_MAIL MAIL FROM: information 294 | Expected response: Accept/reject action 295 | 296 | char args[][] Array of strings, NUL terminated (address at index 0). 297 | args[0] is sender, with <> qualification. 298 | args[1] and beyond are ESMTP arguments, if any. 299 | 300 | ** 301 | 302 | 'N' SMFIC_EOH End of headers marker 303 | Expected response: Accept/reject action 304 | 305 | ** 306 | 307 | 'O' SMFIC_OPTNEG Option negotiation 308 | Expected response: SMFIC_OPTNEG packet 309 | 310 | uint32 version SMFI_VERSION (2) 311 | uint32 actions Bitmask of allowed actions from SMFIF_* 312 | uint32 protocol Bitmask of possible protocol content from SMFIP_* 313 | 314 | ** 315 | 316 | 'R' SMFIC_RCPT RCPT TO: information 317 | Expected response: Accept/reject action 318 | 319 | char args[][] Array of strings, NUL terminated (address at index 0). 320 | args[0] is recipient, with <> qualification. 321 | args[1] and beyond are ESMTP arguments, if any. 322 | 323 | ** 324 | 325 | 'Q' SMFIC_QUIT Quit milter communication 326 | Expected response: Close milter connection 327 | 328 | ______________ 329 | RESPONSE CODES 330 | 331 | The following are commands transmitted from the milter program to the 332 | MTA, in response to the appropriate type of command packet. The data 333 | structures represented occupy the "cmd" and "data" fields of the packets 334 | described above in LINK/PACKET PROTOCOL. (In other words, the data 335 | structures below take up exactly "len" bytes, including the "cmd" byte.) 336 | 337 | ** 338 | 339 | Response codes: 340 | 341 | '+' SMFIR_ADDRCPT Add recipient (modification action) 342 | 343 | char rcpt[] New recipient, NUL terminated 344 | 345 | ** 346 | 347 | '-' SMFIR_DELRCPT Remove recipient (modification action) 348 | 349 | char rcpt[] Recipient to remove, NUL terminated 350 | (string must match the one in SMFIC_RCPT exactly) 351 | 352 | ** 353 | 354 | 'a' SMFIR_ACCEPT Accept message completely (accept/reject action) 355 | 356 | (This will skip to the end of the milter sequence, and recycle back to 357 | the state before SMFIC_MAIL. The MTA may, instead, close the connection 358 | at that point.) 359 | 360 | ** 361 | 362 | 'b' SMFIR_REPLBODY Replace body (modification action) 363 | 364 | char buf[] A portion of the body to be replaced 365 | 366 | The buffer is not NUL-terminated. 367 | 368 | As with SMFIC_BODY, the body SHOULD be encoded with CRLF line endings. 369 | Sendmail will convert CRLFs to bare LFs as it receives SMFIR_REPLBODY 370 | responses (even if the CR and LF are split across two responses); the 371 | behavior of other MTAs has not been investigated. 372 | 373 | A milter that uses SMFIR_REPLBODY must replace the entire body, but 374 | it may split the new replacement body across multiple SMFIR_REPLBODY 375 | responses and it may make each response as small as it wants (and 376 | they do not need to correspond one to one with SMFIC_BODY messages). 377 | There is no explicit end of body marker; this role is filled by 378 | whatever accept/reject response the milter finishes with. 379 | 380 | ** 381 | 382 | 'c' SMFIR_CONTINUE Accept and keep processing (accept/reject action) 383 | 384 | (If issued at the end of the milter conversation, functions the same as 385 | SMFIR_ACCEPT.) 386 | 387 | ** 388 | 389 | 'd' SMFIR_DISCARD Set discard flag for entire message (accept/reject action) 390 | 391 | (Note that message processing MAY continue afterwards, but the mail will 392 | not be delivered even if accepted with SMFIR_ACCEPT.) 393 | 394 | ** 395 | 396 | 'h' SMFIR_ADDHEADER Add header (modification action) 397 | 398 | char name[] Name of header, NUL terminated 399 | char value[] Value of header, NUL terminated 400 | 401 | ** 402 | 403 | 'm' SMFIR_CHGHEADER Change header (modification action) 404 | 405 | uint32 index Index of the occurrence of this header 406 | char name[] Name of header, NUL terminated 407 | char value[] Value of header, NUL terminated 408 | 409 | (Note that the "index" above is per-name--i.e. a 3 in this field 410 | indicates that the modification is to be applied to the third such 411 | header matching the supplied "name" field. A zero length string for 412 | "value", leaving only a single NUL byte, indicates that the header 413 | should be deleted entirely.) 414 | 415 | ** 416 | 417 | 'p' SMFIR_PROGRESS Progress (asynchronous action) 418 | 419 | This is an asynchronous response which is sent to the MTA to reset the 420 | communications timer during long operations. The MTA should consume 421 | as many of these responses as are sent, waiting for the real response 422 | for the issued command. 423 | 424 | ** 425 | 426 | 'q' SMFIR_QUARANTINE Quarantine message (modification action) 427 | char reason[] Reason for quarantine, NUL terminated 428 | 429 | This quarantines the message into a holding pool defined by the MTA. 430 | (First implemented in Sendmail in version 8.13; offered to the milter by 431 | the SMFIF_QUARANTINE flag in "actions" of SMFIC_OPTNEG.) 432 | 433 | ** 434 | 435 | 'r' SMFIR_REJECT Reject command/recipient with a 5xx (accept/reject action) 436 | 437 | ** 438 | 439 | 't' SMFIR_TEMPFAIL Reject command/recipient with a 4xx (accept/reject action) 440 | 441 | ** 442 | 443 | 'y' SMFIR_REPLYCODE Send specific Nxx reply message (accept/reject action) 444 | 445 | char smtpcode[3] Nxx code (ASCII), not NUL terminated 446 | char space ' ' 447 | char text[] Text of reply message, NUL terminated 448 | 449 | ('%' characters present in "text" must be doubled to prevent problems 450 | with printf-style formatting that may be used by the MTA.) 451 | 452 | ** 453 | 454 | 'O' SMFIC_OPTNEG Option negotiation (in response to SMFIC_OPTNEG) 455 | 456 | uint32 version SMFI_VERSION (2) 457 | uint32 actions Bitmask of requested actions from SMFIF_* 458 | uint32 protocol Bitmask of undesired protocol content from SMFIP_* 459 | 460 | _______ 461 | CREDITS 462 | 463 | Sendmail, Inc. - for the Sendmail program itself 464 | 465 | The anti-spam community - for making e-mail a usable medium again 466 | 467 | The spam community - for convincing me that it's time to really do 468 | somthing to quell the inflow of their crap 469 | 470 | ___ 471 | EOF 472 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/emersion/go-message/textproto" 13 | ) 14 | 15 | var ErrUnsupportedMilterVersion = fmt.Errorf("milter: negotiate: unsupported milter version") 16 | 17 | // Client is a wrapper for managing milter connections. 18 | // 19 | // Currently, it just creates new connections using provided Dialer. 20 | type Client struct { 21 | opts ClientOptions 22 | network string 23 | address string 24 | } 25 | 26 | type Dialer interface { 27 | Dial(network string, addr string) (net.Conn, error) 28 | } 29 | 30 | type ClientOptions struct { 31 | Dialer Dialer 32 | ReadTimeout time.Duration 33 | WriteTimeout time.Duration 34 | ActionMask OptAction 35 | ProtocolMask OptProtocol 36 | } 37 | 38 | var defaultOptions = ClientOptions{ 39 | Dialer: &net.Dialer{ 40 | Timeout: 10 * time.Second, 41 | }, 42 | ReadTimeout: 10 * time.Second, 43 | WriteTimeout: 10 * time.Second, 44 | ActionMask: OptAddHeader | OptAddRcpt | OptChangeBody | OptChangeFrom | OptChangeHeader, 45 | ProtocolMask: 0, 46 | } 47 | 48 | // NewDefaultClient creates a new Client object using default options. 49 | // 50 | // It uses 10 seconds for connection/read/write timeouts and allows milter to 51 | // send any actions supported by library. 52 | func NewDefaultClient(network, address string) *Client { 53 | return NewClientWithOptions(network, address, defaultOptions) 54 | } 55 | 56 | // NewClientWithOptions creates a new Client object using provided options. 57 | // 58 | // You generally want to use options to restrict ActionMask to what your code 59 | // supports and ProtocolMask to what you intend to submit. 60 | // 61 | // If opts.Dialer is not set, empty net.Dialer object will be used. 62 | func NewClientWithOptions(network, address string, opts ClientOptions) *Client { 63 | if opts.Dialer == nil { 64 | opts.Dialer = &net.Dialer{} 65 | } 66 | 67 | return &Client{ 68 | opts: opts, 69 | network: network, 70 | address: address, 71 | } 72 | } 73 | 74 | func (c *Client) Session() (*ClientSession, error) { 75 | s := &ClientSession{ 76 | readTimeout: c.opts.ReadTimeout, 77 | writeTimeout: c.opts.WriteTimeout, 78 | clientProtocolVersion: 6, 79 | } 80 | 81 | // TODO(foxcpp): Connection pooling. 82 | 83 | conn, err := c.opts.Dialer.Dial(c.network, c.address) 84 | if err != nil { 85 | return nil, fmt.Errorf("milter: session create: %w", err) 86 | } 87 | 88 | s.conn = conn 89 | if err := s.negotiate(c.opts.ActionMask, c.opts.ProtocolMask); err != nil { 90 | return nil, err 91 | } 92 | 93 | return s, nil 94 | } 95 | 96 | func (c *Client) Close() error { 97 | // Reserved for use in connection pooling. 98 | return nil 99 | } 100 | 101 | type ClientSession struct { 102 | conn net.Conn 103 | 104 | // Bitmask of negotiated action options. 105 | ActionOpts OptAction 106 | 107 | // Bitmask of negotiated protocol options. 108 | ProtocolOpts OptProtocol 109 | 110 | needAbort bool 111 | 112 | readTimeout time.Duration 113 | writeTimeout time.Duration 114 | // Milter client version. Can be downgraded during negotiation 115 | clientProtocolVersion uint32 116 | } 117 | 118 | // negotiate exchanges OPTNEG messages with the milter and sets s.mask to the 119 | // negotiated value. 120 | func (s *ClientSession) negotiate(actionMask OptAction, protoMask OptProtocol) error { 121 | // Send our mask, get mask from milter.. 122 | msg := &Message{ 123 | Code: byte(CodeOptNeg), // TODO(foxcpp): Get rid of casts by changing msg.Code to have Code type 124 | Data: make([]byte, 4*3), 125 | } 126 | binary.BigEndian.PutUint32(msg.Data, s.clientProtocolVersion) 127 | binary.BigEndian.PutUint32(msg.Data[4:], uint32(actionMask)) 128 | binary.BigEndian.PutUint32(msg.Data[8:], uint32(protoMask)) 129 | 130 | if err := writePacket(s.conn, msg, s.writeTimeout); err != nil { 131 | return fmt.Errorf("milter: negotiate: optneg write: %w", err) 132 | } 133 | msg, err := readPacket(s.conn, s.readTimeout) 134 | if err != nil { 135 | return fmt.Errorf("milter: negotiate: optneg read: %w", err) 136 | } 137 | if Code(msg.Code) != CodeOptNeg { 138 | return fmt.Errorf("milter: negotiate: unexpected code: %v", rune(msg.Code)) 139 | } 140 | if len(msg.Data) < 4*3 /* version + action mask + proto mask */ { 141 | return fmt.Errorf("milter: negotiate: unexpected data size: %v", len(msg.Data)) 142 | } 143 | 144 | milterVersion := binary.BigEndian.Uint32(msg.Data[:4]) 145 | milterActionMask := binary.BigEndian.Uint32(msg.Data[4:]) 146 | s.ActionOpts = OptAction(milterActionMask) 147 | milterProtoMask := binary.BigEndian.Uint32(msg.Data[8:]) 148 | s.ProtocolOpts = OptProtocol(milterProtoMask) 149 | 150 | // If milter advertises lower protocol version than we support, try to downgrade. 151 | if milterVersion < s.clientProtocolVersion { 152 | // Only downgrade if both sides support the same actions and protocols. 153 | // The lowest supported milterVersion is 2. 154 | if milterVersion >= 2 && actionMask&0x3f == actionMask && protoMask&0x7f == protoMask { 155 | s.clientProtocolVersion = milterVersion 156 | } else { 157 | return ErrUnsupportedMilterVersion 158 | } 159 | } 160 | 161 | s.needAbort = true 162 | 163 | return nil 164 | } 165 | 166 | // ProtocolOption checks whether the option is set in negotiated options, that 167 | // is, requested by both sides. 168 | func (s *ClientSession) ProtocolOption(opt OptProtocol) bool { 169 | return s.ProtocolOpts&opt != 0 170 | } 171 | 172 | // ActionOption checks whether the option is set in negotiated options, that 173 | // is, requested by both sides. 174 | func (s *ClientSession) ActionOption(opt OptAction) bool { 175 | return s.ActionOpts&opt != 0 176 | } 177 | 178 | func (s *ClientSession) Macros(code Code, kv ...string) error { 179 | // Note: kv is ...string with the expectation that the list of macro names 180 | // will be static and not dynamically constructed. 181 | 182 | msg := &Message{ 183 | Code: byte(CodeMacro), 184 | Data: []byte{byte(code)}, 185 | } 186 | for _, str := range kv { 187 | msg.Data = appendCString(msg.Data, str) 188 | } 189 | 190 | if err := writePacket(s.conn, msg, s.writeTimeout); err != nil { 191 | return fmt.Errorf("milter: macros: %w", err) 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func appendUint16(dest []byte, val uint16) []byte { 198 | dest = append(dest, 0x00, 0x00) 199 | binary.BigEndian.PutUint16(dest[len(dest)-2:], val) 200 | return dest 201 | } 202 | 203 | type Action struct { 204 | Code ActionCode 205 | 206 | // SMTP code if Code == ActReplyCode. 207 | SMTPCode int 208 | // Reply text if Code == ActReplyCode. 209 | SMTPText string 210 | } 211 | 212 | func (s *ClientSession) readAction() (*Action, error) { 213 | for { 214 | msg, err := readPacket(s.conn, s.readTimeout) 215 | if err != nil { 216 | return nil, fmt.Errorf("action read: %w", err) 217 | } 218 | if msg.Code == 'p' /* progress */ { 219 | continue 220 | } 221 | if ActionCode(msg.Code) != ActContinue { 222 | s.needAbort = false 223 | } 224 | 225 | return parseAction(msg) 226 | } 227 | } 228 | 229 | func parseAction(msg *Message) (*Action, error) { 230 | act := &Action{ 231 | Code: ActionCode(msg.Code), 232 | } 233 | var err error 234 | 235 | switch ActionCode(msg.Code) { 236 | case ActAccept, ActContinue, ActDiscard, ActReject, ActTempFail: 237 | case ActReplyCode: 238 | if len(msg.Data) <= 4 { 239 | return nil, fmt.Errorf("action read: unexpected data length: %v", len(msg.Data)) 240 | } 241 | act.SMTPCode, err = strconv.Atoi(string(msg.Data[:3])) 242 | if err != nil { 243 | return nil, fmt.Errorf("action read: malformed SMTP code: %v", msg.Data[:3]) 244 | } 245 | // There is 0x20 (' ') in between. 246 | act.SMTPText = readCString(msg.Data[4:]) 247 | default: 248 | return nil, fmt.Errorf("action read: unexpected code: %v", msg.Code) 249 | } 250 | 251 | return act, nil 252 | } 253 | 254 | // Conn sends the connection information to the milter. 255 | // 256 | // It should be called once per milter session (from Session to Close). 257 | func (s *ClientSession) Conn(hostname string, family ProtoFamily, port uint16, addr string) (*Action, error) { 258 | if s.ProtocolOpts&OptNoConnect != 0 { 259 | return &Action{Code: ActContinue}, nil 260 | } 261 | 262 | msg := &Message{ 263 | Code: byte(CodeConn), 264 | } 265 | msg.Data = appendCString(msg.Data, hostname) 266 | msg.Data = append(msg.Data, byte(family)) 267 | if family != FamilyUnknown { 268 | if family == FamilyInet || family == FamilyInet6 { 269 | msg.Data = appendUint16(msg.Data, port) 270 | } 271 | msg.Data = appendCString(msg.Data, addr) 272 | } 273 | 274 | if err := writePacket(s.conn, msg, s.writeTimeout); err != nil { 275 | return nil, fmt.Errorf("milter: conn: %w", err) 276 | } 277 | 278 | if !s.ProtocolOption(OptNoConnReply) { 279 | act, err := s.readAction() 280 | if err != nil { 281 | return nil, fmt.Errorf("milter: conn: %w", err) 282 | } 283 | return act, nil 284 | } 285 | return &Action{Code: ActContinue}, nil 286 | } 287 | 288 | // Helo sends the HELO hostname to the milter. 289 | // 290 | // It should be called once per milter session (from Session to Close). 291 | func (s *ClientSession) Helo(helo string) (*Action, error) { 292 | // Synthesise response as if server replied "go on" while in fact it does 293 | // not support that message. 294 | if s.ProtocolOpts&OptNoHelo != 0 { 295 | return &Action{Code: ActContinue}, nil 296 | } 297 | 298 | msg := &Message{ 299 | Code: byte(CodeHelo), 300 | Data: appendCString(nil, helo), 301 | } 302 | 303 | if err := writePacket(s.conn, msg, s.writeTimeout); err != nil { 304 | return nil, fmt.Errorf("milter: helo: %w", err) 305 | } 306 | 307 | if !s.ProtocolOption(OptNoHeloReply) { 308 | act, err := s.readAction() 309 | if err != nil { 310 | return nil, fmt.Errorf("milter: helo: %w", err) 311 | } 312 | return act, nil 313 | } 314 | return &Action{Code: ActContinue}, nil 315 | } 316 | 317 | func (s *ClientSession) Mail(sender string, esmtpArgs []string) (*Action, error) { 318 | if s.ProtocolOpts&OptNoMailFrom != 0 { 319 | return &Action{Code: ActContinue}, nil 320 | } 321 | 322 | msg := &Message{ 323 | Code: byte(CodeMail), 324 | } 325 | 326 | msg.Data = appendCString(msg.Data, "<"+sender+">") 327 | for _, arg := range esmtpArgs { 328 | msg.Data = appendCString(msg.Data, arg) 329 | } 330 | 331 | if err := writePacket(s.conn, msg, s.writeTimeout); err != nil { 332 | return nil, fmt.Errorf("milter: mail: %w", err) 333 | } 334 | 335 | if !s.ProtocolOption(OptNoMailReply) { 336 | act, err := s.readAction() 337 | if err != nil { 338 | return nil, fmt.Errorf("milter: mail: %w", err) 339 | } 340 | return act, nil 341 | } 342 | return &Action{Code: ActContinue}, nil 343 | } 344 | 345 | func (s *ClientSession) Rcpt(rcpt string, esmtpArgs []string) (*Action, error) { 346 | if s.ProtocolOpts&OptNoRcptTo != 0 { 347 | return &Action{Code: ActContinue}, nil 348 | } 349 | 350 | msg := &Message{ 351 | Code: byte(CodeRcpt), 352 | } 353 | 354 | msg.Data = appendCString(msg.Data, "<"+rcpt+">") 355 | for _, arg := range esmtpArgs { 356 | msg.Data = appendCString(msg.Data, arg) 357 | } 358 | 359 | if err := writePacket(s.conn, msg, s.writeTimeout); err != nil { 360 | return nil, fmt.Errorf("milter: rcpt: %w", err) 361 | } 362 | 363 | if !s.ProtocolOption(OptNoRcptReply) { 364 | act, err := s.readAction() 365 | if err != nil { 366 | return nil, fmt.Errorf("milter: rcpt: %w", err) 367 | } 368 | return act, nil 369 | } 370 | return &Action{Code: ActContinue}, nil 371 | } 372 | 373 | // HeaderField sends a single header field to the milter. 374 | // 375 | // Value should be the original field value without any unfolding applied. 376 | // 377 | // HeaderEnd() must be called after the last field. 378 | func (s *ClientSession) HeaderField(key, value string) (*Action, error) { 379 | if s.ProtocolOpts&OptNoHeaders != 0 { 380 | return &Action{Code: ActContinue}, nil 381 | } 382 | 383 | msg := &Message{ 384 | Code: byte(CodeHeader), 385 | } 386 | msg.Data = appendCString(msg.Data, key) 387 | msg.Data = appendCString(msg.Data, value) 388 | 389 | if err := writePacket(s.conn, msg, s.writeTimeout); err != nil { 390 | return nil, fmt.Errorf("milter: header field: %w", err) 391 | } 392 | 393 | if !s.ProtocolOption(OptNoHeaderReply) { 394 | act, err := s.readAction() 395 | if err != nil { 396 | return nil, fmt.Errorf("milter: header field: %w", err) 397 | } 398 | return act, nil 399 | } 400 | return &Action{Code: ActContinue}, nil 401 | } 402 | 403 | // HeaderEnd send the EOH (End-Of-Header) message to the milter. 404 | // 405 | // No HeaderField calls are allowed after this point. 406 | func (s *ClientSession) HeaderEnd() (*Action, error) { 407 | if s.ProtocolOpts&OptNoEOH != 0 { 408 | return &Action{Code: ActContinue}, nil 409 | } 410 | 411 | if err := writePacket(s.conn, &Message{ 412 | Code: byte(CodeEOH), 413 | }, s.writeTimeout); err != nil { 414 | return nil, fmt.Errorf("milter: header end: %w", err) 415 | } 416 | 417 | if !s.ProtocolOption(OptNoEOHReply) { 418 | act, err := s.readAction() 419 | if err != nil { 420 | return nil, fmt.Errorf("milter: header end: %w", err) 421 | } 422 | return act, nil 423 | } 424 | return &Action{Code: ActContinue}, nil 425 | } 426 | 427 | // Header sends each field from textproto.Header followed by EOH unless 428 | // header messages are disabled during negotiation. 429 | func (s *ClientSession) Header(hdr textproto.Header) (*Action, error) { 430 | for f := hdr.Fields(); f.Next(); { 431 | act, err := s.HeaderField(f.Key(), f.Value()) 432 | if err != nil { 433 | return nil, err 434 | } 435 | 436 | if act.Code != ActContinue { 437 | return act, nil 438 | } 439 | } 440 | 441 | return s.HeaderEnd() 442 | } 443 | 444 | // BodyChunk sends a single body chunk to the milter. 445 | // 446 | // It is callers responsibility to ensure every chunk is not bigger than 447 | // MaxBodyChunk. 448 | // 449 | // If OptSkip was specified during negotiation, caller should be ready to 450 | // handle return ActSkip and stop sending body chunks if it is returned. 451 | func (s *ClientSession) BodyChunk(chunk []byte) (*Action, error) { 452 | if s.ProtocolOpts&OptNoBody != 0 { 453 | return &Action{Code: ActContinue}, nil 454 | } 455 | 456 | // Callers tend to be irresponsible... /s 457 | if len(chunk) > MaxBodyChunk { 458 | return nil, fmt.Errorf("milter: body chunk: too big body chunk: %v", len(chunk)) 459 | } 460 | 461 | if err := writePacket(s.conn, &Message{ 462 | Code: byte(CodeBody), 463 | Data: chunk, 464 | }, s.writeTimeout); err != nil { 465 | return nil, fmt.Errorf("milter: body chunk: %w", err) 466 | } 467 | 468 | if !s.ProtocolOption(OptNoBodyReply) { 469 | act, err := s.readAction() 470 | if err != nil { 471 | return nil, fmt.Errorf("milter: body chunk: %w", err) 472 | } 473 | return act, nil 474 | } 475 | return &Action{Code: ActContinue}, nil 476 | } 477 | 478 | // BodyReadFrom is a helper function that calls BodyChunk repeately to transmit entire 479 | // body from io.Reader and then calls End. 480 | // 481 | // See documentation for these functions for details. 482 | func (s *ClientSession) BodyReadFrom(r io.Reader) ([]ModifyAction, *Action, error) { 483 | // It is problematic to use io.WriteCloser since we may need to report 484 | // action after each write. 485 | 486 | buf := make([]byte, MaxBodyChunk) 487 | for { 488 | n, err := r.Read(buf) 489 | if err != nil { 490 | if err == io.EOF { 491 | break 492 | } 493 | return nil, nil, err 494 | } 495 | if n == 0 { 496 | break 497 | } 498 | 499 | act, err := s.BodyChunk(buf[:n]) 500 | if err != nil { 501 | return nil, nil, err 502 | } 503 | if act.Code == ActSkip { 504 | break 505 | } 506 | if act.Code != ActContinue { 507 | return nil, act, nil 508 | } 509 | } 510 | 511 | return s.End() 512 | } 513 | 514 | type ModifyAction struct { 515 | Code ModifyActCode 516 | 517 | // Recipient to add/remove if Code == ActAddRcpt or ActDelRcpt. 518 | Rcpt string 519 | 520 | // New envelope sender if Code = ActChangeFrom. 521 | From string 522 | 523 | // ESMTP arguments for envelope sender if Code = ActChangeFrom. 524 | FromArgs []string 525 | 526 | // Portion of body to be replaced if Code == ActReplBody. 527 | Body []byte 528 | 529 | // Index of the header field to be changed if Code = ActChangeHeader or Code = ActInsertHeader. 530 | // Index is 1-based and is per value of HdrName. 531 | // E.g. HeaderIndex = 3 and HdrName = "DKIM-Signature" mean "change third 532 | // DKIM-Signature field". Order is the same as of HeaderField calls. 533 | HeaderIndex uint32 534 | 535 | // Header field name to be added/changed if Code == ActAddHeader or 536 | // ActChangeHeader or ActInsertHeader. 537 | HeaderName string 538 | 539 | // Header field value to be added/changed if Code == ActAddHeader or 540 | // ActChangeHeader or ActInsertHeader. If set to empty string - the field 541 | // should be removed. 542 | HeaderValue string 543 | 544 | // Quarantine reason if Code == ActQuarantine. 545 | Reason string 546 | } 547 | 548 | func parseModifyAct(msg *Message) (*ModifyAction, error) { 549 | act := &ModifyAction{ 550 | Code: ModifyActCode(msg.Code), 551 | } 552 | 553 | switch ModifyActCode(msg.Code) { 554 | case ActAddRcpt, ActDelRcpt: 555 | act.Rcpt = readCString(msg.Data) 556 | case ActQuarantine: 557 | act.Reason = readCString(msg.Data) 558 | case ActReplBody: 559 | act.Body = msg.Data 560 | case ActChangeFrom: 561 | argv := bytes.Split(msg.Data, []byte{0x00}) 562 | act.From = string(argv[0]) 563 | for _, arg := range argv[1:] { 564 | act.FromArgs = append(act.FromArgs, string(arg)) 565 | } 566 | case ActChangeHeader, ActInsertHeader: 567 | if len(msg.Data) < 4 { 568 | return nil, fmt.Errorf("read modify action: missing header index") 569 | } 570 | act.HeaderIndex = binary.BigEndian.Uint32(msg.Data) 571 | 572 | msg.Data = msg.Data[4:] 573 | fallthrough 574 | case ActAddHeader: 575 | // TODO: Change readCString to return last index. 576 | act.HeaderName = readCString(msg.Data) 577 | nul := bytes.IndexByte(msg.Data, 0x00) 578 | if nul == -1 { 579 | return nil, fmt.Errorf("read modify action: missing NUL delimiter") 580 | } 581 | if nul == len(msg.Data) { 582 | return nil, fmt.Errorf("read modify action: missing header value") 583 | } 584 | act.HeaderValue = readCString(msg.Data[nul+1:]) 585 | default: 586 | return nil, fmt.Errorf("read modify action: unexpected message code: %v", msg.Code) 587 | } 588 | 589 | return act, nil 590 | } 591 | 592 | func (s *ClientSession) readModifyActs() (modifyActs []ModifyAction, act *Action, err error) { 593 | for { 594 | msg, err := readPacket(s.conn, s.readTimeout) 595 | if err != nil { 596 | return nil, nil, fmt.Errorf("action read: %w", err) 597 | } 598 | if msg.Code == 'p' /* progress */ { 599 | continue 600 | } 601 | 602 | switch ModifyActCode(msg.Code) { 603 | case ActAddRcpt, ActDelRcpt, ActReplBody, ActChangeHeader, ActInsertHeader, 604 | ActAddHeader, ActChangeFrom, ActQuarantine: 605 | modifyAct, err := parseModifyAct(msg) 606 | if err != nil { 607 | return nil, nil, err 608 | } 609 | modifyActs = append(modifyActs, *modifyAct) 610 | default: 611 | act, err = parseAction(msg) 612 | if err != nil { 613 | return nil, nil, err 614 | } 615 | 616 | return modifyActs, act, nil 617 | } 618 | } 619 | } 620 | 621 | // End sends the EOB message and resets session back to the state before Mail 622 | // call. The same ClientSession can be used to check another message arrived 623 | // within the same SMTP connection (Helo and Conn information is preserved). 624 | // 625 | // Close should be called to conclude session. 626 | func (s *ClientSession) End() ([]ModifyAction, *Action, error) { 627 | if err := writePacket(s.conn, &Message{ 628 | Code: byte(CodeEOB), 629 | }, s.writeTimeout); err != nil { 630 | return nil, nil, fmt.Errorf("milter: end: %w", err) 631 | } 632 | 633 | modifyActs, act, err := s.readModifyActs() 634 | if err != nil { 635 | return nil, nil, fmt.Errorf("milter: end: %w", err) 636 | } 637 | 638 | return modifyActs, act, nil 639 | } 640 | 641 | // Abort sends Abort to the milter. 642 | // 643 | // This is called for an unexpected end to an email outside the milters 644 | // control. 645 | func (s *ClientSession) Abort() error { 646 | return writePacket(s.conn, &Message{ 647 | Code: byte(CodeAbort), 648 | }, s.writeTimeout) 649 | } 650 | 651 | // Close releases resources associated with the session. 652 | // 653 | // If there a milter sequence in progress - it is aborted. 654 | func (s *ClientSession) Close() error { 655 | if s.needAbort { 656 | _ = s.Abort() 657 | } 658 | 659 | if err := writePacket(s.conn, &Message{ 660 | Code: byte(CodeQuit), 661 | }, s.writeTimeout); err != nil { 662 | return fmt.Errorf("milter: close: %w", err) 663 | } 664 | return s.conn.Close() 665 | } 666 | --------------------------------------------------------------------------------