1package spf
2
3import (
4 "net"
5 "strings"
6
7 "github.com/mjl-/mox/dns"
8 "github.com/mjl-/mox/message"
9)
10
11// ../rfc/7208:2083
12
13// Received represents a Received-SPF header with the SPF verify results, to be
14// prepended to a message.
15//
16// Example:
17//
18// Received-SPF: pass (mybox.example.org: domain of
19// myname@example.com designates 192.0.2.1 as permitted sender)
20// receiver=mybox.example.org; client-ip=192.0.2.1;
21// envelope-from="myname@example.com"; helo=foo.example.com;
22type Received struct {
23 Result Status
24 Comment string // Additional free-form information about the verification result. Optional. Included in message header comment inside "()".
25 ClientIP net.IP // IP address of remote SMTP client, "client-ip=".
26 EnvelopeFrom string // Sender mailbox, typically SMTP MAIL FROM, but will be set to "postmaster" at SMTP EHLO if MAIL FROM is empty, "envelop-from=".
27 Helo dns.IPDomain // IP or host name from EHLO or HELO command, "helo=".
28 Problem string // Optional. "problem="
29 Receiver string // Hostname of receiving mail server, "receiver=".
30 Identity Identity // The identity that was checked, "mailfrom" or "helo", for "identity=".
31 Mechanism string // Mechanism that caused the result, can be "default". Optional.
32}
33
34// Identity that was verified.
35type Identity string
36
37const (
38 ReceivedMailFrom Identity = "mailfrom"
39 ReceivedHELO Identity = "helo"
40)
41
42func receivedValueEncode(s string) string {
43 if s == "" {
44 return quotedString("")
45 }
46 for i, c := range s {
47 if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c > 0x7f {
48 continue
49 }
50 // ../rfc/5322:679
51 const atext = "!#$%&'*+-/=?^_`{|}~"
52 if strings.IndexByte(atext, byte(c)) >= 0 {
53 continue
54 }
55 if c != '.' || (i == 0 || i+1 == len(s)) {
56 return quotedString(s)
57 }
58 }
59 return s
60}
61
62// ../rfc/5322:736
63func quotedString(s string) string {
64 w := &strings.Builder{}
65 w.WriteByte('"')
66 for _, c := range s {
67 if c > ' ' && c < 0x7f && c != '"' && c != '\\' || c > 0x7f || c == ' ' || c == '\t' {
68 // We allow utf-8. This should only be needed when the destination address has an
69 // utf8 localpart, in which case we are already doing smtputf8.
70 // We also allow unescaped space and tab. This is FWS, and the name of ABNF
71 // production "qcontent" implies the FWS is not part of the string, but escaping
72 // space and tab leads to ugly strings. ../rfc/5322:743
73 w.WriteRune(c)
74 continue
75 }
76 switch c {
77 case ' ', '\t', '"', '\\':
78 w.WriteByte('\\')
79 w.WriteRune(c)
80 }
81 }
82 w.WriteByte('"')
83 return w.String()
84}
85
86// Header returns a Received-SPF header including trailing crlf that can be
87// prepended to an incoming message.
88func (r Received) Header() string {
89 // ../rfc/7208:2043
90 w := &message.HeaderWriter{}
91 w.Add("", "Received-SPF: "+string(r.Result))
92 if r.Comment != "" {
93 w.Add(" ", "("+r.Comment+")")
94 }
95 w.Addf(" ", "client-ip=%s;", receivedValueEncode(r.ClientIP.String()))
96 w.Addf(" ", "envelope-from=%s;", receivedValueEncode(r.EnvelopeFrom))
97 var helo string
98 if len(r.Helo.IP) > 0 {
99 helo = r.Helo.IP.String()
100 } else {
101 helo = r.Helo.Domain.ASCII
102 }
103 w.Addf(" ", "helo=%s;", receivedValueEncode(helo))
104 if r.Problem != "" {
105 s := r.Problem
106 max := 77 - len("problem=; ")
107 if len(s) > max {
108 s = s[:max]
109 }
110 w.Addf(" ", "problem=%s;", receivedValueEncode(s))
111 }
112 if r.Mechanism != "" {
113 w.Addf(" ", "mechanism=%s;", receivedValueEncode(r.Mechanism))
114 }
115 w.Addf(" ", "receiver=%s;", receivedValueEncode(r.Receiver))
116 w.Addf(" ", "identity=%s", receivedValueEncode(string(r.Identity)))
117 return w.String()
118}
119