spf

package
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Apr 30, 2024 License: MIT Imports: 14 Imported by: 1

Documentation

Overview

Package spf implements Sender Policy Framework (SPF, RFC 7208) for verifying remote mail server IPs with their published records.

With SPF a domain can publish a policy as a DNS TXT record describing which IPs are allowed to send email with SMTP with the domain in the MAIL FROM command, and how to treat SMTP transactions coming from other IPs.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// Lookup errors.
	ErrName            = errors.New("spf: bad domain name")
	ErrNoRecord        = errors.New("spf: no txt record")
	ErrMultipleRecords = errors.New("spf: multiple spf txt records in dns")
	ErrDNS             = errors.New("spf: lookup of dns record")
	ErrRecordSyntax    = errors.New("spf: malformed spf txt record")

	// Evaluation errors.
	ErrTooManyDNSRequests = errors.New("spf: too many dns requests")
	ErrTooManyVoidLookups = errors.New("spf: too many void lookups")
	ErrMacroSyntax        = errors.New("spf: bad macro syntax")
)

Functions

func Lookup

func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, domain dns.Domain) (rstatus Status, rtxt string, rrecord *Record, authentic bool, rerr error)

Lookup looks up and parses an SPF TXT record for domain.

Authentic indicates if the DNS results were DNSSEC-verified.

Types

type Args

type Args struct {
	// RemoteIP will be checked as sender for email.
	RemoteIP net.IP

	// Address from SMTP MAIL FROM command. Zero values for a null reverse path (used for DSNs).
	MailFromLocalpart smtp.Localpart
	MailFromDomain    dns.Domain

	// HelloDomain is from the SMTP EHLO/HELO command.
	HelloDomain dns.IPDomain

	LocalIP       net.IP
	LocalHostname dns.Domain
	// contains filtered or unexported fields
}

Args are the parameters to the SPF verification algorithm ("check_host" in the RFC).

All fields should be set as they can be required for macro expansions.

type Directive

type Directive struct {
	Qualifier  string // Sets the result if this directive matches. "" and "+" are "pass", "-" is "fail", "?" is "neutral", "~" is "softfail".
	Mechanism  string // "all", "include", "a", "mx", "ptr", "ip4", "ip6", "exists".
	DomainSpec string // For include, a, mx, ptr, exists. Always in lower-case when parsed using ParseRecord.
	IP         net.IP `json:"-"` // For ip4, ip6.
	IPstr      string // Original string for IP, always with /subnet.
	IP4CIDRLen *int   // For a, mx, ip4.
	IP6CIDRLen *int   // For a, mx, ip6.
}

Directive consists of a mechanism that describes how to check if an IP matches, an (optional) qualifier indicating the policy for a match, and optional parameters specific to the mechanism.

func (Directive) MechanismString

func (d Directive) MechanismString() string

MechanismString returns a directive in string form for use in the Received-SPF header.

type Identity

type Identity string

Identity that was verified.

const (
	ReceivedMailFrom Identity = "mailfrom"
	ReceivedHELO     Identity = "helo"
)

type Modifier

type Modifier struct {
	Key   string // Key is case-insensitive.
	Value string
}

Modifier provides additional information for a policy. "redirect" and "exp" are not represented as a Modifier but explicitly in a Record.

type Received

type Received struct {
	Result       Status
	Comment      string       // Additional free-form information about the verification result. Optional. Included in message header comment inside "()".
	ClientIP     net.IP       // IP address of remote SMTP client, "client-ip=".
	EnvelopeFrom string       // Sender mailbox, typically SMTP MAIL FROM, but will be set to "postmaster" at SMTP EHLO if MAIL FROM is empty, "envelop-from=".
	Helo         dns.IPDomain // IP or host name from EHLO or HELO command, "helo=".
	Problem      string       // Optional. "problem="
	Receiver     string       // Hostname of receiving mail server, "receiver=".
	Identity     Identity     // The identity that was checked, "mailfrom" or "helo", for "identity=".
	Mechanism    string       // Mechanism that caused the result, can be "default". Optional.
}

Received represents a Received-SPF header with the SPF verify results, to be prepended to a message.

Example:

Received-SPF: pass (mybox.example.org: domain of
 myname@example.com designates 192.0.2.1 as permitted sender)
 receiver=mybox.example.org; client-ip=192.0.2.1;
 envelope-from="myname@example.com"; helo=foo.example.com;

func Verify

func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, args Args) (received Received, domain dns.Domain, explanation string, authentic bool, rerr error)

Verify checks if a remote IP is allowed to send email for a domain.

If the SMTP "MAIL FROM" is set, it is used as identity (domain) to verify. Otherwise, the EHLO domain is verified if it is a valid domain.

The returned Received.Result status will always be set, regardless of whether an error is returned. For status Temperror and Permerror, an error is always returned. For Fail, explanation may be set, and should be returned in the SMTP session if it is the reason the message is rejected. The caller should ensure the explanation is valid for use in SMTP, taking line length and ascii-only requirement into account.

Verify takes the maximum number of 10 DNS requests into account, and the maximum of 2 lookups resulting in no records ("void lookups").

Authentic indicates if the DNS results were DNSSEC-verified.

Example
package main

import (
	"context"
	"log"
	"log/slog"
	"net"

	"github.com/mjl-/mox/dns"
	"github.com/mjl-/mox/smtp"
	"github.com/mjl-/mox/spf"
)

func main() {
	ctx := context.Background()
	resolver := dns.StrictResolver{}

	args := spf.Args{
		// IP from SMTP session.
		RemoteIP: net.ParseIP("1.2.3.4"),

		// Based on "MAIL FROM" in SMTP session.
		MailFromLocalpart: smtp.Localpart("user"),
		MailFromDomain:    dns.Domain{ASCII: "sendingdomain.example.com"},

		// From HELO/EHLO in SMTP session.
		HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mx.example.com"}},

		// LocalIP and LocalHostname should be set, they may be used when evaluating macro's.
	}

	// Lookup SPF record and evaluate against IP and domain in args.
	received, domain, explanation, authentic, err := spf.Verify(ctx, slog.Default(), resolver, args)

	// received.Result is always set, regardless of err.
	switch received.Result {
	case spf.StatusNone:
		log.Printf("no useful spf result, domain probably has no spf record")
	case spf.StatusNeutral:
		log.Printf("spf has no statement on ip, with \"?\" qualifier")
	case spf.StatusPass:
		log.Printf("ip is authorized")
	case spf.StatusFail:
		log.Printf("ip is not authorized, with \"-\" qualifier")
	case spf.StatusSoftfail:
		log.Printf("ip is probably not authorized, with \"~\" qualifier, softfail")
	case spf.StatusTemperror:
		log.Printf("temporary error, possibly dns lookup failure, try again soon")
	case spf.StatusPermerror:
		log.Printf("permanent error, possibly invalid spf records, later attempts likely have the same result")
	}
	if err != nil {
		log.Printf("error: %v", err)
	}
	if explanation != "" {
		log.Printf("explanation from remote about spf result: %s", explanation)
	}
	log.Printf("result is for domain %s", domain) // mailfrom or ehlo/ehlo.
	log.Printf("dns lookups dnssec-protected: %v", authentic)
}
Output:

func (Received) Header

func (r Received) Header() string

Header returns a Received-SPF header including trailing crlf that can be prepended to an incoming message.

type Record

type Record struct {
	Version     string      // Must be "spf1".
	Directives  []Directive // An IP is evaluated against each directive until a match is found.
	Redirect    string      // Modifier that redirects SPF checks to other domain after directives did not match. Optional. For "redirect=".
	Explanation string      // Modifier for creating a user-friendly error message when an IP results in status "fail".
	Other       []Modifier  // Other modifiers.
}

Record is a parsed SPF DNS record.

An example record for example.com:

v=spf1 +mx a:colo.example.com/28 -all

func ParseRecord

func ParseRecord(s string) (r *Record, isspf bool, rerr error)

ParseRecord parses an SPF DNS TXT record.

func (Record) Record

func (r Record) Record() (string, error)

Record returns an DNS record, to be configured as a TXT record for a domain, e.g. a TXT record for example.com.

type Status

type Status string

Status is the result of an SPF verification.

const (
	StatusNone      Status = "none"      // E.g. no DNS domain name in session, or no SPF record in DNS.
	StatusNeutral   Status = "neutral"   // Explicit statement that nothing is said about the IP, "?" qualifier. None and Neutral must be treated the same.
	StatusPass      Status = "pass"      // IP is authorized.
	StatusFail      Status = "fail"      // IP is exlicitly not authorized. "-" qualifier.
	StatusSoftfail  Status = "softfail"  // Weak statement that IP is probably not authorized, "~" qualifier.
	StatusTemperror Status = "temperror" // Trying again later may succeed, e.g. for temporary DNS lookup error.
	StatusPermerror Status = "permerror" // Error requiring some intervention to correct. E.g. invalid DNS record.
)

func Evaluate

func Evaluate(ctx context.Context, elog *slog.Logger, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error)

Evaluate evaluates the IP and names from args against the SPF DNS record for the domain.

Jump to

Keyboard shortcuts

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