mailfilter

package
v0.0.0-...-5357b86 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: BSD-2-Clause, BSD-3-Clause Imports: 20 Imported by: 0

Documentation

Overview

Package mailfilter allows you to write milter filters without boilerplate code

Index

Examples

Constants

View Source
const (
	// Accept is a decision that tells the MTA to accept the message (milter.RespAccept).
	Accept decision = "250 accept"
	// Reject is a decision that tells the MTA to reject the message (milter.RespReject).
	Reject decision = "550 5.7.1 Command rejected"
	// TempFail is a decision that tells the MTA to temporarily fail the message (milter.RespTempFail).
	TempFail decision = "451 4.7.1 Service unavailable - try again later"
	// Discard is a decision that tells the MTA to discard the message (milter.RespDiscard).
	// The SMTP client does not get notified about this decision and must assume that the SMTP message was successfully delivered.
	Discard decision = "250 discard"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Connect

type Connect struct {
	Host   string        // The host name the MTA figured out for the remote client.
	Family milter.Family // Unknown, UNIX or TCP
	Unix   string        // If Family "unix", the path to the unix socket, else empty.
	Port   uint16        // If Family is TCP, the remote TCP port of the client connecting to the MTA
	Addr   netip.Addr    // If TCP, the IPv4 or IPv6 address of the remote client connecting to the MTA
	IfName string        // The Name of the network interface the MTA connection was accepted at. Might be empty.
	IfAddr string        // The IP address of the network interface the MTA connection was accepted at. Might be empty.
}

type Decision

type Decision interface {
	// String returns the decision as an SMTP response string.
	// This is useful for testing/logging.
	String() string
	// Equal returns true if the decision is semantically equal to the provided decision.
	// This is useful for testing.
	// As a special case, a QuarantineResponse is equal to Accept.
	Equal(Decision) bool
}

func CustomErrorResponse

func CustomErrorResponse(code uint16, reason string) Decision

CustomErrorResponse can get used to send a custom error response to the client. The code must be between 400 and 599. The reason can contain new-lines and can start with a valid RFC 2034 extended error code. Line ending canonicalization and wrapping is done automatically. SMTP line continuation rules (including RFC 2034 extension) are applied automatically. E.g.:

CustomErrorResponse(550, "5.7.1 Command rejected\nContact support")

will result in this SMTP response:

550-5.7.1 Command rejected\r\n
550 5.7.1 Contact support

func QuarantineResponse

func QuarantineResponse(reason string) Decision

QuarantineResponse can get used to quarantine a message. The message will be accepted but quarantined. You cannot provide extended error codes or multiline responses, since reason will be used as the quarantine reason and will not be passed to the client.

type DecisionAt

type DecisionAt int

DecisionAt defines when the filter decision is made.

const (
	// The DecisionAtConnect constant makes the mail filter call the decision function after the connected event.
	DecisionAtConnect DecisionAt = iota

	// The DecisionAtHelo constant makes the mail filter call the decision function after the HELO/EHLO event.
	DecisionAtHelo

	// The DecisionAtMailFrom constant makes the mail filter call the decision function after the MAIL FROM event.
	DecisionAtMailFrom

	// The DecisionAtData constant makes the mail filter call the decision function after the DATA event (all RCPT TO were sent).
	DecisionAtData

	// The DecisionAtEndOfHeaders constant makes the mail filter call the decision function after the EOH event (all headers were sent).
	DecisionAtEndOfHeaders

	// The DecisionAtEndOfMessage constant makes the mail filter call the decision function at the end of the SMTP transaction.
	// This is the default.
	DecisionAtEndOfMessage
)

type DecisionModificationFunc

type DecisionModificationFunc func(ctx context.Context, trx Trx) (decision Decision, err error)

DecisionModificationFunc is the callback function that you need to implement to create a mail filter.

ctx is a context.Context that might get canceled when the connection to the MTA fails while your callback is running. If your decision function is running longer than one second, the MailFilter automatically sends progress notifications every second so that MTA does not time out the milter connection.

trx is the Trx object that you can inspect to see what the MailFilter got as information about the current SMTP transaction. You can also use trx to modify the transaction (e.g., change recipients, alter headers).

decision is your Decision about this SMTP transaction. Use Accept, TempFail, Reject, Discard, QuarantineResponse, or CustomErrorResponse.

If you return a non-nil error WithErrorHandling will determine what happens with the current SMTP transaction.

type ErrorHandling

type ErrorHandling int
const (
	// Error just throws the error. The connection to the MTA will break, and the MTA will decide what happens to the SMTP transaction.
	Error ErrorHandling = iota
	// AcceptWhenError accepts the transaction despite the error (it gets logged).
	AcceptWhenError
	// TempFailWhenError temporarily rejects the transaction (and logs the error).
	TempFailWhenError
	// RejectWhenError rejects the transaction (and logs the error).
	RejectWhenError
)

type Helo

type Helo struct {
	Name        string // The HELO/EHLO hostname the client provided
	TlsVersion  string // TLSv1.3, TLSv1.2, ... or empty when no STARTTLS was used. Might even be empty when STARTTLS was used (when the MTA does not support the corresponding macro – almost all do).
	Cipher      string // The Cipher that the client and MTA negotiated.
	CipherBits  string // The bits of the cipher used. E.g., 256. Might be "RSA equivalent" bits for e.g., elliptic curve ciphers.
	CertSubject string // If MutualTLS was used for the connection between the client and MTA, this holds the subject of the validated client certificate.
	CertIssuer  string // If MutualTLS was used for the connection between the client and MTA, this holds the subject of the issuer of the client certificate (CA or Sub-CA).
}

type MTA

type MTA struct {
	Version string // value of [milter.MacroMTAVersion] macro
	FQDN    string // value of [milter.MacroMTAFQDN] macro
	Daemon  string // value of [milter.MacroDaemonName] macro
}

func (*MTA) IsSendmail

func (m *MTA) IsSendmail() bool

IsSendmail returns true when MTA.Version looks like a Sendmail version number

type MailFilter

type MailFilter struct {
	// contains filtered or unexported fields
}

func New deprecated

func New(network, address string, decision DecisionModificationFunc, opts ...Option) (*MailFilter, error)

New creates and starts a new MailFilter with a socket listening on network and address. decision is the callback that should implement the filter logic. opts are optional Option functions that configure/fine-tune the mail filter.

Deprecated: Use NewBind, NewUnix or NewMailFilter instead. Is going to be replaced by NewMailFilter soon.

Example
package main

import (
	"context"
	"flag"
	"log"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"catinello.eu/milter/mailfilter"
)

func main() {
	// parse commandline arguments
	var protocol, address string
	flag.StringVar(&protocol, "proto", "tcp", "Protocol family (unix or tcp)")
	flag.StringVar(&address, "addr", "127.0.0.1:10003", "Bind to address or unix domain socket")
	flag.Parse()

	// create and start the mail filter
	filter, err := mailfilter.New(protocol, address,
		func(_ context.Context, trx mailfilter.Trx) (mailfilter.Decision, error) {
			// Quarantine mail when it is addressed to our SPAM trap
			if trx.HasRcptTo("spam-trap@スパム.example.com") {
				return mailfilter.QuarantineResponse("train as spam"), nil
			}
			// Prefix Subject with [⚠️EXTERNAL] when the user is not logged in
			if trx.MailFrom().AuthenticatedUser() == "" {
				subject, _ := trx.Headers().Subject()
				if !strings.HasPrefix(subject, "[⚠️EXTERNAL] ") {
					subject = "[⚠️EXTERNAL] " + subject
					trx.Headers().SetSubject(subject)
				}
			}
			return mailfilter.Accept, nil
		},
		mailfilter.WithRcptToValidator(func(_ context.Context, in *mailfilter.RcptToValidationInput) (mailfilter.Decision, error) {
			if in.MailFrom.UnicodeDomain() == "スパム.example.com" {
				time.Sleep(time.Second * 5) // slow down the spammer
				return mailfilter.CustomErrorResponse(554, "5.7.1 You cannot send from this domain"), nil
			}
			return mailfilter.Accept, nil
		}),
		// Optimization: call the decision function when all headers were sent to us. Modifications get automatically deferred to EndOfHeaders.
		mailfilter.WithDecisionAt(mailfilter.DecisionAtEndOfHeaders),
	)
	if err != nil {
		log.Println(err)
	}
	log.Printf("Started milter on %s:%s", filter.Addr().Network(), filter.Addr().String())

	// wait for SIGINT or SIGTERM
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sig
		log.Printf("Gracefully shutting down milter…")
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		filter.Shutdown(ctx)
	}()
	// wait for the mail filter to end
	filter.Wait()
}

func NewBind

func NewBind(address *bind.Target, decision DecisionModificationFunc, opts ...Option) (*MailFilter, error)

func NewMailFilter

func NewMailFilter(family milter.Family, addr *bind.Target, unix string, decision DecisionModificationFunc, opts ...Option) (*MailFilter, error)

func NewUnix

func NewUnix(unix string, decision DecisionModificationFunc, opts ...Option) (*MailFilter, error)

func (*MailFilter) Addr

func (f *MailFilter) Addr() net.Addr

Addr returns the net.Addr of the listening socket of this MailFilter. This method returns nil when the socket is not set.

func (*MailFilter) Close

func (f *MailFilter) Close()

Close stops the MailFilter server.

func (*MailFilter) MilterCount

func (f *MailFilter) MilterCount() uint64

MilterCount returns the number of milter backends that this MailFilter created in total. A Milter instance gets created for each new connection from the MTA (after successful negotiation). Use this function for logging purposes.

func (*MailFilter) Shutdown

func (f *MailFilter) Shutdown(ctx context.Context) error

Shutdown gracefully stops the MailFilter server.

func (*MailFilter) Wait

func (f *MailFilter) Wait()

Wait waits for the end of the MailFilter server.

type MaxAction

type MaxAction int
const (
	// RejectMessageWhenTooBig rejects the message with "552 5.3.4 Maximum allowed body size of %d bytes exceeded." or
	// "552 5.3.4 Maximum allowed header lines (%d) exceeded."
	RejectMessageWhenTooBig MaxAction = iota
	// ClearWhenTooBig will allow the message to pass, but Trx.Body or Trx.Headers will be empty.
	ClearWhenTooBig
	// TruncateWhenTooBig will allow the message to pass,
	// but Trx.Body will be truncated to only the first maxSize bytes
	// or Trx.Headers will be truncated to only the first maxHeaders headers.
	TruncateWhenTooBig
)

type Option

type Option func(opt *options)

func WithBody

func WithBody(maxMem int, maxSize int64, maxAction MaxAction) Option

WithBody configures the MailFilter body collection. When the message body is bigger than maxMem bytes, the body will be written to a temporary file. Otherwise, it will be kept in memory. If maxSize is bigger than 0, the milter will stop collecting the body after maxSize bytes and use maxAction to determine what happens.

If you do not call this function, the default values are:

  • maxMem: 200 KiB
  • maxSize: 100 MiB
  • maxAction: TruncateWhenTooBig

func WithDecisionAt

func WithDecisionAt(decisionAt DecisionAt) Option

WithDecisionAt sets the decision point for the MailFilter. The default is DecisionAtEndOfMessage. If your decision function also made modifications to the Trx (e.g., added a recipient), the Mailfilter will automatically delay sending your decision to the MTA until milter.Milter.EndOfMessage gets called (after the DATA command finished).

func WithErrorHandling

func WithErrorHandling(errorHandling ErrorHandling) Option

WithErrorHandling sets the error handling for the MailFilter. The default is TempFailWhenError.

func WithHeader

func WithHeader(maxHeaders uint32, maxAction MaxAction) Option

WithHeader sets the maximum number of headers the MailFilter will collect. If the number of headers is bigger than maxHeaders, the milter will use maxAction to determine what happens.

If you do not call this function, the default values are:

  • maxHeaders: 512
  • maxAction: TruncateWhenTooBig

func WithRcptToValidator

func WithRcptToValidator(validator RcptToValidator) Option

WithRcptToValidator set a custom validator function that can be used to reject individual RCPT TO addresses. If you do not set this, all recipient addresses will be accepted. Your decision function can, of course, always remove recipients from the transaction (without notifying the SMTP client).

func WithoutBody

func WithoutBody() Option

WithoutBody configures the MailFilter to not request and collect the mail body. Use this option when you do not need Trx.Body or Trx.Data in your decision function.

type RcptToValidationInput

type RcptToValidationInput struct {
	// MTA holds information about the connecting MTA
	MTA *MTA
	// Connect holds the [Connect] information of this transaction.
	Connect *Connect
	// Helo holds the [Helo] information of this transaction.
	Helo *Helo
	// MailFrom holds the [addr.MailFrom] of this transaction.
	MailFrom *addr.MailFrom
	// RcptTo is the recipient address that needs to be validated
	RcptTo *addr.RcptTo
}

RcptToValidationInput is the input for the RcptToValidator function. It contains information about the current SMTP transaction and the recipient address that needs to be validated. You cannot modify anything in this struct, the validator function receives only copies of the values.

type RcptToValidator

type RcptToValidator func(ctx context.Context, in *RcptToValidationInput) (Decision, error)

RcptToValidator is a function that validates a RCPT TO address. It is called for each RCPT TO address received by the MTA. If the function returns anything other than Accept, the address is rejected. Returning QuarantineResponse is an error. It will silently be treated as Accept. Returning Discard will discard (silently ignore) the whole message. Your decision function will not be called in this case. The function gets passed in a context.Context, it might get canceled when the connection to the MTA fails while your callback is running.

type Trx

type Trx interface {
	// MTA holds information about the connecting MTA
	MTA() *MTA
	// Connect holds the [Connect] information of this transaction.
	Connect() *Connect
	// Helo holds the [Helo] information of this transaction.
	//
	// Only populated if [WithDecisionAt] is bigger than [DecisionAtConnect].
	Helo() *Helo

	// MailFrom holds the [addr.MailFrom] of this transaction.
	// Your changes to this pointer's Addr and Args values get send back to the MTA.
	//
	// Only populated if [WithDecisionAt] is bigger than [DecisionAtHelo].
	MailFrom() *addr.MailFrom
	// ChangeMailFrom changes the MailFrom Addr and Args.
	// This is just a convenience method, you could also directly change the MailFrom.
	//
	// When your filter should work with Sendmail you should set esmtpArgs to the empty string
	// since Sendmail validates the provided esmtpArgs and also rejects valid values like `SIZE=20`.
	ChangeMailFrom(from string, esmtpArgs string)

	// RcptTos holds the [RcptTo] recipient slice of this transaction.
	// Your changes to Addr and/or Args values of the elements of this slice get sent back to the MTA.
	// But you should use DelRcptTo and AddRcptTo
	//
	// Only populated if [WithDecisionAt] is bigger than [DecisionAtMailFrom].
	RcptTos() []*addr.RcptTo
	// HasRcptTo returns true when rcptTo is in the list of recipients.
	//
	// rcptTo get compared to the existing recipients IDNA address aware.
	HasRcptTo(rcptTo string) bool
	// AddRcptTo adds the rcptTo (without angles) to the list of recipients with the ESMTP arguments esmtpArgs.
	// If rcptTo is already in the list of recipients, only the esmtpArgs of this recipient get updated.
	//
	// rcptTo get compared to the existing recipients IDNA address aware.
	//
	// When your filter should work with Sendmail you should set esmtpArgs to the empty string
	// since Sendmail validates the provided esmtpArgs and also rejects valid values like `BODY=8BITMIME`.
	AddRcptTo(rcptTo string, esmtpArgs string)
	// DelRcptTo deletes the rcptTo (without angles) from the list of recipients.
	//
	// rcptTo get compared to the existing recipients IDNA address aware.
	DelRcptTo(rcptTo string)

	// Headers are the [Header] fields of this message.
	// You can use methods of [Header] to change the header fields of the current message.
	//
	// Only populated if [WithDecisionAt] is bigger than [DecisionAtData].
	Headers() header.Header
	// HeadersEnforceOrder activates a workaround for Sendmail to ensure that the header ordering of the resulting email
	// is exactly the same as the order in Headers. To ensure that, we delete all existing headers and add all headers
	// as new headers. This is, of course, a significant overhead, so you should only call this method when you really need
	// to enforce a specific header order.
	//
	// Sendmail may re-fold your header values (newline characters you inserted might get removed).
	//
	// For other MTAs this method does not do anything (since there we can ensure correct header ordering without this workaround).
	HeadersEnforceOrder()

	// Body gets you a [io.ReadSeeker] of the body.
	// The reader gets seeked to the start of the body whenever you call this method.
	//
	// This method returns nil when you used [WithDecisionAt] with anything other than [DecisionAtEndOfMessage]
	// or you used [WithoutBody].
	Body() io.ReadSeeker
	// ReplaceBody replaces the body of the current message with the contents of the [io.Reader] r.
	// The reader will only get read once, but it might get buffered when you call [Data] on the transaction.
	// When the reader implements the [io.Closer] interface, the milter will call [io.Closer.Close] on the reader when it is done with it.
	ReplaceBody(r io.Reader)

	// QueueId is the queue ID the MTA assigned for this transaction.
	// You cannot change this value.
	//
	// Only populated if [WithDecisionAt] is bigger than [DecisionAtMailFrom].
	QueueId() string

	// Data returns the full email data (headers and body) of the current message.
	// It includes any modifications you made to the Headers and either uses Body or ReplaceBody as the body of the message.
	// If you set WithoutBody, WithDecisionAt is not DecisionAtEndOfMessage, or the body is bigger than the configured maximum body size, Data will be the same as [header.Header.Reader].
	// A call to Data might re-use the Body io.ReadSeeker. Using Data and Body at the same time is not supported.
	Data() io.Reader
}

Trx can be used to examine the data of the current mail transaction and also send changes to the message back to the MTA.

Directories

Path Synopsis
Package addr includes IDNA aware address structs
Package addr includes IDNA aware address structs
Package header includes interfaces to access and modify email headers
Package header includes interfaces to access and modify email headers
Package testtrx can be used to test mailfilter based filter functions
Package testtrx can be used to test mailfilter based filter functions

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL