├── LICENSE ├── README.md ├── mechanism.go ├── mechanism_test.go ├── net.go ├── spf.go └── spf_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Unless otherwise stated in the script, all code in this repository falls under 2 | the following license. 3 | 4 | Copyright Stephen Haywood (AverageSecurityGuy) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | Neither the name of Stephen Haywood nor AverageSecurityguy of its may be used 18 | to endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 25 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Package spf 2 | 3 | [![Documentation](https://godoc.org/github.com/asggo/spf?status.svg)](http://godoc.org/github.com/asggo/spf) 4 | 5 | Package spf parses an SPF record and determines if a given IP address 6 | is allowed to send email based on that record. SPF handles all of the 7 | mechanisms defined at http://www.open-spf.org/SPF_Record_Syntax/. 8 | 9 | ## Example 10 | 11 | ```Go 12 | package main 13 | 14 | import "github.com/asggo/spf" 15 | 16 | func main() { 17 | 18 | SMTPClientIP := "1.1.1.1" 19 | envelopeFrom := "info@example.com" 20 | 21 | result, err := spf.SPFTest(SMTPClientIP, envelopeFrom) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | switch result { 27 | case spf.Pass: 28 | // allow action 29 | case spf.Fail: 30 | // deny action 31 | } 32 | //... 33 | } 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /mechanism.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | type Result string 12 | 13 | const ( 14 | Pass Result = "Pass" 15 | Neutral Result = "Neutral" 16 | Fail Result = "Fail" 17 | SoftFail Result = "SoftFail" 18 | None Result = "None" 19 | TempError Result = "TempError" 20 | PermError Result = "PermError" 21 | ) 22 | 23 | var ( 24 | ErrNoMatch = errors.New("Client was not covered by the mechanism.") 25 | ) 26 | 27 | // Mechanism represents a single mechanism in an SPF record. 28 | type Mechanism struct { 29 | Name string 30 | Domain string 31 | Prefix string 32 | Result Result 33 | Count int 34 | } 35 | 36 | // Return a Mechanism as a string 37 | func (m *Mechanism) String() string { 38 | var buf bytes.Buffer 39 | 40 | buf.WriteString(m.Name) 41 | 42 | if len(m.Domain) != 0 { 43 | buf.WriteString(fmt.Sprintf(":%s", m.Domain)) 44 | } 45 | 46 | if len(m.Prefix) != 0 { 47 | buf.WriteString(fmt.Sprintf("/%s", m.Prefix)) 48 | } 49 | 50 | buf.WriteString(fmt.Sprintf(" - %s", m.Result)) 51 | 52 | return buf.String() 53 | } 54 | 55 | // ResultTag maps the Result code to a suitable char 56 | func (m *Mechanism) ResultTag() string { 57 | switch m.Result { 58 | case Fail: 59 | return "-" 60 | case SoftFail: 61 | return "~" 62 | case Pass: 63 | return "+" 64 | case Neutral: 65 | return "?" 66 | } 67 | 68 | return "+" 69 | } 70 | 71 | // SPFString return a string representation of a mechanism, suitable for using 72 | // in a TXT record. 73 | func (m *Mechanism) SPFString() string { 74 | var buf bytes.Buffer 75 | 76 | tag := m.ResultTag() 77 | 78 | switch m.Name { 79 | case "redirect": 80 | buf.WriteString(fmt.Sprintf("%s=%s", m.Name, m.Domain)) 81 | case "all": 82 | buf.WriteString(fmt.Sprintf("%s%s", tag, m.Name)) 83 | default: 84 | if tag != "+" { 85 | buf.WriteString(tag) 86 | } 87 | 88 | buf.WriteString(m.Name) 89 | 90 | if len(m.Domain) != 0 { 91 | buf.WriteString(fmt.Sprintf(":%s", m.Domain)) 92 | } 93 | 94 | if len(m.Prefix) != 0 { 95 | buf.WriteString(fmt.Sprintf("/%s", m.Prefix)) 96 | } 97 | } 98 | 99 | return buf.String() 100 | } 101 | 102 | // Ensure the mechanism is valid 103 | func (m *Mechanism) Valid() bool { 104 | var hasResult bool 105 | var hasName bool 106 | var isIP bool 107 | 108 | switch m.Result { 109 | case Pass, Fail, SoftFail, Neutral: 110 | hasResult = true 111 | default: 112 | hasResult = false 113 | } 114 | 115 | switch m.Name { 116 | case "all", "a", "mx", "ip4", "ip6", "exists", "include", "ptr", "redirect": 117 | hasName = true 118 | default: 119 | hasName = false 120 | } 121 | 122 | isIP = true 123 | if m.Name == "ip4" || m.Name == "ip6" { 124 | valid := net.ParseIP(m.Domain) 125 | isIP = (valid != nil) 126 | } 127 | 128 | return hasResult && hasName && isIP 129 | } 130 | 131 | // Evaluate determines if the given IP address is covered by the mechanism. 132 | // If the IP is covered, the mechanism result is returned and error is nil. 133 | // If the IP is not covered an error is returned. The caller must check for 134 | // the error to determine if the result is valid. 135 | func (m *Mechanism) Evaluate(ip string, count int) (Result, error) { 136 | 137 | parsedIP := net.ParseIP(ip) 138 | 139 | switch m.Name { 140 | case "all": 141 | return m.Result, nil 142 | case "exists": 143 | _, err := net.LookupHost(m.Domain) 144 | if err == nil { 145 | return m.Result, nil 146 | } 147 | case "redirect": 148 | spf, err := NewSPF(m.Domain, "", count) 149 | 150 | // There is no clear definition of what to do with errors on a 151 | // redirected domain. Trying to make wise choices here. 152 | switch err { 153 | case ErrFailedLookup: 154 | return TempError, nil 155 | default: 156 | return PermError, nil 157 | } 158 | 159 | return spf.Test(ip), nil 160 | case "include": 161 | spf, err := NewSPF(m.Domain, "", count) 162 | 163 | // If there is no SPF record for the included domain or if we have too 164 | // many mechanisms that require DNS lookups it is considered a 165 | // PermError. Any other error is ok to ignore. 166 | if err == ErrNoRecord || err == ErrMaxCount { 167 | return PermError, nil 168 | } 169 | 170 | // The include statment is meant to be used as an if-pass or on-pass 171 | // statement. Meaning if we get a result other than Pass or PermError, 172 | // it is ok to ignore it and move on to the other mechanisms. 173 | result := spf.Test(ip) 174 | if result == Pass || result == PermError { 175 | return result, nil 176 | } 177 | case "a": 178 | networks := aNetworks(m) 179 | if ipInNetworks(parsedIP, networks) { 180 | return m.Result, nil 181 | } 182 | case "mx": 183 | networks := mxNetworks(m) 184 | if ipInNetworks(parsedIP, networks) { 185 | return m.Result, nil 186 | } 187 | case "ptr": 188 | if testPTR(m, ip) { 189 | return m.Result, nil 190 | } 191 | default: 192 | network, err := networkCIDR(m.Domain, m.Prefix) 193 | if err == nil { 194 | if network.Contains(parsedIP) { 195 | return m.Result, nil 196 | } 197 | } 198 | } 199 | 200 | return None, ErrNoMatch 201 | } 202 | 203 | // NewMechanism creates a new Mechanism struct using the given string and 204 | // domain name. When the mechanism does not define the domain, the provided 205 | // domain is used as the default. 206 | func NewMechanism(str, domain string) (Mechanism, error) { 207 | var m Mechanism 208 | var err error 209 | 210 | switch string(str[0]) { 211 | case "-": 212 | m, err = parseMechanism(Fail, str[1:], domain) 213 | case "~": 214 | m, err = parseMechanism(SoftFail, str[1:], domain) 215 | case "+": 216 | m, err = parseMechanism(Pass, str[1:], domain) 217 | case "?": 218 | m, err = parseMechanism(Neutral, str[1:], domain) 219 | default: 220 | m, err = parseMechanism(Pass, str, domain) 221 | } 222 | 223 | return m, err 224 | } 225 | 226 | func parseMechanism(r Result, str, domain string) (Mechanism, error) { 227 | var m Mechanism 228 | var n string 229 | var d string 230 | var p string 231 | 232 | ci := strings.Index(str, ":") 233 | pi := strings.Index(str, "/") 234 | ei := strings.Index(str, "=") 235 | 236 | switch { 237 | case ei != -1: 238 | n = str[:ei] 239 | d = str[ei+1:] 240 | 241 | // Domain should not be empty 242 | if d == "" { 243 | return m, ErrInvalidMechanism 244 | } 245 | case ci != -1 && pi != -1 && ci < pi: // name:domain/prefix 246 | n = str[:ci] 247 | d = str[ci+1 : pi] 248 | p = str[pi+1:] 249 | 250 | // Domain and prefix should not be empty 251 | if d == "" || p == "" { 252 | return m, ErrInvalidMechanism 253 | } 254 | case ci != -1: // name:domain 255 | n = str[:ci] 256 | d = str[ci+1:] 257 | // Domain should not be empty 258 | if d == "" { 259 | return m, ErrInvalidMechanism 260 | } 261 | case pi != -1: // name/prefix 262 | n = str[:pi] 263 | d = domain 264 | p = str[pi+1:] 265 | 266 | // Prefix should not be empty 267 | if p == "" { 268 | return m, ErrInvalidMechanism 269 | } 270 | default: // name 271 | n = str 272 | d = domain 273 | } 274 | 275 | m.Result = r 276 | m.Domain = d 277 | m.Name = n 278 | m.Prefix = p 279 | 280 | return m, nil 281 | } 282 | -------------------------------------------------------------------------------- /mechanism_test.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type mechtest struct { 8 | raw string 9 | name string 10 | domain string 11 | prefix string 12 | result Result 13 | } 14 | 15 | func TestValidMechanism(t *testing.T) { 16 | tests := []string{ 17 | "ip4:", 18 | "include:", 19 | "ip4:127.0.0.1/", 20 | "ip4:/", 21 | "ip4/:", 22 | "/:", 23 | ":/", 24 | "redirect=", 25 | "=", 26 | } 27 | 28 | for _, expected := range tests { 29 | _, err := NewMechanism(expected, "domain") 30 | if err == nil { 31 | t.Log("Analyzing", expected) 32 | t.Error("Expecting invalid mechanism") 33 | } 34 | } 35 | } 36 | 37 | func TestNewMechanism(t *testing.T) { 38 | tests := []mechtest{ 39 | mechtest{"+all", "all", domain, "", Pass}, 40 | mechtest{"-ip6:1080::8:800:68.0.3.1", "ip6", "1080::8:800:68.0.3.1", "", Fail}, 41 | mechtest{"~ip6:1080::8:800:68.0.3.1/96", "ip6", "1080::8:800:68.0.3.1", "96", SoftFail}, 42 | mechtest{"?ip4:192.168.0.1", "ip4", "192.168.0.1", "", Neutral}, 43 | mechtest{"ip4:192.168.0.1/16", "ip4", "192.168.0.1", "16", Pass}, 44 | mechtest{"-a", "a", domain, "", Fail}, 45 | mechtest{"~a/24", "a", domain, "24", SoftFail}, 46 | mechtest{"?a:offsite.example.com", "a", "offsite.example.com", "", Neutral}, 47 | mechtest{"a:offsite.example.com/24", "a", "offsite.example.com", "24", Pass}, 48 | mechtest{"mx", "mx", domain, "", Pass}, 49 | mechtest{"mx/24", "mx", domain, "24", Pass}, 50 | mechtest{"mx:deferrals.domain.com", "mx", "deferrals.domain.com", "", Pass}, 51 | mechtest{"mx:deferrals.domain.com/24", "mx", "deferrals.domain.com", "24", Pass}, 52 | mechtest{"ptr", "ptr", domain, "", Pass}, 53 | mechtest{"ptr:domain.name", "ptr", "domain.name", "", Pass}, 54 | mechtest{"include:domain.name", "include", "domain.name", "", Pass}, 55 | mechtest{"exists:domain.name", "exists", "domain.name", "", Pass}, 56 | mechtest{"redirect:domain.name", "redirect", "domain.name", "", Pass}, 57 | } 58 | 59 | for _, expected := range tests { 60 | actual, _ := NewMechanism(expected.raw, domain) 61 | 62 | if expected.name != actual.Name { 63 | t.Error("Expected", expected.name, "got", actual.Name, ":", expected.raw) 64 | } 65 | if expected.domain != actual.Domain { 66 | t.Error("Expected", expected.domain, "got", actual.Domain, ":", expected.raw) 67 | } 68 | if expected.prefix != actual.Prefix { 69 | t.Error("Expected", expected.prefix, "got", actual.Prefix, ":", expected.raw) 70 | } 71 | if expected.result != actual.Result { 72 | t.Error("Expected", expected.prefix, "got", actual.Prefix, ":", expected.raw) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /net.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | func networkCIDR(ip, prefix string) (*net.IPNet, error) { 10 | if prefix == "" { 11 | ip := net.ParseIP(ip) 12 | 13 | if ip.To4() != nil { 14 | prefix = "32" 15 | } else { 16 | prefix = "128" 17 | } 18 | } 19 | 20 | cidrStr := fmt.Sprintf("%s/%s", ip, prefix) 21 | 22 | _, network, err := net.ParseCIDR(cidrStr) 23 | return network, err 24 | } 25 | 26 | func ipInNetworks(ip net.IP, networks []*net.IPNet) bool { 27 | for _, network := range networks { 28 | if network.Contains(ip) { 29 | return true 30 | } 31 | } 32 | 33 | return false 34 | } 35 | 36 | func buildNetworks(ips []string, prefix string) []*net.IPNet { 37 | var networks []*net.IPNet 38 | 39 | for _, ip := range ips { 40 | network, err := networkCIDR(ip, prefix) 41 | if err == nil { 42 | networks = append(networks, network) 43 | } 44 | } 45 | 46 | return networks 47 | } 48 | 49 | func aNetworks(m *Mechanism) []*net.IPNet { 50 | ips, _ := net.LookupHost(m.Domain) 51 | 52 | return buildNetworks(ips, m.Prefix) 53 | } 54 | 55 | func mxNetworks(m *Mechanism) []*net.IPNet { 56 | var networks []*net.IPNet 57 | 58 | mxs, _ := net.LookupMX(m.Domain) 59 | 60 | for _, mx := range mxs { 61 | ips, _ := net.LookupHost(mx.Host) 62 | networks = append(networks, buildNetworks(ips, m.Prefix)...) 63 | } 64 | 65 | return networks 66 | } 67 | 68 | func testPTR(m *Mechanism, ip string) bool { 69 | names, err := net.LookupAddr(ip) 70 | 71 | if err != nil { 72 | return false 73 | } 74 | 75 | for _, name := range names { 76 | if strings.HasSuffix(name, m.Domain) { 77 | return true 78 | } 79 | } 80 | 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /spf.go: -------------------------------------------------------------------------------- 1 | // Package spf can parse an SPF record and determine if a given IP address is 2 | // allowed to send email based on that record. SPF can handle all of the 3 | // mechanisms defined at http://www.openspf.org/SPF_Record_Syntax. The redirect 4 | // mechanism is ignored. 5 | package spf 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "net" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | MaxCount = 10 17 | ) 18 | 19 | var ( 20 | ErrNoRecord = errors.New("No SPF Record found.") 21 | ErrFailedLookup = errors.New("DNS Lookup failed.") 22 | ErrInvalidSPF = errors.New("Invalid SPF string.") 23 | ErrIncludeLoop = errors.New("Include loop detected.") 24 | ErrInvalidMechanism = errors.New("Invalid mechanism in SPF string.") 25 | ErrMaxCount = errors.New("Exceeded maximum lookups.") 26 | ) 27 | 28 | // SPF represents an SPF record for a particular Domain. The SPF record 29 | // holds all of the Allow, Deny, and Neutral mechanisms. 30 | type SPF struct { 31 | Raw string 32 | Domain string 33 | Version string 34 | Mechanisms []Mechanism 35 | Count int 36 | } 37 | 38 | // Test evaluates each mechanism to determine the result for the client. 39 | // Mechanisms are evaluated in order until one of them provides a valid 40 | // result. If no valid results are provided, the default result of "Neutral" 41 | // is returned. 42 | func (s *SPF) Test(ip string) Result { 43 | for _, m := range s.Mechanisms { 44 | result, err := m.Evaluate(ip, s.Count) 45 | if err == nil { 46 | return result 47 | } 48 | } 49 | 50 | return Neutral 51 | } 52 | 53 | // Return an SPF record as a string. 54 | func (s *SPF) String() string { 55 | var buf bytes.Buffer 56 | 57 | buf.WriteString(fmt.Sprintf("Raw: %s\n", s.Raw)) 58 | buf.WriteString(fmt.Sprintf("Domain: %s\n", s.Domain)) 59 | buf.WriteString(fmt.Sprintf("Version: %s\n", s.Version)) 60 | 61 | buf.WriteString("Mechanisms:\n") 62 | for _, m := range s.Mechanisms { 63 | buf.WriteString(fmt.Sprintf("\t%s\n", m.String())) 64 | } 65 | 66 | return buf.String() 67 | } 68 | 69 | // SPFString returns a formatted SPF object as a string suitable for use in a 70 | // TXT record. 71 | func (s *SPF) SPFString() string { 72 | var buf bytes.Buffer 73 | 74 | buf.WriteString(fmt.Sprintf("v=%s", s.Version)) 75 | for _, m := range s.Mechanisms { 76 | buf.WriteString(fmt.Sprintf(" %s", m.SPFString())) 77 | } 78 | 79 | return buf.String() 80 | } 81 | 82 | func getSPFRecord(domain string) (string, error) { 83 | var spfText string 84 | 85 | // DNS errors during domain name lookup should result in "TempError". 86 | records, err := net.LookupTXT(domain) 87 | if err != nil { 88 | return "", ErrFailedLookup 89 | } 90 | 91 | // Find the SPF record among the TXT records for the domain. 92 | for _, record := range records { 93 | if strings.HasPrefix(record, "v=spf1") { 94 | spfText = record 95 | break 96 | } 97 | } 98 | 99 | return spfText, nil 100 | } 101 | 102 | // Create a new SPF record for the given domain using the provided string. If 103 | // the provided string is not valid an error is returned. 104 | func NewSPF(domain, record string, count int) (SPF, error) { 105 | var spf SPF 106 | 107 | if record == "" { 108 | spfText, err := getSPFRecord(domain) 109 | if err != nil { 110 | return spf, err 111 | } 112 | 113 | if spfText == "" { 114 | return spf, ErrNoRecord 115 | } 116 | 117 | record = spfText 118 | } 119 | 120 | spf.Count = count 121 | spf.Raw = record 122 | spf.Domain = domain 123 | 124 | if !strings.HasPrefix(record, "v=spf1") { 125 | return spf, ErrInvalidSPF 126 | } 127 | 128 | for _, f := range strings.Fields(record) { 129 | switch { 130 | case strings.HasPrefix(f, "v="): 131 | spf.Version = f[2:] 132 | default: 133 | mechanism, err := NewMechanism(f, domain) 134 | 135 | if err != nil { 136 | return spf, err 137 | } 138 | 139 | if !mechanism.Valid() { 140 | return spf, ErrInvalidMechanism 141 | } 142 | 143 | switch mechanism.Name { 144 | case "include": 145 | spf.Count = spf.Count + 1 146 | if mechanism.Domain == domain { 147 | return spf, ErrIncludeLoop 148 | } 149 | case "redirect", "exists", "a", "mx", "ptr": 150 | spf.Count = spf.Count + 1 151 | default: 152 | // No action 153 | } 154 | 155 | spf.Mechanisms = append(spf.Mechanisms, mechanism) 156 | } 157 | } 158 | 159 | if spf.Count >= MaxCount { 160 | return spf, ErrMaxCount 161 | } 162 | 163 | return spf, nil 164 | } 165 | 166 | /* 167 | Exported functions. 168 | */ 169 | 170 | // SPFTest determines the clients sending status for the given email addres. 171 | // 172 | // SPFTest will return one of the following results: 173 | // Pass, Fail, SoftFail, Neutral, None, TempError, or PermError 174 | func SPFTest(ip, email string) (Result, error) { 175 | var domain string 176 | 177 | // Get domain name from email address. 178 | if strings.Contains(email, "@") { 179 | parts := strings.Split(email, "@") 180 | domain = parts[1] 181 | } else { 182 | return None, errors.New("Email address must contain an @ sign.") 183 | } 184 | 185 | spfText, err := getSPFRecord(domain) 186 | if err != nil { 187 | return TempError, err 188 | } 189 | 190 | // No SPF record should result in None. 191 | if spfText == "" { 192 | return None, nil 193 | } 194 | 195 | // Create a new SPF struct 196 | spf, err := NewSPF(domain, spfText, 0) 197 | if err != nil { 198 | return PermError, err 199 | } 200 | 201 | return spf.Test(ip), nil 202 | } 203 | -------------------------------------------------------------------------------- /spf_test.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const domain = "google.com" 8 | 9 | type spferror struct { 10 | domain string 11 | raw string 12 | } 13 | 14 | type spftest struct { 15 | server string 16 | email string 17 | result Result 18 | } 19 | 20 | type spfstr struct { 21 | raw string 22 | expected string 23 | } 24 | 25 | func TestNewSPF(t *testing.T) { 26 | errorTests := []spferror{ 27 | spferror{"google.com", "somestring"}, 28 | spferror{"google.com", "v=spf1 include:_spf.google.com ~all -none"}, 29 | spferror{"google.com", "v=spf1 include:google.com"}, 30 | } 31 | 32 | for _, expected := range errorTests { 33 | _, err := NewSPF(expected.domain, expected.raw, 0) 34 | 35 | if err == nil { 36 | t.Log("Analyzing:", expected.raw) 37 | t.Error("Expected error got nil") 38 | } 39 | } 40 | } 41 | 42 | func TestSPFTest(t *testing.T) { 43 | tests := []spftest{ 44 | spftest{"127.0.0.1", "info@google.com", SoftFail}, 45 | spftest{"74.125.141.26", "info@google.com", Pass}, 46 | spftest{"35.190.247.0", "info@google.com", Pass}, 47 | spftest{"172.217.0.0", "info@_netblocks3.google.com", Pass}, 48 | spftest{"172.217.0.0", "info@google.com", Pass}, 49 | spftest{"1.1.1.1", "admin@pchome.com.tw", PermError}, 50 | } 51 | 52 | for _, expected := range tests { 53 | actual, err := SPFTest(expected.server, expected.email) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | 58 | if actual != expected.result { 59 | t.Error("For", expected.server, "at", expected.email, "Expected", expected.result, "got", actual) 60 | } 61 | } 62 | } 63 | 64 | func TestSPFString(t *testing.T) { 65 | tests := []spfstr{ 66 | spfstr{ 67 | "v=spf1 ip4:45.55.100.54 ip4:192.241.161.190 ip4:188.226.145.26 ~all", 68 | "v=spf1 ip4:45.55.100.54 ip4:192.241.161.190 ip4:188.226.145.26 ~all", 69 | }, 70 | spfstr{ 71 | "v=spf1 ip4:127.0.0.0/8 -ip4:127.0.0.1 ?ip4:127.0.0.2 -all", 72 | "v=spf1 ip4:127.0.0.0/8 -ip4:127.0.0.1 ?ip4:127.0.0.2 -all", 73 | }, 74 | spfstr{ 75 | "v=spf1 redirect=_spf.sample.invalid", 76 | "v=spf1 redirect=_spf.sample.invalid", 77 | }, 78 | } 79 | 80 | for _, tcase := range tests { 81 | s, err := NewSPF("domain", tcase.raw, 0) 82 | if err != nil { 83 | t.Log("Analyzing", tcase.raw) 84 | t.Error(err) 85 | } 86 | 87 | r := s.SPFString() 88 | if r != tcase.expected { 89 | t.Log("Analyzing", tcase.raw) 90 | t.Error("Expected", tcase.expected, "got", r) 91 | } 92 | } 93 | } 94 | --------------------------------------------------------------------------------