Class: MailerToGo::SPF::Term
- Inherits:
-
Object
- Object
- MailerToGo::SPF::Term
- 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 anall(which ends evaluation, and whose qualifier is the domain's policy) rather than filed as junk and the record read as having noallwhatsoever. /\Aall([^a-z0-9].*)?\z/i
Instance Attribute Summary collapse
-
#lookups ⇒ Object
readonly
Returns the value of attribute lookups.
-
#position ⇒ Object
readonly
Returns the value of attribute position.
-
#qualifier ⇒ Object
readonly
Returns the value of attribute qualifier.
-
#raw ⇒ Object
readonly
Returns the value of attribute raw.
-
#running_total ⇒ Object
readonly
Returns the value of attribute running_total.
Instance Method Summary collapse
- #all? ⇒ Boolean
-
#all_suffix ⇒ Object
The junk glued onto an
all(~all;google-site-verification=…), or nil. - #include? ⇒ Boolean
-
#initialize(raw:, position: 0, unreachable: false, lookups: nil, running_total: nil) ⇒ Term
constructor
raw — the term exactly as published.
- #ip? ⇒ Boolean
-
#kind ⇒ Object
Which of the three a term is: a named mechanism, "modifier", "unknown".
-
#mechanism ⇒ Object
"include", "ip4", "all"… nil for a modifier or for junk.
-
#modifier ⇒ Object
"redirect", "exp", or another modifier name; nil for a mechanism.
-
#priced(lookups:, running_total:) ⇒ Object
A copy carrying the cost ChainAudit measured.
-
#qualifier_meaning ⇒ Object
:pass | :fail | :softfail | :neutral — what a match under this qualifier means to a receiver (§4.6.2).
-
#querying? ⇒ Boolean
Does this term spend from the §4.6.4 budget at all?.
- #redirect? ⇒ Boolean
-
#target ⇒ Object
The value after the ":" or "=" — an include target, an IP range, an explanation name.
- #to_s ⇒ Object
-
#unknown? ⇒ Boolean
Not a mechanism SPF defines and not a modifier: junk.
-
#unreachable? ⇒ Boolean
Never evaluated by a receiver, so it authorises nothing and costs nothing.
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
#lookups ⇒ Object (readonly)
Returns the value of attribute lookups.
61 62 63 |
# File 'lib/mailertogo/spf/term.rb', line 61 def lookups @lookups end |
#position ⇒ Object (readonly)
Returns the value of attribute position.
61 62 63 |
# File 'lib/mailertogo/spf/term.rb', line 61 def position @position end |
#qualifier ⇒ Object (readonly)
Returns the value of attribute qualifier.
61 62 63 |
# File 'lib/mailertogo/spf/term.rb', line 61 def qualifier @qualifier end |
#raw ⇒ Object (readonly)
Returns the value of attribute raw.
61 62 63 |
# File 'lib/mailertogo/spf/term.rb', line 61 def raw @raw end |
#running_total ⇒ Object (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
121 |
# File 'lib/mailertogo/spf/term.rb', line 121 def all? = mechanism == "all" |
#all_suffix ⇒ Object
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
122 |
# File 'lib/mailertogo/spf/term.rb', line 122 def include? = mechanism == "include" |
#ip? ⇒ Boolean
124 |
# File 'lib/mailertogo/spf/term.rb', line 124 def ip? = %w[ip4 ip6].include?(mechanism) |
#kind ⇒ Object
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 |
#mechanism ⇒ Object
"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 |
#modifier ⇒ Object
"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_meaning ⇒ Object
: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?
127 |
# File 'lib/mailertogo/spf/term.rb', line 127 def = QUERYING.include?(mechanism) || redirect? |
#redirect? ⇒ Boolean
123 |
# File 'lib/mailertogo/spf/term.rb', line 123 def redirect? = modifier == "redirect" |
#target ⇒ Object
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_s ⇒ Object
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.
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.
132 |
# File 'lib/mailertogo/spf/term.rb', line 132 def unreachable? = @unreachable == true |