2017-05-26 00:27:36 +08:00
|
|
|
package spflib
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
2017-09-30 03:30:36 +08:00
|
|
|
"bytes"
|
|
|
|
|
|
|
|
"io"
|
2018-02-06 05:17:20 +08:00
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2017-05-26 00:27:36 +08:00
|
|
|
)
|
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// SPFRecord stores the parts of an SPF record.
|
2017-05-26 00:27:36 +08:00
|
|
|
type SPFRecord struct {
|
2017-09-30 03:30:36 +08:00
|
|
|
Parts []*SPFPart
|
|
|
|
}
|
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// Lookups returns the number of DNS lookups required by s.
|
2017-09-30 03:30:36 +08:00
|
|
|
func (s *SPFRecord) Lookups() int {
|
|
|
|
count := 0
|
|
|
|
for _, p := range s.Parts {
|
|
|
|
if p.IsLookup {
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
if p.IncludeRecord != nil {
|
|
|
|
count += p.IncludeRecord.Lookups()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// SPFPart stores a part of an SPF record, with attributes.
|
2017-05-26 00:27:36 +08:00
|
|
|
type SPFPart struct {
|
|
|
|
Text string
|
2017-09-30 03:30:36 +08:00
|
|
|
IsLookup bool
|
2017-05-26 00:27:36 +08:00
|
|
|
IncludeRecord *SPFRecord
|
2017-09-30 03:30:36 +08:00
|
|
|
IncludeDomain string
|
2017-05-26 02:25:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
var qualifiers = map[byte]bool{
|
|
|
|
'?': true,
|
|
|
|
'~': true,
|
|
|
|
'-': true,
|
|
|
|
'+': true,
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// Parse parses a raw SPF record.
|
2017-09-30 03:30:36 +08:00
|
|
|
func Parse(text string, dnsres Resolver) (*SPFRecord, error) {
|
2017-05-26 00:27:36 +08:00
|
|
|
if !strings.HasPrefix(text, "v=spf1 ") {
|
2018-02-06 05:17:20 +08:00
|
|
|
return nil, errors.Errorf("Not an spf record")
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
|
|
|
parts := strings.Split(text, " ")
|
|
|
|
rec := &SPFRecord{}
|
2019-06-20 01:46:56 +08:00
|
|
|
for pi, part := range parts[1:] {
|
2018-05-03 22:03:38 +08:00
|
|
|
if part == "" {
|
|
|
|
continue
|
|
|
|
}
|
2017-05-26 00:27:36 +08:00
|
|
|
p := &SPFPart{Text: part}
|
2017-05-26 02:25:39 +08:00
|
|
|
if qualifiers[part[0]] {
|
|
|
|
part = part[1:]
|
|
|
|
}
|
2017-05-26 00:27:36 +08:00
|
|
|
rec.Parts = append(rec.Parts, p)
|
2017-05-26 02:25:39 +08:00
|
|
|
if part == "all" {
|
2018-01-10 01:53:16 +08:00
|
|
|
// all. nothing else matters.
|
2017-05-26 00:27:36 +08:00
|
|
|
break
|
2017-05-26 02:25:39 +08:00
|
|
|
} else if strings.HasPrefix(part, "a") || strings.HasPrefix(part, "mx") {
|
2017-09-30 03:30:36 +08:00
|
|
|
p.IsLookup = true
|
2017-05-26 00:27:36 +08:00
|
|
|
} else if strings.HasPrefix(part, "ip4:") || strings.HasPrefix(part, "ip6:") {
|
2018-01-10 01:53:16 +08:00
|
|
|
// ip address, 0 lookups
|
2017-05-26 00:27:36 +08:00
|
|
|
continue
|
2019-06-20 01:46:56 +08:00
|
|
|
} else if strings.HasPrefix(part, "include:") || strings.HasPrefix(part, "redirect:") {
|
|
|
|
if strings.HasPrefix(part, "redirect:") {
|
|
|
|
// pi + 2: because pi starts at 0 when it iterates starting on parts[1],
|
|
|
|
// and because len(parts) is one bigger than the highest index.
|
|
|
|
if (pi + 2) != len(parts) {
|
|
|
|
return nil, errors.Errorf("%s must be last item", part)
|
|
|
|
}
|
|
|
|
p.IncludeDomain = strings.TrimPrefix(part, "redirect:")
|
|
|
|
} else {
|
|
|
|
p.IncludeDomain = strings.TrimPrefix(part, "include:")
|
|
|
|
}
|
2017-09-30 03:30:36 +08:00
|
|
|
p.IsLookup = true
|
|
|
|
if dnsres != nil {
|
|
|
|
subRecord, err := dnsres.GetSPF(p.IncludeDomain)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
p.IncludeRecord, err = Parse(subRecord, dnsres)
|
|
|
|
if err != nil {
|
2018-02-06 05:17:20 +08:00
|
|
|
return nil, errors.Errorf("In included spf: %s", err)
|
2017-09-30 03:30:36 +08:00
|
|
|
}
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
2018-08-02 22:57:41 +08:00
|
|
|
} else if strings.HasPrefix(part, "exists:") || strings.HasPrefix(part, "ptr:") {
|
2018-05-03 20:54:19 +08:00
|
|
|
p.IsLookup = true
|
2017-05-26 00:27:36 +08:00
|
|
|
} else {
|
2018-02-06 05:17:20 +08:00
|
|
|
return nil, errors.Errorf("Unsupported spf part %s", part)
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return rec, nil
|
|
|
|
}
|
|
|
|
|
2017-09-30 03:30:36 +08:00
|
|
|
func dump(rec *SPFRecord, indent string, w io.Writer) {
|
|
|
|
|
|
|
|
fmt.Fprintf(w, "%sTotal Lookups: %d\n", indent, rec.Lookups())
|
|
|
|
fmt.Fprint(w, indent+"v=spf1")
|
2017-05-26 00:27:36 +08:00
|
|
|
for _, p := range rec.Parts {
|
2017-09-30 03:30:36 +08:00
|
|
|
fmt.Fprint(w, " "+p.Text)
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
2017-09-30 03:30:36 +08:00
|
|
|
fmt.Fprintln(w)
|
2017-05-26 00:27:36 +08:00
|
|
|
indent += "\t"
|
|
|
|
for _, p := range rec.Parts {
|
2017-09-30 03:30:36 +08:00
|
|
|
if p.IsLookup {
|
|
|
|
fmt.Fprintln(w, indent+p.Text)
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
2017-05-26 02:25:39 +08:00
|
|
|
if p.IncludeRecord != nil {
|
2017-09-30 03:30:36 +08:00
|
|
|
dump(p.IncludeRecord, indent+"\t", w)
|
2017-05-26 00:27:36 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-09-30 03:30:36 +08:00
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// Print prints an SPFRecord.
|
|
|
|
func (s *SPFRecord) Print() string {
|
2017-09-30 03:30:36 +08:00
|
|
|
w := &bytes.Buffer{}
|
2018-01-10 01:53:16 +08:00
|
|
|
dump(s, "", w)
|
2017-09-30 03:30:36 +08:00
|
|
|
return w.String()
|
|
|
|
}
|