mailfilter

package
v0.8.4 Latest Latest
Warning

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

Go to latest
Published: Dec 28, 2023 License: BSD-2-Clause Imports: 16 Imported by: 10

Documentation

Overview

Package mailfilter allows you to write milter filters without boilerplate code

Index

Examples

Constants

View Source
const (
	Accept   decision = "250 accept"
	Reject   decision = "550 5.7.1 Command rejected"
	TempFail decision = "451 4.7.1 Service unavailable - try again later"
	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 string // "unknown", "unix", "tcp4" or "tcp6"
	Port   uint16 // If Family is "tcp4" or "tcp6" the remote port of client connecting to the MTA
	Addr   string // If Family "unix" the path to the unix socket. If "tcp4" or "tcp6" 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 {
	// contains filtered or unexported methods
}

func CustomErrorResponse

func CustomErrorResponse(code uint16, reason string) Decision

func QuarantineResponse added in v0.6.5

func QuarantineResponse(reason string) Decision

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 connect 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 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 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 client and MTA this holds the subject of the validated client certificate.
	CertIssuer  string // If MutualTLS was used for the connection between client and MTA this holds the subject of the issuer of the client certificate (CA or Sub-CA).
}

type MTA added in v0.6.6

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 added in v0.6.6

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

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 function that configure/fine-tune the mail filter.

Example
package main

import (
	"context"
	"flag"
	"log"
	"strings"

	"github.com/d--j/go-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
	mailFilter, 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 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
		},
		// optimization: call the decision function when all headers were sent to us
		mailfilter.WithDecisionAt(mailfilter.DecisionAtEndOfHeaders),
	)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Started milter on %s:%s", mailFilter.Addr().Network(), mailFilter.Addr().String())

	// wait for the mail filter to end
	mailFilter.Wait()
}
Output:

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) Wait

func (f *MailFilter) Wait()

Wait waits for the end of the MailFilter server.

type Option

type Option func(opt *options)

func WithDecisionAt

func WithDecisionAt(decisionAt DecisionAt) Option

WithDecisionAt sets the decision point for the MailFilter. The default is DecisionAtEndOfMessage.

func WithErrorHandling

func WithErrorHandling(errorHandling ErrorHandling) Option

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

func WithoutBody

func WithoutBody() Option

WithoutBody configures the MailFilter to not request and collect the mail body.

type Trx added in v0.7.0

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 [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 send 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 gets 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 gets 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 gets 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.
	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
}

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