Documentation
¶
Overview ¶
Package mailfilter allows you to write milter filters without boilerplate code
Index ¶
- Constants
- type Connect
- type Decision
- type DecisionAt
- type DecisionModificationFunc
- type ErrorHandling
- type Helo
- type MTA
- type MailFilter
- func New(network, address string, decision DecisionModificationFunc, opts ...Option) (*MailFilter, error)deprecated
- func NewBind(address *bind.Target, decision DecisionModificationFunc, opts ...Option) (*MailFilter, error)
- func NewMailFilter(family milter.Family, addr *bind.Target, unix string, ...) (*MailFilter, error)
- func NewUnix(unix string, decision DecisionModificationFunc, opts ...Option) (*MailFilter, error)
- type MaxAction
- type Option
- func WithBody(maxMem int, maxSize int64, maxAction MaxAction) Option
- func WithDecisionAt(decisionAt DecisionAt) Option
- func WithErrorHandling(errorHandling ErrorHandling) Option
- func WithHeader(maxHeaders uint32, maxAction MaxAction) Option
- func WithRcptToValidator(validator RcptToValidator) Option
- func WithoutBody() Option
- type RcptToValidationInput
- type RcptToValidator
- type Trx
Examples ¶
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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()
}
Output:
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) 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 ¶
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 ¶
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.
Source Files
¶
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 |