package spf import ( "net" "strings" "github.com/mjl-/mox/dns" "github.com/mjl-/mox/message" ) // ../rfc/7208:2083 // 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; 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. } // Identity that was verified. type Identity string const ( ReceivedMailFrom Identity = "mailfrom" ReceivedHELO Identity = "helo" ) func receivedValueEncode(s string) string { if s == "" { return quotedString("") } for i, c := range s { if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c > 0x7f { continue } // ../rfc/5322:679 const atext = "!#$%&'*+-/=?^_`{|}~" if strings.IndexByte(atext, byte(c)) >= 0 { continue } if c != '.' || (i == 0 || i+1 == len(s)) { return quotedString(s) } } return s } // ../rfc/5322:736 func quotedString(s string) string { w := &strings.Builder{} w.WriteByte('"') for _, c := range s { if c > ' ' && c < 0x7f && c != '"' && c != '\\' || c > 0x7f || c == ' ' || c == '\t' { // We allow utf-8. This should only be needed when the destination address has an // utf8 localpart, in which case we are already doing smtputf8. // We also allow unescaped space and tab. This is FWS, and the name of ABNF // production "qcontent" implies the FWS is not part of the string, but escaping // space and tab leads to ugly strings. ../rfc/5322:743 w.WriteRune(c) continue } switch c { case ' ', '\t', '"', '\\': w.WriteByte('\\') w.WriteRune(c) } } w.WriteByte('"') return w.String() } // Header returns a Received-SPF header including trailing crlf that can be // prepended to an incoming message. func (r Received) Header() string { // ../rfc/7208:2043 w := &message.HeaderWriter{} w.Add("", "Received-SPF: "+string(r.Result)) if r.Comment != "" { w.Add(" ", "("+r.Comment+")") } w.Addf(" ", "client-ip=%s;", receivedValueEncode(r.ClientIP.String())) w.Addf(" ", "envelope-from=%s;", receivedValueEncode(r.EnvelopeFrom)) var helo string if len(r.Helo.IP) > 0 { helo = r.Helo.IP.String() } else { helo = r.Helo.Domain.ASCII } w.Addf(" ", "helo=%s;", receivedValueEncode(helo)) if r.Problem != "" { s := r.Problem max := 77 - len("problem=; ") if len(s) > max { s = s[:max] } w.Addf(" ", "problem=%s;", receivedValueEncode(s)) } if r.Mechanism != "" { w.Addf(" ", "mechanism=%s;", receivedValueEncode(r.Mechanism)) } w.Addf(" ", "receiver=%s;", receivedValueEncode(r.Receiver)) w.Addf(" ", "identity=%s", receivedValueEncode(string(r.Identity))) return w.String() }