Module: MailerToGo::SPF::Record
- Defined in:
- lib/mailertogo/spf/record.rb
Overview
Reading and normalising the raw text of an SPF record. Everything here is pure: no DNS, no state, no opinions about who you are.
Class Method Summary collapse
-
.include_target(record) ⇒ Object
The name an SPF record's
include:mechanism points at ("v=spf1 include:X ~all" → "X"), so a caller can pass the record it wants published and let us derive the mechanism from it. -
.ip_nets(terms) ⇒ Object
The ip4:/ip6: terms of a term list, as IPAddr networks.
-
.mechanism_of(term) ⇒ Object
The mechanism name of a term ("include:x" → "include", "ip4:1.2.3.4" → "ip4", "a/24" → "a"), or nil when the term is a modifier or junk.
-
.normalize_name(name) ⇒ Object
A hostname as SPF compares them: case-insensitive, root dot optional (RFC 7208 §4.3 — the domain-spec is a DNS name, and DNS names are compared case-insensitively).
-
.normalize_txt(txt) ⇒ Object
A TXT answer as a single string.
- .qualifier_of(term) ⇒ Object
-
.spf_record?(txt) ⇒ Boolean
Is this TXT string an SPF record? RFC 7208 §4.5: the version section is exactly "v=spf1", matched case-insensitively, followed by a space or the end of the record.
-
.strip_qualifier(term) ⇒ Object
RFC 7208 §4.6.2 — a mechanism may carry a leading qualifier (+ - ~ ?); "+" is the default when it is absent.
-
.terms(record) ⇒ Object
The terms of a record, without the leading "v=spf1".
Class Method Details
.include_target(record) ⇒ Object
The name an SPF record's include: mechanism points at
("v=spf1 include:X ~all" → "X"), so a caller can pass the record it wants
published and let us derive the mechanism from it. nil when the record
has no include.
41 42 43 44 |
# File 'lib/mailertogo/spf/record.rb', line 41 def include_target(record) term = record.to_s.split(/\s+/).find { |t| t.downcase.start_with?("include:") } term&.split(":", 2)&.last end |
.ip_nets(terms) ⇒ Object
The ip4:/ip6: terms of a term list, as IPAddr networks. Unparseable literals are dropped rather than raised on: a customer's malformed term is their record's problem, not a reason for us to blow up.
49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/mailertogo/spf/record.rb', line 49 def ip_nets(terms) terms.filter_map do |term| t = strip_qualifier(term) next unless t.downcase.start_with?("ip4:", "ip6:") begin IPAddr.new(t.split(":", 2).last) rescue StandardError nil end end end |
.mechanism_of(term) ⇒ Object
The mechanism name of a term ("include:x" → "include", "ip4:1.2.3.4" → "ip4", "a/24" → "a"), or nil when the term is a modifier or junk.
74 75 76 |
# File 'lib/mailertogo/spf/record.rb', line 74 def mechanism_of(term) strip_qualifier(term)[%r{\A([a-z0-9]+)(?::|/|\z)}i, 1]&.downcase end |
.normalize_name(name) ⇒ Object
A hostname as SPF compares them: case-insensitive, root dot optional (RFC 7208 §4.3 — the domain-spec is a DNS name, and DNS names are compared case-insensitively).
15 16 17 |
# File 'lib/mailertogo/spf/record.rb', line 15 def normalize_name(name) name.to_s.strip.downcase.chomp(".") end |
.normalize_txt(txt) ⇒ Object
A TXT answer as a single string.
A TXT record is a sequence of character-strings, each capped at 255
octets (RFC 1035 §3.3.14), so a long SPF record arrives as adjacent
quoted chunks — "v=spf1 include:_spf.mailer" "togo.net ~all". RFC 7208
§3.3 says to concatenate them with no separator. SPF terms never contain
a quote, so joining on quote boundaries is safe.
26 27 28 |
# File 'lib/mailertogo/spf/record.rb', line 26 def normalize_txt(txt) txt.to_s.strip.gsub(/"\s*"/, "").delete('"').strip end |
.qualifier_of(term) ⇒ Object
68 69 70 |
# File 'lib/mailertogo/spf/record.rb', line 68 def qualifier_of(term) term.to_s[/\A[+\-~?]/] end |
.spf_record?(txt) ⇒ Boolean
Is this TXT string an SPF record? RFC 7208 §4.5: the version section is exactly "v=spf1", matched case-insensitively, followed by a space or the end of the record. A TXT record starting "v=spf10" is not SPF.
33 34 35 |
# File 'lib/mailertogo/spf/record.rb', line 33 def spf_record?(txt) txt.to_s.match?(/\Av=spf1(\s|\z)/i) end |
.strip_qualifier(term) ⇒ Object
RFC 7208 §4.6.2 — a mechanism may carry a leading qualifier (+ - ~ ?); "+" is the default when it is absent.
64 65 66 |
# File 'lib/mailertogo/spf/record.rb', line 64 def strip_qualifier(term) term.to_s.sub(/\A[+\-~?]/, "") end |
.terms(record) ⇒ Object
The terms of a record, without the leading "v=spf1".
79 80 81 |
# File 'lib/mailertogo/spf/record.rb', line 79 def terms(record) record.to_s.split(/\s+/).drop(1) end |