Class: MailerToGo::SPF::Term

Inherits:
Object
  • Object
show all
Defined in:
lib/mailertogo/spf/term.rb

Overview

One term of an SPF record: what it is, and what it costs.

Record reads the raw text — split a record into terms, strip a qualifier, name a mechanism. This is the same knowledge asked as questions instead of pattern-matched at every call site: term.include?, term.querying?, term.unreachable?. Nothing here re-parses what Record already parses; it builds on it, which is the only way two readings of the same term cannot drift apart.

Deliberately NOT here: any English sentence about what a term means to a human. A description is product voice — it belongs to whoever is writing to their own customers, in their own words, and no gem should be in the business of writing it. What a consumer wants is a place to hang that copy, so Term is designed to be subclassed: every predicate it answers is a predicate your sentence can switch on, and both #priced and Record.parse_terms(record, term_class:) build your subclass rather than this class.

class AnnotatedTerm < MailerToGo::SPF::Term
def meaning
  return "Everything not matched above is marked, not rejected." if all? && qualifier == "~"
  
end
end

Immutable, and frozen on construction: pricing a term builds a copy via #priced rather than mutating it, so a term can never be observed half-priced.

Constant Summary collapse

QUERYING =

RFC 7208 §4.6.4 — the mechanisms that cost a DNS query. The lookup budget is a count of these (plus the redirect= modifier).

%w[include a mx ptr exists].freeze
QUALIFIERS =

§4.6.2 — the qualifier a mechanism may carry, and what a match under it means. "+" when absent.

{ "+" => :pass, "-" => :fail, "~" => :softfail, "?" => :neutral }.freeze
MECHANISMS =

Every mechanism SPF defines. Anything else in mechanism position is a syntax error (see #unknown?), not a term that quietly does nothing.

(QUERYING + %w[ip4 ip6 all]).freeze
MODIFIER =

A modifier (redirect=, exp=, or an unknown one) rather than a mechanism. Modifiers are name=value and position-independent (§4.6.1).

/\A([a-z][a-z0-9\-_.]*)=(.*)\z/i
ALL =

all, with whatever junk is glued onto it. The junk is real and surprisingly common — records ending ~all;google-site-verification=… exist in the wild, where the ;-joined fragment is not a valid SPF term at all. Matching it HERE is what keeps such a record's terminal term recognised as an all (which ends evaluation, and whose qualifier is the domain's policy) rather than filed as junk and the record read as having no all whatsoever.

/\Aall([^a-z0-9].*)?\z/i

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw:, position: 0, unreachable: false, lookups: nil, running_total: nil) ⇒ Term

raw — the term exactly as published. position — 1-based index in the record; 0 when it was not parsed as part of one. For display, and for nothing else. unreachable — the term is never evaluated: it sits after all, or it is a redirect= in a record that has an all (§5.1/§6.1). Such a term authorises nothing and costs nothing. lookups — DNS lookups this term costs a receiver: itself, plus everything the record it pulls in costs. nil until priced by ChainAudit. running_total — the record's cumulative cost through this term.



74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/mailertogo/spf/term.rb', line 74

def initialize(raw:, position: 0, unreachable: false, lookups: nil, running_total: nil)
  @raw = raw.to_s
  @position = position
  @unreachable = unreachable
  @lookups = lookups
  @running_total = running_total
  @bare = Record.strip_qualifier(@raw)
  @qualifier = Record.qualifier_of(@raw)
  @modifier_match = MODIFIER.match(@bare)
  @all_match = ALL.match(@bare)
  @mechanism = @modifier_match ? nil : Record.mechanism_of(@bare)
  freeze
end

Instance Attribute Details

#lookupsObject (readonly)

Returns the value of attribute lookups.



61
62
63
# File 'lib/mailertogo/spf/term.rb', line 61

def lookups
  @lookups
end

#positionObject (readonly)

Returns the value of attribute position.



61
62
63
# File 'lib/mailertogo/spf/term.rb', line 61

def position
  @position
end

#qualifierObject (readonly)

Returns the value of attribute qualifier.



61
62
63
# File 'lib/mailertogo/spf/term.rb', line 61

def qualifier
  @qualifier
end

#rawObject (readonly)

Returns the value of attribute raw.



61
62
63
# File 'lib/mailertogo/spf/term.rb', line 61

def raw
  @raw
end

#running_totalObject (readonly)

Returns the value of attribute running_total.



61
62
63
# File 'lib/mailertogo/spf/term.rb', line 61

def running_total
  @running_total
end

Instance Method Details

#all?Boolean

Returns:

  • (Boolean)


121
# File 'lib/mailertogo/spf/term.rb', line 121

def all? = mechanism == "all"

#all_suffixObject

The junk glued onto an all (~all;google-site-verification=…), or nil. Worth surfacing: it is inside the record rather than beside it, so it is not the second TXT record its author thought they were writing.



150
151
152
153
# File 'lib/mailertogo/spf/term.rb', line 150

def all_suffix
  suffix = @all_match && @all_match[1].to_s.strip
  suffix.to_s.empty? ? nil : suffix
end

#include?Boolean

Returns:

  • (Boolean)


122
# File 'lib/mailertogo/spf/term.rb', line 122

def include? = mechanism == "include"

#ip?Boolean

Returns:

  • (Boolean)


124
# File 'lib/mailertogo/spf/term.rb', line 124

def ip? = %w[ip4 ip6].include?(mechanism)

#kindObject

Which of the three a term is: a named mechanism, "modifier", "unknown".



140
141
142
143
144
145
# File 'lib/mailertogo/spf/term.rb', line 140

def kind
  return "modifier" if modifier
  return "unknown" if unknown?

  mechanism
end

#mechanismObject

"include", "ip4", "all"… nil for a modifier or for junk.



98
99
100
101
102
# File 'lib/mailertogo/spf/term.rb', line 98

def mechanism
  return "all" if @all_match

  @mechanism if MECHANISMS.include?(@mechanism)
end

#modifierObject

"redirect", "exp", or another modifier name; nil for a mechanism.



105
106
107
# File 'lib/mailertogo/spf/term.rb', line 105

def modifier
  @modifier_match && @modifier_match[1].downcase
end

#priced(lookups:, running_total:) ⇒ Object

A copy carrying the cost ChainAudit measured. self.class so a subclass that adds its own copy stays that subclass through pricing.



90
91
92
93
# File 'lib/mailertogo/spf/term.rb', line 90

def priced(lookups:, running_total:)
  self.class.new(raw: raw, position: position, unreachable: unreachable?,
                 lookups: lookups, running_total: running_total)
end

#qualifier_meaningObject

:pass | :fail | :softfail | :neutral — what a match under this qualifier means to a receiver (§4.6.2). On the terminal all this is the domain's whole policy for unauthorised mail (§5.1).



119
# File 'lib/mailertogo/spf/term.rb', line 119

def qualifier_meaning = QUALIFIERS[@qualifier || "+"]

#querying?Boolean

Does this term spend from the §4.6.4 budget at all?

Returns:

  • (Boolean)


127
# File 'lib/mailertogo/spf/term.rb', line 127

def querying? = QUERYING.include?(mechanism) || redirect?

#redirect?Boolean

Returns:

  • (Boolean)


123
# File 'lib/mailertogo/spf/term.rb', line 123

def redirect? = modifier == "redirect"

#targetObject

The value after the ":" or "=" — an include target, an IP range, an explanation name. nil for a bare a, mx or all.



111
112
113
114
# File 'lib/mailertogo/spf/term.rb', line 111

def target
  value = @modifier_match ? @modifier_match[2] : @bare.split(":", 2)[1]
  value.to_s.empty? ? nil : value
end

#to_sObject



155
# File 'lib/mailertogo/spf/term.rb', line 155

def to_s = raw

#unknown?Boolean

Not a mechanism SPF defines and not a modifier: junk. Per §4.6 a syntax error in a record permits a receiver to permerror the whole thing, so this is never harmless.

Returns:

  • (Boolean)


137
# File 'lib/mailertogo/spf/term.rb', line 137

def unknown? = mechanism.nil? && modifier.nil?

#unreachable?Boolean

Never evaluated by a receiver, so it authorises nothing and costs nothing. Set by whoever read the record in order (ChainAudit); a term on its own cannot know what precedes it.

Returns:

  • (Boolean)


132
# File 'lib/mailertogo/spf/term.rb', line 132

def unreachable? = @unreachable == true