test / test (push) Waiting to run
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>
666 lines
17 KiB
Go
666 lines
17 KiB
Go
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()
|
|
}
|