├── go.mod ├── errors.go ├── message.go ├── .gitignore ├── README.md ├── cstrings.go ├── server.go ├── LICENSE ├── interface.go ├── response.go ├── modifier.go ├── session.go └── milter-protocol.txt /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/phalaaxx/milter 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // pre-defined errors 8 | var ( 9 | errCloseSession = errors.New("Stop current milter processing") 10 | errMacroNoData = errors.New("Macro definition with no data") 11 | ) 12 | -------------------------------------------------------------------------------- /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 | // Define milter response codes 10 | const ( 11 | rspAccept = 'a' 12 | rspContinue = 'c' 13 | rspDiscard = 'd' 14 | rspReject = 'r' 15 | rspTempFail = 't' 16 | ) 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/phalaaxx/milter?status.svg)](https://godoc.org/github.com/phalaaxx/milter) 2 | 3 | # milter 4 | A Go library for milter support heavily inspired from https://github.com/andybalholm/milter 5 | For example how to use the library see https://github.com/phalaaxx/pf-milters - postfix milter for email classification with bogofilter and blacklisting messages which contain files with executable extensions. 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // A Go library for milter support 2 | package milter 3 | 4 | import ( 5 | "net" 6 | ) 7 | 8 | // MilterInit initializes milter options 9 | // multiple options can be set using a bitmask 10 | type MilterInit func() (Milter, OptAction, OptProtocol) 11 | 12 | // RunServer provides a convenient way to start a milter server 13 | func RunServer(server net.Listener, init MilterInit) error { 14 | for { 15 | // accept connection from client 16 | client, err := server.Accept() 17 | if err != nil { 18 | return err 19 | } 20 | // create milter object 21 | milter, actions, protocol := init() 22 | session := milterSession{ 23 | actions: actions, 24 | protocol: protocol, 25 | sock: client, 26 | milter: milter, 27 | } 28 | // handle connection commands 29 | go session.HandleMilterCommands() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "net" 5 | "net/textproto" 6 | ) 7 | 8 | // Milter is an interface for milter callback handlers 9 | type Milter interface { 10 | // Connect is called to provide SMTP connection data for incoming message 11 | // suppress with NoConnect 12 | Connect(host string, family string, port uint16, addr net.IP, m *Modifier) (Response, error) 13 | 14 | // Helo is called to process any HELO/EHLO related filters 15 | // suppress with NoHelo 16 | Helo(name string, m *Modifier) (Response, error) 17 | 18 | // MailFrom is called to process filters on envelope FROM address 19 | // suppress with NoMailForm 20 | MailFrom(from string, m *Modifier) (Response, error) 21 | 22 | // RcptTo is called to process filters on envelope TO address 23 | // suppress with NoRcptTo 24 | RcptTo(rcptTo string, m *Modifier) (Response, error) 25 | 26 | // Header is called once for each header in incoming message 27 | // suppress with NoHeaders 28 | Header(name string, value string, m *Modifier) (Response, error) 29 | 30 | // Headers is called when all message headers have been processed 31 | // suppress with NoHeaders 32 | Headers(h textproto.MIMEHeader, m *Modifier) (Response, error) 33 | 34 | // BodyChunk is called to process next message body chunk data (up to 64KB in size) 35 | // suppress with NoBody 36 | BodyChunk(chunk []byte, m *Modifier) (Response, error) 37 | 38 | // Body is called at the end of each message 39 | // all changes to message's content & attributes must be done here 40 | Body(m *Modifier) (Response, error) 41 | } 42 | -------------------------------------------------------------------------------- /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 byte(r) == rspContinue 21 | } 22 | 23 | // Define standard responses with no data 24 | const ( 25 | RespAccept = SimpleResponse(rspAccept) 26 | RespContinue = SimpleResponse(rspContinue) 27 | RespDiscard = SimpleResponse(rspDiscard) 28 | RespReject = SimpleResponse(rspReject) 29 | RespTempFail = SimpleResponse(rspTempFail) 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 []byte{rspAccept, rspDiscard, rspReject, rspTempFail} { 47 | if c.code == 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 | -------------------------------------------------------------------------------- /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 | // Modifier provides access to Macros, Headers and Body data to callback handlers. It also defines a 13 | // number of functions that can be used by callback handlers to modify processing of the email message 14 | type Modifier struct { 15 | Macros map[string]string 16 | Headers textproto.MIMEHeader 17 | 18 | writePacket func(*Message) error 19 | } 20 | 21 | // AddRecipient appends a new envelope recipient for current message 22 | func (m *Modifier) AddRecipient(r string) error { 23 | data := []byte(fmt.Sprintf("<%s>", r) + null) 24 | return m.writePacket(NewResponse('+', data).Response()) 25 | } 26 | 27 | // DeleteRecipient removes an envelope recipient address from message 28 | func (m *Modifier) DeleteRecipient(r string) error { 29 | data := []byte(fmt.Sprintf("<%s>", r) + null) 30 | return m.writePacket(NewResponse('-', data).Response()) 31 | } 32 | 33 | // ReplaceBody substitutes message body with provided body 34 | func (m *Modifier) ReplaceBody(body []byte) error { 35 | return m.writePacket(NewResponse('b', body).Response()) 36 | } 37 | 38 | // AddHeader appends a new email message header the message 39 | func (m *Modifier) AddHeader(name, value string) error { 40 | data := []byte(name + null + value + null) 41 | return m.writePacket(NewResponse('h', data).Response()) 42 | } 43 | 44 | // Quarantine a message by giving a reason to hold it 45 | func (m *Modifier) Quarantine(reason string) error { 46 | return m.writePacket(NewResponse('q', []byte(reason+null)).Response()) 47 | } 48 | 49 | // ChangeHeader replaces the header at the specified position with a new one 50 | func (m *Modifier) ChangeHeader(index int, name, value string) error { 51 | buffer := new(bytes.Buffer) 52 | // encode header index in the beginning 53 | if err := binary.Write(buffer, binary.BigEndian, uint32(index)); err != nil { 54 | return err 55 | } 56 | // add header name and value to buffer 57 | data := []byte(name + null + value + null) 58 | if _, err := buffer.Write(data); err != nil { 59 | return err 60 | } 61 | // prepare and send response packet 62 | return m.writePacket(NewResponse('m', buffer.Bytes()).Response()) 63 | } 64 | 65 | // InsertHeader inserts the header at the specified position 66 | func (m *Modifier) InsertHeader(index int, name, value string) error { 67 | buffer := new(bytes.Buffer) 68 | // encode header index in the beginning 69 | if err := binary.Write(buffer, binary.BigEndian, uint32(index)); err != nil { 70 | return err 71 | } 72 | // add header name and value to buffer 73 | data := []byte(name + null + value + null) 74 | if _, err := buffer.Write(data); err != nil { 75 | return err 76 | } 77 | // prepare and send response packet 78 | return m.writePacket(NewResponse('i', buffer.Bytes()).Response()) 79 | } 80 | 81 | // ChangeFrom replaces the FROM envelope header with a new one 82 | func (m *Modifier) ChangeFrom(value string) error { 83 | buffer := new(bytes.Buffer) 84 | // add header name and value to buffer 85 | data := []byte(value + null) 86 | if _, err := buffer.Write(data); err != nil { 87 | return err 88 | } 89 | // prepare and send response packet 90 | return m.writePacket(NewResponse('e', buffer.Bytes()).Response()) 91 | } 92 | 93 | // newModifier creates a new Modifier instance from milterSession 94 | func newModifier(s *milterSession) *Modifier { 95 | return &Modifier{ 96 | Macros: s.macros, 97 | Headers: s.headers, 98 | writePacket: s.WritePacket, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package milter 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "io" 8 | "log" 9 | "net" 10 | "net/textproto" 11 | "strings" 12 | ) 13 | 14 | // OptAction sets which actions the milter wants to perform. 15 | // Multiple options can be set using a bitmask. 16 | type OptAction uint32 17 | 18 | // OptProtocol masks out unwanted parts of the SMTP transaction. 19 | // Multiple options can be set using a bitmask. 20 | type OptProtocol uint32 21 | 22 | const ( 23 | // set which actions the milter wants to perform 24 | OptAddHeader OptAction = 0x01 25 | OptChangeBody OptAction = 0x02 26 | OptAddRcpt OptAction = 0x04 27 | OptRemoveRcpt OptAction = 0x08 28 | OptChangeHeader OptAction = 0x10 29 | OptQuarantine OptAction = 0x20 30 | OptChangeFrom OptAction = 0x40 31 | 32 | // mask out unwanted parts of the SMTP transaction 33 | OptNoConnect OptProtocol = 0x01 34 | OptNoHelo OptProtocol = 0x02 35 | OptNoMailFrom OptProtocol = 0x04 36 | OptNoRcptTo OptProtocol = 0x08 37 | OptNoBody OptProtocol = 0x10 38 | OptNoHeaders OptProtocol = 0x20 39 | OptNoEOH OptProtocol = 0x40 40 | ) 41 | 42 | // milterSession keeps session state during MTA communication 43 | type milterSession struct { 44 | actions OptAction 45 | protocol OptProtocol 46 | sock io.ReadWriteCloser 47 | headers textproto.MIMEHeader 48 | macros map[string]string 49 | milter Milter 50 | } 51 | 52 | // ReadPacket reads incoming milter packet 53 | func (c *milterSession) ReadPacket() (*Message, error) { 54 | // read packet length 55 | var length uint32 56 | if err := binary.Read(c.sock, binary.BigEndian, &length); err != nil { 57 | return nil, err 58 | } 59 | 60 | // read packet data 61 | data := make([]byte, length) 62 | if _, err := io.ReadFull(c.sock, data); err != nil { 63 | return nil, err 64 | } 65 | 66 | // prepare response data 67 | message := Message{ 68 | Code: data[0], 69 | Data: data[1:], 70 | } 71 | 72 | return &message, nil 73 | } 74 | 75 | // WritePacket sends a milter response packet to socket stream 76 | func (m *milterSession) WritePacket(msg *Message) error { 77 | buffer := bufio.NewWriter(m.sock) 78 | 79 | // calculate and write response length 80 | length := uint32(len(msg.Data) + 1) 81 | if err := binary.Write(buffer, binary.BigEndian, length); err != nil { 82 | return err 83 | } 84 | 85 | // write response code 86 | if err := buffer.WriteByte(msg.Code); err != nil { 87 | return err 88 | } 89 | 90 | // write response data 91 | if _, err := buffer.Write(msg.Data); err != nil { 92 | return err 93 | } 94 | 95 | // flush data to network socket stream 96 | if err := buffer.Flush(); err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | // Process processes incoming milter commands 104 | func (m *milterSession) Process(msg *Message) (Response, error) { 105 | switch msg.Code { 106 | case 'A': 107 | // abort current message and start over 108 | m.headers = nil 109 | m.macros = nil 110 | // do not send response 111 | return nil, nil 112 | 113 | case 'B': 114 | // body chunk 115 | return m.milter.BodyChunk(msg.Data, newModifier(m)) 116 | 117 | case 'C': 118 | // new connection, get hostname 119 | Hostname := readCString(msg.Data) 120 | msg.Data = msg.Data[len(Hostname)+1:] 121 | // get protocol family 122 | protocolFamily := msg.Data[0] 123 | msg.Data = msg.Data[1:] 124 | // get port 125 | var Port uint16 126 | if protocolFamily == '4' || protocolFamily == '6' { 127 | if len(msg.Data) < 2 { 128 | return RespTempFail, nil 129 | } 130 | Port = binary.BigEndian.Uint16(msg.Data) 131 | msg.Data = msg.Data[2:] 132 | if protocolFamily == '6' { 133 | // trim IPv6 prefix when necessary 134 | Prefix := []byte("IPv6:") 135 | if bytes.HasPrefix(msg.Data, Prefix) { 136 | msg.Data = bytes.TrimPrefix(msg.Data, Prefix) 137 | } 138 | } 139 | } 140 | // get address 141 | Address := readCString(msg.Data) 142 | // convert address and port to human readable string 143 | family := map[byte]string{ 144 | 'U': "unknown", 145 | 'L': "unix", 146 | '4': "tcp4", 147 | '6': "tcp6", 148 | } 149 | // run handler and return 150 | return m.milter.Connect( 151 | Hostname, 152 | family[protocolFamily], 153 | Port, 154 | net.ParseIP(Address), 155 | newModifier(m)) 156 | 157 | case 'D': 158 | // define macros 159 | m.macros = make(map[string]string) 160 | // convert data to Go strings 161 | data := decodeCStrings(msg.Data[1:]) 162 | if len(data) != 0 { 163 | // store data in a map 164 | for i := 0; i < len(data); i += 2 { 165 | m.macros[data[i]] = data[i+1] 166 | } 167 | } 168 | // do not send response 169 | return nil, nil 170 | 171 | case 'E': 172 | // call and return milter handler 173 | return m.milter.Body(newModifier(m)) 174 | 175 | case 'H': 176 | // helo command 177 | name := strings.TrimSuffix(string(msg.Data), null) 178 | return m.milter.Helo(name, newModifier(m)) 179 | 180 | case 'L': 181 | // make sure headers is initialized 182 | if m.headers == nil { 183 | m.headers = make(textproto.MIMEHeader) 184 | } 185 | // add new header to headers map 186 | HeaderData := decodeCStrings(msg.Data) 187 | if len(HeaderData) != 0 { 188 | headerDataStr := "" 189 | if len(HeaderData) == 2 { 190 | headerDataStr = HeaderData[1] 191 | } 192 | m.headers.Add(HeaderData[0], headerDataStr) 193 | // call and return milter handler 194 | return m.milter.Header(HeaderData[0], headerDataStr, newModifier(m)) 195 | } 196 | 197 | case 'M': 198 | // envelope from address 199 | envfrom := readCString(msg.Data) 200 | return m.milter.MailFrom(strings.Trim(envfrom, "<>"), newModifier(m)) 201 | 202 | case 'N': 203 | // end of headers 204 | return m.milter.Headers(m.headers, newModifier(m)) 205 | 206 | case 'O': 207 | // ignore request and prepare response buffer 208 | buffer := new(bytes.Buffer) 209 | // prepare response data 210 | for _, value := range []uint32{2, uint32(m.actions), uint32(m.protocol)} { 211 | if err := binary.Write(buffer, binary.BigEndian, value); err != nil { 212 | return nil, err 213 | } 214 | } 215 | // build and send packet 216 | return NewResponse('O', buffer.Bytes()), nil 217 | 218 | case 'Q': 219 | // client requested session close 220 | return nil, errCloseSession 221 | 222 | case 'R': 223 | // envelope to address 224 | envto := readCString(msg.Data) 225 | return m.milter.RcptTo(strings.Trim(envto, "<>"), newModifier(m)) 226 | 227 | case 'T': 228 | // data, ignore 229 | 230 | default: 231 | // print error and close session 232 | log.Printf("Unrecognized command code: %c", msg.Code) 233 | return nil, errCloseSession 234 | } 235 | 236 | // by default continue with next milter message 237 | return RespContinue, nil 238 | } 239 | 240 | // HandleMilterComands processes all milter commands in the same connection 241 | func (m *milterSession) HandleMilterCommands() { 242 | // close session socket on exit 243 | defer m.sock.Close() 244 | 245 | for { 246 | // ReadPacket 247 | msg, err := m.ReadPacket() 248 | if err != nil { 249 | if err != io.EOF { 250 | log.Printf("Error reading milter command: %v", err) 251 | } 252 | return 253 | } 254 | 255 | // process command 256 | resp, err := m.Process(msg) 257 | if err != nil { 258 | if err != errCloseSession { 259 | // log error condition 260 | log.Printf("Error performing milter command: %v", err) 261 | } 262 | return 263 | } 264 | 265 | // ignore empty responses 266 | if resp != nil { 267 | // send back response message 268 | if err = m.WritePacket(resp.Response()); err != nil { 269 | log.Printf("Error writing packet: %v", err) 270 | return 271 | } 272 | 273 | if !resp.Continue() { 274 | return 275 | } 276 | 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------