The security documentation's last open item of panel-dkim-signing (the owner's decision, 2026-10-16): go-milter v0.4.1 added every header of a session to a map of its own, emptied only on ABORT, and handed it to Headers - which no milter of the panel reads. Postfix bounds one header, not their number, so a message of nothing but headers up to the message size limit was held whole by the library in the sender check, the signer and the journal milter on 465/587 and in the inbound journal milter on port 25, where strangers send; the signer's 1 MiB refusal came after it was all in. third_party/go-milter is v0.4.1 from the module cache (BSD 2-Clause, LICENSE kept) with session.go changed: CodeHeader hands the header to Header and keeps nothing, CodeEOH hands Headers an empty map. PATCHES.md says what and why, and how to take a new upstream version. go.mod replaces the module with it; the Dockerfile copies third_party before "go mod download". TestTheLibraryKeepsNoHeaders holds it through the library's own client over two messages of one connection. Comments, security.md (the accepted risk becomes a note), development.md's library table, the plan and CHANGELOG follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vendored
+665
@@ -0,0 +1,665 @@
|
||||
package milter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
var ErrUnsupportedMilterVersion = fmt.Errorf("milter: negotiate: unsupported milter version")
|
||||
|
||||
// Client is a wrapper for managing milter connections.
|
||||
//
|
||||
// Currently, it just creates new connections using provided Dialer.
|
||||
type Client struct {
|
||||
opts ClientOptions
|
||||
network string
|
||||
address string
|
||||
}
|
||||
|
||||
type Dialer interface {
|
||||
Dial(network string, addr string) (net.Conn, error)
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Dialer Dialer
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
ActionMask OptAction
|
||||
ProtocolMask OptProtocol
|
||||
}
|
||||
|
||||
var defaultOptions = ClientOptions{
|
||||
Dialer: &net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
ActionMask: OptAddHeader | OptAddRcpt | OptChangeBody | OptChangeFrom | OptChangeHeader,
|
||||
ProtocolMask: 0,
|
||||
}
|
||||
|
||||
// NewDefaultClient creates a new Client object using default options.
|
||||
//
|
||||
// It uses 10 seconds for connection/read/write timeouts and allows milter to
|
||||
// send any actions supported by library.
|
||||
func NewDefaultClient(network, address string) *Client {
|
||||
return NewClientWithOptions(network, address, defaultOptions)
|
||||
}
|
||||
|
||||
// NewClientWithOptions creates a new Client object using provided options.
|
||||
//
|
||||
// You generally want to use options to restrict ActionMask to what your code
|
||||
// supports and ProtocolMask to what you intend to submit.
|
||||
//
|
||||
// If opts.Dialer is not set, empty net.Dialer object will be used.
|
||||
func NewClientWithOptions(network, address string, opts ClientOptions) *Client {
|
||||
if opts.Dialer == nil {
|
||||
opts.Dialer = &net.Dialer{}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
opts: opts,
|
||||
network: network,
|
||||
address: address,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Session() (*ClientSession, error) {
|
||||
s := &ClientSession{
|
||||
readTimeout: c.opts.ReadTimeout,
|
||||
writeTimeout: c.opts.WriteTimeout,
|
||||
clientProtocolVersion: 6,
|
||||
}
|
||||
|
||||
// TODO(foxcpp): Connection pooling.
|
||||
|
||||
conn, err := c.opts.Dialer.Dial(c.network, c.address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: session create: %w", err)
|
||||
}
|
||||
|
||||
s.conn = conn
|
||||
if err := s.negotiate(c.opts.ActionMask, c.opts.ProtocolMask); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
// Reserved for use in connection pooling.
|
||||
return nil
|
||||
}
|
||||
|
||||
type ClientSession struct {
|
||||
conn net.Conn
|
||||
|
||||
// Bitmask of negotiated action options.
|
||||
ActionOpts OptAction
|
||||
|
||||
// Bitmask of negotiated protocol options.
|
||||
ProtocolOpts OptProtocol
|
||||
|
||||
needAbort bool
|
||||
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
// Milter client version. Can be downgraded during negotiation
|
||||
clientProtocolVersion uint32
|
||||
}
|
||||
|
||||
// negotiate exchanges OPTNEG messages with the milter and sets s.mask to the
|
||||
// negotiated value.
|
||||
func (s *ClientSession) negotiate(actionMask OptAction, protoMask OptProtocol) error {
|
||||
// Send our mask, get mask from milter..
|
||||
msg := &Message{
|
||||
Code: byte(CodeOptNeg), // TODO(foxcpp): Get rid of casts by changing msg.Code to have Code type
|
||||
Data: make([]byte, 4*3),
|
||||
}
|
||||
binary.BigEndian.PutUint32(msg.Data, s.clientProtocolVersion)
|
||||
binary.BigEndian.PutUint32(msg.Data[4:], uint32(actionMask))
|
||||
binary.BigEndian.PutUint32(msg.Data[8:], uint32(protoMask))
|
||||
|
||||
if err := writePacket(s.conn, msg, s.writeTimeout); err != nil {
|
||||
return fmt.Errorf("milter: negotiate: optneg write: %w", err)
|
||||
}
|
||||
msg, err := readPacket(s.conn, s.readTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("milter: negotiate: optneg read: %w", err)
|
||||
}
|
||||
if Code(msg.Code) != CodeOptNeg {
|
||||
return fmt.Errorf("milter: negotiate: unexpected code: %v", rune(msg.Code))
|
||||
}
|
||||
if len(msg.Data) < 4*3 /* version + action mask + proto mask */ {
|
||||
return fmt.Errorf("milter: negotiate: unexpected data size: %v", len(msg.Data))
|
||||
}
|
||||
|
||||
milterVersion := binary.BigEndian.Uint32(msg.Data[:4])
|
||||
milterActionMask := binary.BigEndian.Uint32(msg.Data[4:])
|
||||
s.ActionOpts = OptAction(milterActionMask)
|
||||
milterProtoMask := binary.BigEndian.Uint32(msg.Data[8:])
|
||||
s.ProtocolOpts = OptProtocol(milterProtoMask)
|
||||
|
||||
// If milter advertises lower protocol version than we support, try to downgrade.
|
||||
if milterVersion < s.clientProtocolVersion {
|
||||
// Only downgrade if both sides support the same actions and protocols.
|
||||
// The lowest supported milterVersion is 2.
|
||||
if milterVersion >= 2 && actionMask&0x3f == actionMask && protoMask&0x7f == protoMask {
|
||||
s.clientProtocolVersion = milterVersion
|
||||
} else {
|
||||
return ErrUnsupportedMilterVersion
|
||||
}
|
||||
}
|
||||
|
||||
s.needAbort = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProtocolOption checks whether the option is set in negotiated options, that
|
||||
// is, requested by both sides.
|
||||
func (s *ClientSession) ProtocolOption(opt OptProtocol) bool {
|
||||
return s.ProtocolOpts&opt != 0
|
||||
}
|
||||
|
||||
// ActionOption checks whether the option is set in negotiated options, that
|
||||
// is, requested by both sides.
|
||||
func (s *ClientSession) ActionOption(opt OptAction) bool {
|
||||
return s.ActionOpts&opt != 0
|
||||
}
|
||||
|
||||
func (s *ClientSession) Macros(code Code, kv ...string) error {
|
||||
// Note: kv is ...string with the expectation that the list of macro names
|
||||
// will be static and not dynamically constructed.
|
||||
|
||||
msg := &Message{
|
||||
Code: byte(CodeMacro),
|
||||
Data: []byte{byte(code)},
|
||||
}
|
||||
for _, str := range kv {
|
||||
msg.Data = appendCString(msg.Data, str)
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, msg, s.writeTimeout); err != nil {
|
||||
return fmt.Errorf("milter: macros: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendUint16(dest []byte, val uint16) []byte {
|
||||
dest = append(dest, 0x00, 0x00)
|
||||
binary.BigEndian.PutUint16(dest[len(dest)-2:], val)
|
||||
return dest
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
Code ActionCode
|
||||
|
||||
// SMTP code if Code == ActReplyCode.
|
||||
SMTPCode int
|
||||
// Reply text if Code == ActReplyCode.
|
||||
SMTPText string
|
||||
}
|
||||
|
||||
func (s *ClientSession) readAction() (*Action, error) {
|
||||
for {
|
||||
msg, err := readPacket(s.conn, s.readTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action read: %w", err)
|
||||
}
|
||||
if msg.Code == 'p' /* progress */ {
|
||||
continue
|
||||
}
|
||||
if ActionCode(msg.Code) != ActContinue {
|
||||
s.needAbort = false
|
||||
}
|
||||
|
||||
return parseAction(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func parseAction(msg *Message) (*Action, error) {
|
||||
act := &Action{
|
||||
Code: ActionCode(msg.Code),
|
||||
}
|
||||
var err error
|
||||
|
||||
switch ActionCode(msg.Code) {
|
||||
case ActAccept, ActContinue, ActDiscard, ActReject, ActTempFail:
|
||||
case ActReplyCode:
|
||||
if len(msg.Data) <= 4 {
|
||||
return nil, fmt.Errorf("action read: unexpected data length: %v", len(msg.Data))
|
||||
}
|
||||
act.SMTPCode, err = strconv.Atoi(string(msg.Data[:3]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action read: malformed SMTP code: %v", msg.Data[:3])
|
||||
}
|
||||
// There is 0x20 (' ') in between.
|
||||
act.SMTPText = readCString(msg.Data[4:])
|
||||
default:
|
||||
return nil, fmt.Errorf("action read: unexpected code: %v", msg.Code)
|
||||
}
|
||||
|
||||
return act, nil
|
||||
}
|
||||
|
||||
// Conn sends the connection information to the milter.
|
||||
//
|
||||
// It should be called once per milter session (from Session to Close).
|
||||
func (s *ClientSession) Conn(hostname string, family ProtoFamily, port uint16, addr string) (*Action, error) {
|
||||
if s.ProtocolOpts&OptNoConnect != 0 {
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
msg := &Message{
|
||||
Code: byte(CodeConn),
|
||||
}
|
||||
msg.Data = appendCString(msg.Data, hostname)
|
||||
msg.Data = append(msg.Data, byte(family))
|
||||
if family != FamilyUnknown {
|
||||
if family == FamilyInet || family == FamilyInet6 {
|
||||
msg.Data = appendUint16(msg.Data, port)
|
||||
}
|
||||
msg.Data = appendCString(msg.Data, addr)
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, msg, s.writeTimeout); err != nil {
|
||||
return nil, fmt.Errorf("milter: conn: %w", err)
|
||||
}
|
||||
|
||||
if !s.ProtocolOption(OptNoConnReply) {
|
||||
act, err := s.readAction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: conn: %w", err)
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
// Helo sends the HELO hostname to the milter.
|
||||
//
|
||||
// It should be called once per milter session (from Session to Close).
|
||||
func (s *ClientSession) Helo(helo string) (*Action, error) {
|
||||
// Synthesise response as if server replied "go on" while in fact it does
|
||||
// not support that message.
|
||||
if s.ProtocolOpts&OptNoHelo != 0 {
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
msg := &Message{
|
||||
Code: byte(CodeHelo),
|
||||
Data: appendCString(nil, helo),
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, msg, s.writeTimeout); err != nil {
|
||||
return nil, fmt.Errorf("milter: helo: %w", err)
|
||||
}
|
||||
|
||||
if !s.ProtocolOption(OptNoHeloReply) {
|
||||
act, err := s.readAction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: helo: %w", err)
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
func (s *ClientSession) Mail(sender string, esmtpArgs []string) (*Action, error) {
|
||||
if s.ProtocolOpts&OptNoMailFrom != 0 {
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
msg := &Message{
|
||||
Code: byte(CodeMail),
|
||||
}
|
||||
|
||||
msg.Data = appendCString(msg.Data, "<"+sender+">")
|
||||
for _, arg := range esmtpArgs {
|
||||
msg.Data = appendCString(msg.Data, arg)
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, msg, s.writeTimeout); err != nil {
|
||||
return nil, fmt.Errorf("milter: mail: %w", err)
|
||||
}
|
||||
|
||||
if !s.ProtocolOption(OptNoMailReply) {
|
||||
act, err := s.readAction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: mail: %w", err)
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
func (s *ClientSession) Rcpt(rcpt string, esmtpArgs []string) (*Action, error) {
|
||||
if s.ProtocolOpts&OptNoRcptTo != 0 {
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
msg := &Message{
|
||||
Code: byte(CodeRcpt),
|
||||
}
|
||||
|
||||
msg.Data = appendCString(msg.Data, "<"+rcpt+">")
|
||||
for _, arg := range esmtpArgs {
|
||||
msg.Data = appendCString(msg.Data, arg)
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, msg, s.writeTimeout); err != nil {
|
||||
return nil, fmt.Errorf("milter: rcpt: %w", err)
|
||||
}
|
||||
|
||||
if !s.ProtocolOption(OptNoRcptReply) {
|
||||
act, err := s.readAction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: rcpt: %w", err)
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
// HeaderField sends a single header field to the milter.
|
||||
//
|
||||
// Value should be the original field value without any unfolding applied.
|
||||
//
|
||||
// HeaderEnd() must be called after the last field.
|
||||
func (s *ClientSession) HeaderField(key, value string) (*Action, error) {
|
||||
if s.ProtocolOpts&OptNoHeaders != 0 {
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
msg := &Message{
|
||||
Code: byte(CodeHeader),
|
||||
}
|
||||
msg.Data = appendCString(msg.Data, key)
|
||||
msg.Data = appendCString(msg.Data, value)
|
||||
|
||||
if err := writePacket(s.conn, msg, s.writeTimeout); err != nil {
|
||||
return nil, fmt.Errorf("milter: header field: %w", err)
|
||||
}
|
||||
|
||||
if !s.ProtocolOption(OptNoHeaderReply) {
|
||||
act, err := s.readAction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: header field: %w", err)
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
// HeaderEnd send the EOH (End-Of-Header) message to the milter.
|
||||
//
|
||||
// No HeaderField calls are allowed after this point.
|
||||
func (s *ClientSession) HeaderEnd() (*Action, error) {
|
||||
if s.ProtocolOpts&OptNoEOH != 0 {
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, &Message{
|
||||
Code: byte(CodeEOH),
|
||||
}, s.writeTimeout); err != nil {
|
||||
return nil, fmt.Errorf("milter: header end: %w", err)
|
||||
}
|
||||
|
||||
if !s.ProtocolOption(OptNoEOHReply) {
|
||||
act, err := s.readAction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: header end: %w", err)
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
// Header sends each field from textproto.Header followed by EOH unless
|
||||
// header messages are disabled during negotiation.
|
||||
func (s *ClientSession) Header(hdr textproto.Header) (*Action, error) {
|
||||
for f := hdr.Fields(); f.Next(); {
|
||||
act, err := s.HeaderField(f.Key(), f.Value())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if act.Code != ActContinue {
|
||||
return act, nil
|
||||
}
|
||||
}
|
||||
|
||||
return s.HeaderEnd()
|
||||
}
|
||||
|
||||
// BodyChunk sends a single body chunk to the milter.
|
||||
//
|
||||
// It is callers responsibility to ensure every chunk is not bigger than
|
||||
// MaxBodyChunk.
|
||||
//
|
||||
// If OptSkip was specified during negotiation, caller should be ready to
|
||||
// handle return ActSkip and stop sending body chunks if it is returned.
|
||||
func (s *ClientSession) BodyChunk(chunk []byte) (*Action, error) {
|
||||
if s.ProtocolOpts&OptNoBody != 0 {
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
// Callers tend to be irresponsible... /s
|
||||
if len(chunk) > MaxBodyChunk {
|
||||
return nil, fmt.Errorf("milter: body chunk: too big body chunk: %v", len(chunk))
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, &Message{
|
||||
Code: byte(CodeBody),
|
||||
Data: chunk,
|
||||
}, s.writeTimeout); err != nil {
|
||||
return nil, fmt.Errorf("milter: body chunk: %w", err)
|
||||
}
|
||||
|
||||
if !s.ProtocolOption(OptNoBodyReply) {
|
||||
act, err := s.readAction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("milter: body chunk: %w", err)
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
return &Action{Code: ActContinue}, nil
|
||||
}
|
||||
|
||||
// BodyReadFrom is a helper function that calls BodyChunk repeately to transmit entire
|
||||
// body from io.Reader and then calls End.
|
||||
//
|
||||
// See documentation for these functions for details.
|
||||
func (s *ClientSession) BodyReadFrom(r io.Reader) ([]ModifyAction, *Action, error) {
|
||||
// It is problematic to use io.WriteCloser since we may need to report
|
||||
// action after each write.
|
||||
|
||||
buf := make([]byte, MaxBodyChunk)
|
||||
for {
|
||||
n, err := r.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
act, err := s.BodyChunk(buf[:n])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if act.Code == ActSkip {
|
||||
break
|
||||
}
|
||||
if act.Code != ActContinue {
|
||||
return nil, act, nil
|
||||
}
|
||||
}
|
||||
|
||||
return s.End()
|
||||
}
|
||||
|
||||
type ModifyAction struct {
|
||||
Code ModifyActCode
|
||||
|
||||
// Recipient to add/remove if Code == ActAddRcpt or ActDelRcpt.
|
||||
Rcpt string
|
||||
|
||||
// New envelope sender if Code = ActChangeFrom.
|
||||
From string
|
||||
|
||||
// ESMTP arguments for envelope sender if Code = ActChangeFrom.
|
||||
FromArgs []string
|
||||
|
||||
// Portion of body to be replaced if Code == ActReplBody.
|
||||
Body []byte
|
||||
|
||||
// Index of the header field to be changed if Code = ActChangeHeader or Code = ActInsertHeader.
|
||||
// Index is 1-based and is per value of HdrName.
|
||||
// E.g. HeaderIndex = 3 and HdrName = "DKIM-Signature" mean "change third
|
||||
// DKIM-Signature field". Order is the same as of HeaderField calls.
|
||||
HeaderIndex uint32
|
||||
|
||||
// Header field name to be added/changed if Code == ActAddHeader or
|
||||
// ActChangeHeader or ActInsertHeader.
|
||||
HeaderName string
|
||||
|
||||
// Header field value to be added/changed if Code == ActAddHeader or
|
||||
// ActChangeHeader or ActInsertHeader. If set to empty string - the field
|
||||
// should be removed.
|
||||
HeaderValue string
|
||||
|
||||
// Quarantine reason if Code == ActQuarantine.
|
||||
Reason string
|
||||
}
|
||||
|
||||
func parseModifyAct(msg *Message) (*ModifyAction, error) {
|
||||
act := &ModifyAction{
|
||||
Code: ModifyActCode(msg.Code),
|
||||
}
|
||||
|
||||
switch ModifyActCode(msg.Code) {
|
||||
case ActAddRcpt, ActDelRcpt:
|
||||
act.Rcpt = readCString(msg.Data)
|
||||
case ActQuarantine:
|
||||
act.Reason = readCString(msg.Data)
|
||||
case ActReplBody:
|
||||
act.Body = msg.Data
|
||||
case ActChangeFrom:
|
||||
argv := bytes.Split(msg.Data, []byte{0x00})
|
||||
act.From = string(argv[0])
|
||||
for _, arg := range argv[1:] {
|
||||
act.FromArgs = append(act.FromArgs, string(arg))
|
||||
}
|
||||
case ActChangeHeader, ActInsertHeader:
|
||||
if len(msg.Data) < 4 {
|
||||
return nil, fmt.Errorf("read modify action: missing header index")
|
||||
}
|
||||
act.HeaderIndex = binary.BigEndian.Uint32(msg.Data)
|
||||
|
||||
msg.Data = msg.Data[4:]
|
||||
fallthrough
|
||||
case ActAddHeader:
|
||||
// TODO: Change readCString to return last index.
|
||||
act.HeaderName = readCString(msg.Data)
|
||||
nul := bytes.IndexByte(msg.Data, 0x00)
|
||||
if nul == -1 {
|
||||
return nil, fmt.Errorf("read modify action: missing NUL delimiter")
|
||||
}
|
||||
if nul == len(msg.Data) {
|
||||
return nil, fmt.Errorf("read modify action: missing header value")
|
||||
}
|
||||
act.HeaderValue = readCString(msg.Data[nul+1:])
|
||||
default:
|
||||
return nil, fmt.Errorf("read modify action: unexpected message code: %v", msg.Code)
|
||||
}
|
||||
|
||||
return act, nil
|
||||
}
|
||||
|
||||
func (s *ClientSession) readModifyActs() (modifyActs []ModifyAction, act *Action, err error) {
|
||||
for {
|
||||
msg, err := readPacket(s.conn, s.readTimeout)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("action read: %w", err)
|
||||
}
|
||||
if msg.Code == 'p' /* progress */ {
|
||||
continue
|
||||
}
|
||||
|
||||
switch ModifyActCode(msg.Code) {
|
||||
case ActAddRcpt, ActDelRcpt, ActReplBody, ActChangeHeader, ActInsertHeader,
|
||||
ActAddHeader, ActChangeFrom, ActQuarantine:
|
||||
modifyAct, err := parseModifyAct(msg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
modifyActs = append(modifyActs, *modifyAct)
|
||||
default:
|
||||
act, err = parseAction(msg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return modifyActs, act, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End sends the EOB message and resets session back to the state before Mail
|
||||
// call. The same ClientSession can be used to check another message arrived
|
||||
// within the same SMTP connection (Helo and Conn information is preserved).
|
||||
//
|
||||
// Close should be called to conclude session.
|
||||
func (s *ClientSession) End() ([]ModifyAction, *Action, error) {
|
||||
if err := writePacket(s.conn, &Message{
|
||||
Code: byte(CodeEOB),
|
||||
}, s.writeTimeout); err != nil {
|
||||
return nil, nil, fmt.Errorf("milter: end: %w", err)
|
||||
}
|
||||
|
||||
modifyActs, act, err := s.readModifyActs()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("milter: end: %w", err)
|
||||
}
|
||||
|
||||
return modifyActs, act, nil
|
||||
}
|
||||
|
||||
// Abort sends Abort to the milter.
|
||||
//
|
||||
// This is called for an unexpected end to an email outside the milters
|
||||
// control.
|
||||
func (s *ClientSession) Abort() error {
|
||||
return writePacket(s.conn, &Message{
|
||||
Code: byte(CodeAbort),
|
||||
}, s.writeTimeout)
|
||||
}
|
||||
|
||||
// Close releases resources associated with the session.
|
||||
//
|
||||
// If there a milter sequence in progress - it is aborted.
|
||||
func (s *ClientSession) Close() error {
|
||||
if s.needAbort {
|
||||
_ = s.Abort()
|
||||
}
|
||||
|
||||
if err := writePacket(s.conn, &Message{
|
||||
Code: byte(CodeQuit),
|
||||
}, s.writeTimeout); err != nil {
|
||||
return fmt.Errorf("milter: close: %w", err)
|
||||
}
|
||||
return s.conn.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user