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 ¶
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") )
var (
MetricVerify stub.HistogramVec = stub.HistogramVecIgnore{}
)
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 ¶
MechanismString returns a directive in string form for use in the Received-SPF header.
type Modifier ¶
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:
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 ¶
ParseRecord parses an SPF DNS TXT record.
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. )