sieve

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 8 Imported by: 0

README

go-sieve

CI Go Reference Go Report Card

A parser and interpreter for the Sieve mail-filtering language (RFC 5228) in Go — standard library only, no external dependencies.

About

Sieve is the language mail servers use to let users sort, file, redirect, and auto-reply to incoming mail without running arbitrary code. This package turns a Sieve script into an executable decision over one message.

Parsing and evaluation are decoupled from any mailbox model. You Parse a script once into a *Script, adapt your message into the neutral Message type, and call Script.Evaluate with an Executor you implement. The evaluator walks the script, evaluates its tests against the message, and calls your Executor's methods to apply the actions the script selected — how each action maps onto a real mailbox (folder names, flag storage, vacation de-duplication, notification transport) is entirely your concern. The package decides what to do; your host decides how. The two terminal dispositions, discard and reject, are reported through the returned Outcome rather than the Executor.

Delivery follows RFC 5228's implicit-keep model: unless the script cancels it (with keep, fileinto, redirect, or discard), the message is delivered to the default mailbox, reported as Outcome.ImplicitKeep. A discard only cancels that keep — it does not stop the script, so any other delivering action still takes effect. A runtime error during evaluation fails safe to the implicit keep (Outcome.Error) rather than losing the message.

Features

  • Control: if / elsif / else, require, stop.
  • Tests: address, header, envelope, exists, size (:over/:under), body, allof, anyof, not, true/false.
  • Actions: keep, discard, fileinto (+:create), redirect, reject, imap4flags (setflag/addflag/removeflag), vacation, notify.
  • Match types: :is, :contains, :matches (glob with * and ?), plus a non-standard :regex. Comparators: i;ascii-casemap (default), i;octet, i;ascii-numeric. Address parts: :all/:localpart/:domain.
  • Strict where it counts, lenient elsewhere: the parser is strict about the constructs it understands (so Validate catches real mistakes) but skips unknown commands, tests, and tagged arguments, so scripts using extensions this package does not implement still load and run their recognised parts.
  • Zero external dependencies.

Install

go get github.com/rest-mail/go-sieve

Quickstart

Parse a script, run it against a message, and inspect the actions it selected. Your Executor maps each action onto your delivery model; the terminal discard/reject decisions come back in the Outcome.

package main

import (
	"fmt"

	sieve "github.com/rest-mail/go-sieve"
)

// mailbox implements sieve.Executor, applying actions to your delivery model.
type mailbox struct {
	folder string
	flags  []string
}

func (m *mailbox) Keep()                               {}
func (m *mailbox) FileInto(folder string, create bool) { m.folder = folder }
func (m *mailbox) Redirect(addr string)                {}
func (m *mailbox) Flag(op string, flags []string)      { m.flags = flags }
func (m *mailbox) Vacation(v sieve.Vacation)           {}
func (m *mailbox) Notify(method, message string)       {}

func main() {
	script, err := sieve.Parse(`require ["fileinto", "imap4flags"];
if header :contains "Subject" "invoice" {
    setflag "\\Flagged";
    fileinto :create "Invoices";
}`)
	if err != nil {
		panic(err)
	}

	msg := &sieve.Message{
		Headers: sieve.Headers{Subject: "Your March invoice is ready"},
	}

	mb := &mailbox{}
	outcome := script.Evaluate(msg, mb)

	switch outcome.Disposition {
	case sieve.Discard:
		fmt.Println("discard")
	case sieve.Reject:
		fmt.Println("reject:", outcome.RejectReason)
	default:
		if outcome.ImplicitKeep {
			mb.Keep() // implicit keep: deliver to the default mailbox
		}
		fmt.Printf("deliver to %q with flags %v\n", mb.folder, mb.flags)
	}
	// Prints: deliver to "Invoices" with flags [\Flagged]
}

Check a script's syntax without evaluating it with sieve.Validate(src).

The Executor

Executor is the seam between the language and your mailbox. Its methods (Keep, FileInto, Redirect, Flag, Vacation, Notify) are invoked in script order as each non-terminal action fires; you decide what a folder name, flag, redirect, or vacation reply actually means for your storage and transport. The terminal dispositions are not methods — reject refuses the message and discard (when nothing else delivers it) drops it, and both surface through Outcome.Disposition (with Outcome.RejectReason for a reject). A discard alongside a delivering action does not drop the message; it only cancels the implicit keep, so Outcome.ImplicitKeep tells you whether to also deliver to the default mailbox. For a vacation action the evaluator computes Vacation.ReplyTo (the envelope sender, falling back to the From header) and reports the minimum reply interval as Vacation.Interval (a duration, so a :seconds argument keeps its precision); a message with a null reverse-path (MAIL FROM:<>) gets no Vacation call at all. De-duplication and actually sending the auto-reply remain your responsibility.

Adapt your own email representation into the neutral Message before evaluating: Headers carries the common structured fields plus a Raw map consulted (case-insensitively) for any other header, so custom headers such as X-Priority are testable; Envelope supplies the SMTP identities the envelope test reads; Body and Attachments feed the body and size tests.

Documentation

Full API reference: pkg.go.dev/github.com/rest-mail/go-sieve.

License

MIT © 2026 rest-mail

Documentation

Overview

Package sieve parses and executes scripts written in the Sieve mail-filtering language (RFC 5228) — the language mail servers use to let users sort, file, redirect, and auto-reply to incoming mail.

Parsing and evaluation are decoupled from any mailbox model. A caller [Parse]s a script once into a Script, adapts its own email into the neutral Message type, and calls Script.Evaluate with an Executor it implements. The evaluator walks the script, evaluates each test against the message, and calls the Executor's methods to apply the actions the script selected (fileinto, redirect, imap4flags, vacation, notify, keep). The two terminal dispositions, discard and reject, are reported through the returned Outcome rather than the Executor. How each action maps onto a real mailbox — folder names, flag storage, vacation de-duplication, notification transport — is entirely the Executor's concern: this package decides what to do, the host decides how. It depends only on the Go standard library.

Evaluating a script

script, err := sieve.Parse(src)
if err != nil {
	// syntax error
}
outcome := script.Evaluate(msg, exec) // exec implements sieve.Executor
switch outcome.Disposition {
case sieve.Discard:
	// silently drop the message
case sieve.Reject:
	// refuse it, citing outcome.RejectReason
default:
	// deliver, honouring the actions exec recorded; additionally deliver to
	// the default mailbox when outcome.ImplicitKeep is set (the RFC 5228
	// §2.10.2 implicit keep)
}

The evaluator applies the RFC 5228 §2.10.2 implicit keep: unless the script cancels it with keep, fileinto, redirect, or discard, the message is delivered to the default mailbox, signalled by Outcome.ImplicitKeep. A discard only cancels that keep — it does not stop the script, so later actions still run. A runtime error during evaluation fails safe to the implicit keep (§2.10.6), reported via Outcome.Error, rather than losing the message.

Use Validate to check a script's syntax without evaluating it.

Supported language

Control commands: if / elsif / else, require, stop.

Tests: address, header, envelope, exists, size (:over / :under), body, allof, anyof, not, true / false.

Actions: keep, discard, fileinto (with :create), redirect, reject, imap4flags (setflag / addflag / removeflag), vacation, notify.

Match types: :is, :contains, :matches (glob with * and ?), and a non-standard :regex extension. Comparators: i;ascii-casemap (the default), i;octet, and i;ascii-numeric. Address parts: :all, :localpart, :domain.

require and extensions

Parsing enforces RFC 5228 "require" semantics. Every extension a script uses must be declared with require before use; requiring an extension this package does not implement is an error; require must precede every other command; and an unknown command or test (including a typo) is a parse error rather than a silent no-op. Parse and Validate therefore reject a script that uses an unsupported or undeclared extension instead of running it partially. Script.Requires reports the extensions the script declared via "require".

Example

Example parses a small Sieve script, runs it against a message, and inspects the actions the script selected.

package main

import (
	"fmt"

	sieve "github.com/rest-mail/go-sieve"
)

// mailbox is a minimal sieve.Executor that records where the script decided to
// deliver the message and which flags it set. A real implementation would move
// the message into an actual folder, persist IMAP flags, send the vacation
// reply, and so on — the package decides what to do, this type decides how.
type mailbox struct {
	folder string
	flags  []string
}

func (m *mailbox) Keep()                               {}
func (m *mailbox) FileInto(folder string, create bool) { m.folder = folder }
func (m *mailbox) Redirect(addr string)                {}
func (m *mailbox) Flag(op string, flags []string)      { m.flags = flags }
func (m *mailbox) Vacation(v sieve.Vacation)           {}
func (m *mailbox) Notify(method, message string)       {}

// Example parses a small Sieve script, runs it against a message, and inspects
// the actions the script selected.
func main() {
	script, err := sieve.Parse(`require ["fileinto", "imap4flags", "mailbox"];
if header :contains "Subject" "invoice" {
    setflag "\\Flagged";
    fileinto :create "Invoices";
}`)
	if err != nil {
		panic(err)
	}

	msg := &sieve.Message{
		Headers: sieve.Headers{Subject: "Your March invoice is ready"},
	}

	mb := &mailbox{}
	outcome := script.Evaluate(msg, mb)

	switch outcome.Disposition {
	case sieve.Discard:
		fmt.Println("discard")
	case sieve.Reject:
		fmt.Println("reject:", outcome.RejectReason)
	default:
		fmt.Printf("deliver to %q with flags %v\n", mb.folder, mb.flags)
	}
}
Output:
deliver to "Invoices" with flags [\Flagged]

Index

Examples

Constants

View Source
const DefaultMaxDepth = 64

DefaultMaxDepth is the default limit on how deeply tests and control blocks may nest before Parse rejects a script. The parser and evaluator both recurse over this nesting, so an unbounded script would otherwise exhaust the goroutine stack and crash the host process (a denial-of-service vector when script content is user-supplied). RFC 5228 §2.10.7 explicitly sanctions a finite limit and requires implementations to support only 15 levels; 64 leaves generous headroom for real scripts while capping the recursion a crafted script can drive. Override it per-call with WithMaxDepth.

Variables

This section is empty.

Functions

func Validate

func Validate(script string, opts ...Option) error

Validate reports whether a Sieve script is syntactically valid. It accepts the same [Option]s as Parse; nesting beyond DefaultMaxDepth (or the limit set with WithMaxDepth) is reported as invalid.

Types

type Address

type Address struct {
	Name    string
	Address string
}

Address is a structured email address.

type Attachment

type Attachment struct {
	Size int64
}

Attachment contributes its octet Size to the size test.

type Body

type Body struct {
	ContentType string
	Content     string
	Parts       []Body
}

Body is a (possibly multipart) message body used by the body test and size.

type Disposition

type Disposition int

Disposition is the terminal delivery decision an evaluation reached.

const (
	// Continue means no terminal action fired; deliver honouring whatever
	// actions the Executor recorded, plus the implicit keep to the default
	// mailbox when Outcome.ImplicitKeep is set.
	Continue Disposition = iota
	// Discard means the script asked to silently drop the message.
	Discard
	// Reject means the script asked to refuse the message (see Outcome.RejectReason).
	Reject
)

type Envelope

type Envelope struct {
	// From is the SMTP reverse-path (MAIL FROM) address. An empty From with
	// FromNull unset means the envelope sender is absent (no MAIL FROM was
	// seen); the envelope "from" test then produces no value to match.
	From string
	// FromNull marks a null reverse-path (SMTP "MAIL FROM:<>", i.e. a bounce).
	// Per RFC 5228 §5.4 the envelope "from" value is then the empty string, so
	// `envelope :is "from" ""` matches. Set it (with From left empty) to
	// distinguish a genuine null reverse-path from a merely absent sender.
	FromNull bool
	To       []string
}

Envelope holds the SMTP envelope identities used by the envelope test. The host resolves any override (e.g. a gateway-supplied sender) before populating these fields.

type Executor

type Executor interface {
	// Keep requests explicit delivery to the default mailbox. The RFC 5228
	// §2.10.2 implicit keep does not call this method; it is reported through
	// Outcome.ImplicitKeep so the host can distinguish it from an explicit keep.
	Keep()
	// FileInto delivers into the named folder, creating it when create is set.
	FileInto(folder string, create bool)
	// Redirect forwards the message to addr.
	Redirect(addr string)
	// Flag applies an imap4flags operation ("setflag", "addflag", "removeflag").
	Flag(op string, flags []string)
	// Vacation records a vacation auto-reply request.
	Vacation(v Vacation)
	// Notify records a notification request.
	Notify(method, message string)
}

Executor applies the (non-terminal) actions a Sieve script selects. The host implements it to map each action onto its mailbox/delivery model. Methods are invoked in script order; the terminal actions discard and reject are not Executor methods but are reported via the Outcome returned by Script.Evaluate.

type Headers

type Headers struct {
	Subject    string
	From       []Address
	To         []Address
	Cc         []Address
	Bcc        []Address
	MessageID  string
	InReplyTo  string
	Date       string
	References []string
	Raw        map[string][]string
}

Headers holds the structured and raw headers used by header/address/exists tests. Raw is consulted (case-insensitively) for any header not covered by a structured field, so custom headers such as X-Priority are testable.

type Message

type Message struct {
	Headers     Headers
	Envelope    Envelope
	Body        Body
	Attachments []Attachment
	// RawSize is the exact octet count of the whole message as it appears on
	// the wire — the header block, the blank line separating headers from body,
	// and the body. The size test (RFC 5228 §5.9) counts the entire message, so
	// when the host knows this figure it should set RawSize and it is used
	// verbatim. When RawSize is zero the size is reconstructed from the Headers,
	// Body, and Attachments fields, which is only an approximation because the
	// reconstructed headers need not be byte-identical to the original.
	RawSize int64
}

Message is the neutral view of an email the evaluator tests against. A host maps its own representation onto this type before evaluating; the evaluator never sees the host's message model.

type Option added in v0.2.0

type Option func(*parseOptions)

Option configures optional Parse / Validate behaviour.

func WithMaxDepth added in v0.2.0

func WithMaxDepth(n int) Option

WithMaxDepth overrides DefaultMaxDepth, the maximum nesting depth of tests (not / allof / anyof) and control blocks (if / elsif / else) the parser will accept before returning an error. A non-positive value restores the default.

type Outcome

type Outcome struct {
	Disposition  Disposition
	RejectReason string // set only when Disposition == Reject
	// ImplicitKeep reports that the RFC 5228 §2.10.2 implicit keep is in effect:
	// the script cancelled no keep and performed no fileinto, redirect, or
	// discard, so the host must deliver the message to the default mailbox in
	// addition to honouring any actions the Executor recorded. It is only ever set
	// alongside Disposition == Continue. It is also set on the §2.10.6 fail-safe
	// keep (see Error). When a delivering action ran, or a discard dropped the
	// message, ImplicitKeep is false.
	ImplicitKeep bool
	// Error is non-nil when evaluation was aborted by a runtime error — for
	// example an Executor callback panicked. Per RFC 5228 §2.10.6 the message is
	// then kept rather than lost (ImplicitKeep is true and Disposition is
	// Continue) and the host should notify the user of the failure.
	Error error
}

Outcome is the result of evaluating a script.

type Script

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

Script is a fully parsed Sieve script. Parse it with Parse; evaluate it with Script.Evaluate.

func Parse

func Parse(src string, opts ...Option) (*Script, error)

Parse parses a Sieve script into an evaluable Script. It is strict about the constructs it understands and lenient about unknown extensions (see the package overview). Nesting of tests and control blocks is capped at DefaultMaxDepth (override with WithMaxDepth); a script that exceeds the cap is rejected rather than allowed to exhaust the stack at parse or evaluation time.

func (*Script) Empty

func (s *Script) Empty() bool

Empty reports whether the script has no executable commands (a nil script, or one consisting only of require/comments).

func (*Script) Evaluate

func (s *Script) Evaluate(msg *Message, exec Executor) (out Outcome)

Evaluate runs the script against msg, invoking exec for each action it selects, and returns the terminal Outcome.

It applies the RFC 5228 §2.10.2 implicit-keep model: unless the script cancels it (with keep, fileinto, redirect, or discard) the message is delivered to the default mailbox, reported via Outcome.ImplicitKeep. Per §2.10.6, a runtime error during evaluation fails safe to that implicit keep rather than losing the message.

func (*Script) Requires

func (s *Script) Requires() []string

Requires returns the extensions the script declared via "require".

type Vacation

type Vacation struct {
	// Interval is the minimum period that must elapse before another auto-reply
	// is sent to the same sender (RFC 5230 :days / RFC 6131 :seconds). It is a
	// duration so a :seconds argument keeps its sub-day precision; the Executor
	// uses it for de-duplication.
	Interval time.Duration
	Subject  string
	Body     string
	ReplyTo  string
}

Vacation carries the arguments of a matched vacation action (RFC 5230). The evaluator computes ReplyTo (the envelope sender, falling back to the From header); the Executor is responsible for de-duplication and for actually sending the auto-reply.

A Vacation is only reported when a reply is permitted: the evaluator suppresses it entirely for a message with a null reverse-path (see the RFC 5230 §4.6 / RFC 3834 note on vacationReplyTo), so the Executor never has to make that decision.

Jump to

Keyboard shortcuts

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