Skip to content

Instantly share code, notes, and snippets.

@nesv
Last active June 30, 2016 19:07
Show Gist options
  • Save nesv/a8a4b9d76b01b66b6342f06c8b7176f3 to your computer and use it in GitHub Desktop.
Save nesv/a8a4b9d76b01b66b6342f06c8b7176f3 to your computer and use it in GitHub Desktop.
Parse SPF records
package spf
import (
"errors"
"fmt"
)
var (
ErrInvalidPrefix = errors.New("invalid or missing spf prefix")
)
func NewMalformedMechanismError(m string) error {
return fmt.Errorf("malformed mechanism %q", m)
}
func NewUnknownMechanismTypeError(m string) error {
return fmt.Errorf("unknown mechanism type: %v", m)
}
func NewMalformedModifierError(modifier string) error {
return fmt.Errorf("malformed modifier %q", modifier)
}
func NewDuplicateModifierError(m string) error {
return fmt.Errorf("duplicate modifier %q", m)
}
package spf
import (
"bytes"
"fmt"
"net"
"strings"
)
func runTests() {
tests := []string{
"v=spf1 ip4:192.168.0.1/16 -all",
"v=spf1 a mx -all",
"v=spf1 exp=refused.example.com -all",
"v=spf1 redirect=bounce.example.com",
"v=spf1 fart:narf/14 +all",
"v=spf1 ip4:10.0.2.4 -all",
"?all",
}
for _, t := range tests {
r, err := Parse(t)
if err != nil {
fmt.Printf("%q -> error: %v\n", t, err)
continue
}
fmt.Printf("%q -> %q\n", t, r)
}
}
type Qualifier rune
const (
Pass Qualifier = '+' // Note pass is a default qualifier should no other qualifier be explicitly stated
Fail = '-'
SoftFail = '~'
Neutral = '?'
)
type MechanismType string
const (
All MechanismType = "all"
IP4 = "ip4"
IP6 = "ip6"
A = "a"
MX = "mx"
PTR = "ptr"
Exists = "exists"
Include = "include"
)
var MechanismTypes = map[MechanismType]struct{}{
All: struct{}{},
IP4: struct{}{},
IP6: struct{}{},
A: struct{}{},
MX: struct{}{},
PTR: struct{}{},
Exists: struct{}{},
Include: struct{}{},
}
type Mechanism struct {
Qualifier Qualifier
Type MechanismType
Value string
}
// Expand returns an expanded version of the mechanism's value, and optionally
// prefixes it with the qualifier if the qualifier is any value other than
// Pass.
//
// When the mechanism's type is A, MX, PTR, Exists, or Include, this method may
// return an error given that expanding these values may require network
// activity.
func (m Mechanism) Expand() (string, error) {
return "", errors.New("not implemented")
}
func (m Mechanism) String() string {
if m.Value == "" {
return fmt.Sprintf("%s%s", string(m.Qualifier), m.Type)
}
return fmt.Sprintf("%s%s:%s", string(m.Qualifier), m.Type, m.Value)
}
// ParseMechanism parses the given string representation of an SPF mechanism, m.
func ParseMechanism(m string) (Mechanism, error) {
mech := Mechanism{}
// Check to see if the given string has a qualifier before.
// If not, then use the default qualifier "+".
if i := strings.IndexAny(m, "+-~?"); i == -1 {
mech.Qualifier = Pass
} else {
mech.Qualifier = Qualifier(m[i])
// Alter the input string to not include the qualifier.
m = m[i+1:]
}
// Check to see if we have a separator anywhere.
if sep := strings.Index(m, ":"); sep < 0 {
switch v := MechanismType(m); v {
case All, A, MX, PTR:
// These mechanism types can be bare.
mech.Type = v
default:
return Mechanism{}, NewMalformedMechanismError(m)
}
} else if sep == 0 {
return Mechanism{}, NewMalformedMechanismError(m)
} else {
mt := MechanismType(m[:sep])
if _, ok := MechanismTypes[mt]; !ok {
return Mechanism{}, NewUnknownMechanismTypeError(m[:sep])
}
mech.Type = mt
v, err := checkMechanismValue(mt, m[sep+1:])
if err != nil {
return Mechanism{}, err
}
mech.Value = v
}
return mech, nil
}
// checkMechanismValue validates that v is of an acceptable format for t,
// and returns a canonicalized form of v.
func checkMechanismValue(t MechanismType, v string) (string, error) {
switch t {
case IP4, IP6:
return checkIP(v)
}
return v, nil
}
func checkIP(s string) (string, error) {
ip := net.ParseIP(s)
if ip != nil {
if ip.To4() != nil {
return ip.String() + "/32", nil
}
return ip.String() + "/128", nil
}
_, ipnet, err := net.ParseCIDR(s)
return ipnet.String(), err
}
func NewMechanism(q Qualifier, t MechanismType, v string) Mechanism {
return Mechanism{
Qualifier: q,
Type: t,
Value: v,
}
}
func hasPrefix(txt string) bool {
return strings.HasPrefix(txt, "v=spf1 ")
}
func isModifier(s string) bool {
return strings.HasPrefix(s, "exp=") || strings.HasPrefix(s, "redirect=")
}
func splitModifier(m string) (name, value string, valid bool) {
s := strings.SplitN(m, "=", 2)
switch len(s) {
case 2:
return s[0], s[1], true
case 1:
return s[0], "", false
}
return "", "", false
}
func Parse(spfStr string) (SPF, error) {
if !hasPrefix(spfStr) {
return SPF{}, ErrInvalidPrefix
}
var (
mechanisms = make([]Mechanism, 0)
modifiers = make(map[string]string)
)
for _, f := range strings.Fields(spfStr)[1:] {
if isModifier(f) {
k, v, ok := splitModifier(f)
if !ok {
return SPF{}, NewMalformedModifierError(f)
}
if _, exists := modifiers[k]; exists {
return SPF{}, NewDuplicateModifierError(k)
} else {
modifiers[k] = v
}
continue
}
m, err := ParseMechanism(f)
if err != nil {
return SPF{}, err
}
mechanisms = append(mechanisms, m)
}
return SPF{
Mechanisms: mechanisms,
Modifiers: modifiers,
}, nil
}
type SPF struct {
Mechanisms []Mechanism
Modifiers map[string]string
}
func (s SPF) String() string {
buf := new(bytes.Buffer)
fmt.Fprint(buf, "v=spf1")
for k, v := range s.Modifiers {
fmt.Fprintf(buf, " %s=%s", k, v)
}
for _, m := range s.Mechanisms {
fmt.Fprintf(buf, " %s", m)
}
return buf.String()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment