Skip to content
This repository was archived by the owner on Sep 2, 2025. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 117 additions & 47 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,13 @@ func (p *parser) checkHost(ip net.IP, domain, sender string) (r Result, expl str
for _, t := range u.mechanisms {
p.fireUnusedDirective(t)
}
p.fireUnusedDirective(u.redirect)
// Is inspect mode tokens are handled as is; no sorting applied
// and thus u.mechanisms have redirect, if it is in the tree and has not been met
if !p.ignoreMatches {
p.fireUnusedDirective(u.redirect)
}
}()

/*
* As per RFC 7208 Section 4.3:
* If the <domain> is malformed (e.g., label longer than 63
Expand Down Expand Up @@ -241,58 +246,115 @@ type unused struct {
redirect *token
}

// check aggregates all steps required for SPF evaluation.
// After lexing and tokenizing step it sorts tokens (and returns Permerror if
// there is any syntax error) and starts evaluating
// each token (from left to right). Once a token matches parse stops and
// returns matched result.
func (p *parser) check() (Result, string, unused, error) {
p.visited.push(p.domain)
defer p.visited.pop()

p.fireSPFRecord(p.query)
tokens := lex(p.query)
func (p *parser) observe(tokens []*token) (Result, string, unused, error) {
mechanisms, _, _, unknownModifiers, err := sortTokens(tokens)
if err != nil {
return Permerror, "", unused{mechanisms, nil}, err
}

var (
result = Neutral
matches bool
token *token
i int
extras *ResponseExtras
token *token
i int
result = Neutral
)

mechanisms, redirect, explanation, unknownModifiers, err := sortTokens(tokens)
for i, token = range tokens {
match := false

switch token.mechanism {
case tVersion:
match, result, err = p.parseVersion(token)
case tAll:
match, result, err = p.parseAll(token)
case tA:
match, result, _, err = p.parseA(token)
case tIP4:
match, result, err = p.parseIP4(token)
case tIP6:
match, result, err = p.parseIP6(token)
case tMX:
match, result, _, err = p.parseMX(token)
case tInclude:
match, result, err = p.parseInclude(token)
case tExists:
match, result, _, err = p.parseExists(token)
case tPTR:
match, result, _, err = p.parsePtr(token)
case tRedirect:
result, _ = p.handleRedirect(token)
case tExp:
exp, _ := p.handleExplanation(token)
p.fireDirective(token, exp)
default:
p.fireDirective(token, "")
}

// Store the first match result if not already set
if match {
p.fireFirstMatch(result, err)
}

p.fireNonMatch(token, result, err)

// in walker-mode we want to count number of errors and check the counter against some threshold
if p.stopAtError != nil && p.stopAtError(err) {
return unreliableResult, "", unused{tokens[i+1:], nil}, ErrTooManyErrors
}

// all expected errors should be thrown with match=true
// others are being registered by listener
}

for i, token = range unknownModifiers {
p.fireDirective(token, "")
}

return unreliableResult, "", unused{}, ErrUnreliableResult
}

func (p *parser) evaluate(tokens []*token) (Result, string, unused, error) {
mechanisms, redirect, explanation, _, err := sortTokens(tokens)
if err != nil {
return Permerror, "", unused{mechanisms, redirect}, err
}

var all bool
var (
token *token
i int
all bool
result = Neutral
)

for i, token = range mechanisms {
var (
match bool
extras *ResponseExtras
)
switch token.mechanism {
case tVersion:
matches, result, err = p.parseVersion(token)
match, result, err = p.parseVersion(token)
case tAll:
all = true
matches, result, err = p.parseAll(token)
match, result, err = p.parseAll(token)
case tA:
matches, result, extras, err = p.parseA(token)
match, result, extras, err = p.parseA(token)
case tIP4:
matches, result, err = p.parseIP4(token)
match, result, err = p.parseIP4(token)
case tIP6:
matches, result, err = p.parseIP6(token)
match, result, err = p.parseIP6(token)
case tMX:
matches, result, extras, err = p.parseMX(token)
match, result, extras, err = p.parseMX(token)
case tInclude:
matches, result, err = p.parseInclude(token)
match, result, err = p.parseInclude(token)
case tExists:
matches, result, extras, err = p.parseExists(token)
match, result, extras, err = p.parseExists(token)
case tPTR:
matches, result, extras, err = p.parsePtr(token)
match, result, extras, err = p.parsePtr(token)
default:
p.fireDirective(token, "")
}

if !p.ignoreMatches && matches {
if match {
var s string
if result == Fail && explanation != nil {
s, err = p.handleExplanation(explanation)
Expand All @@ -301,34 +363,37 @@ func (p *parser) check() (Result, string, unused, error) {
return result, s, unused{mechanisms[i+1:], redirect}, err
}

// Store the first match result if not already set
if p.ignoreMatches && matches {
p.fireFirstMatch(result, err)
}

p.fireNonMatch(token, result, err)

// in walker-mode we want to count number of errors and check the counter against some threshold
if p.ignoreMatches && p.stopAtError != nil && p.stopAtError(err) {
return unreliableResult, "", unused{mechanisms[i+1:], redirect}, ErrTooManyErrors
}

// all expected errors should be thrown with matches=true
// all expected errors should be thrown with match=true
// others are being registered by listener
}

if !all {
result, err = p.handleRedirect(redirect)
}

return result, "", unused{}, err
}

// check aggregates all steps required for SPF evaluation.
// After lexing and tokenizing step it sorts tokens (and returns Permerror if
// there is any syntax error) and starts evaluating
// each token (from left to right). Once a token matches parse stops and
// returns matched result.
func (p *parser) check() (Result, string, unused, error) {
p.visited.push(p.domain)
defer p.visited.pop()

p.fireSPFRecord(p.query)

tokens := lex(p.query)

if p.ignoreMatches {
for i, token = range unknownModifiers {
p.fireDirective(token, "")
}
return unreliableResult, "", unused{}, ErrUnreliableResult
return p.observe(tokens)
}

return result, "", unused{}, err
return p.evaluate(tokens)
}

func (p *parser) fireCheckHost(ip net.IP, domain, sender string) {
Expand Down Expand Up @@ -472,7 +537,7 @@ func (p *parser) parseVersion(t *token) (bool, Result, error) {
if t.value == "spf1" {
return false, None, nil
}
return true, Permerror, NewSpfError(spferr.KindSyntax, fmt.Errorf("invalid spf qualifier: %v", t.value), t)
return true, Permerror, NewSpfError(spferr.KindSyntax, fmt.Errorf("invalid version: %v", t.value), t)
}

func (p *parser) parseAll(t *token) (bool, Result, error) {
Expand Down Expand Up @@ -644,7 +709,7 @@ func (p *parser) parseInclude(t *token) (bool, Result, error) {
| | |
| none | return permerror |
+---------------------------------+---------------------------------+
*/ if err != nil {
*/if err != nil {
err = wrap(t, err)
}

Expand Down Expand Up @@ -777,6 +842,11 @@ func (p *parser) handleRedirect(t *token) (Result, error) {
redirectDomain := NormalizeFQDN(domain)

p.fireDirective(t, redirectDomain)
if p.ignoreMatches {
// In inspect mode we only register modifier presence, w/o actual handling
return unreliableResult, ErrUnreliableResult
}

if err != nil {
return Permerror, NewSpfError(spferr.KindSyntax, err, t)
}
Expand Down