Class: MailerToGo::SPF::MergePlan
- Inherits:
-
Object
- Object
- MailerToGo::SPF::MergePlan
- Defined in:
- lib/mailertogo/spf/merge_plan.rb
Overview
What a domain owner should PUBLISH for SPF, given what is ALREADY at that name in DNS.
Why this exists: the standard setup instruction — "add a TXT record: v=spf1 include:_spf.mailertogo.net ~all" — is correct only for a domain with no SPF at all. A domain that already has SPF (Google Workspace, a registrar's default, Microsoft 365, another ESP) and follows that instruction literally ends up publishing a SECOND v=spf1 record beside the first. RFC 7208 §3.2 says a domain MUST NOT have more than one, and §4.5 makes the pair a permerror — which does not merely fail to authorize the new sender, it breaks SPF for every sender the domain had. The customer's mail gets worse, and they did exactly what they were told.
The only correct action for a domain that already has SPF is to MERGE the new mechanism into the existing record. So: before rendering the instruction, look at the name. If SPF is already there, hand them ONE merged record to REPLACE it with.
Three things this is careful about, because each is a way to make a customer's mail worse rather than better:
* Their `all` qualifier is THEIR policy (§5.1) — `-all` rejects
unauthorized mail, `~all` only marks it. Carry it across verbatim
rather than quietly relaxing (or tightening) what receivers do with
mail nobody asked us about.
* The new include goes LAST, immediately before the terminal `all`,
because a mechanism after `all` is never evaluated (§5.1/§6.1).
* Merging costs DNS lookups, and §4.6.4 caps an evaluation at 10. A
record already near the cap can be pushed over it by one more include,
and a record over the cap permerrors for everyone. Measure the merged
record and say so, rather than handing over a line that breaks on
arrival.
Reads DNS, changes nothing. When DNS does not answer we fall back to the standalone instruction rather than guessing — better to under-help than to tell someone to replace a record we could not read.
Class Method Summary collapse
-
.call(name, record:, sender:, resolver:, authorization: nil, logger: nil) ⇒ Object
What a term IS — is it the terminal
all(with whatever junk is glued onto it), is it a modifier, does it name the sender — is asked of Term rather than re-matched here.
Instance Method Summary collapse
-
#initialize(name, record:, sender:, resolver:, authorization: nil, logger: nil) ⇒ MergePlan
constructor
A new instance of MergePlan.
- #run ⇒ Object
Constructor Details
#initialize(name, record:, sender:, resolver:, authorization: nil, logger: nil) ⇒ MergePlan
Returns a new instance of MergePlan.
68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/mailertogo/spf/merge_plan.rb', line 68 def initialize(name, record:, sender:, resolver:, authorization: nil, logger: nil) @name = Record.normalize_name(name) @record = record.to_s @sender = sender @include_name = sender.include_name @lookup = resolver @authorization = @logger = logger @notes = [] @all_qualifier = nil end |
Class Method Details
.call(name, record:, sender:, resolver:, authorization: nil, logger: nil) ⇒ Object
What a term IS — is it the terminal all (with whatever junk is glued
onto it), is it a modifier, does it name the sender — is asked of Term
rather than re-matched here. There was a second, informal copy of that
knowledge in this file; two readings of the same term is exactly how a
record ending ~all;google-site-verification=… ends up merged one way
and described another.
name — the DNS name the record goes at.
record — the standalone record you would otherwise have told them
to publish, e.g. Sender#record. It is what a :publish
plan hands back; the merged line is built from sender.
sender — a Sender: the names that mean "me".
resolver — anything responding to #call(name); see Resolver.
authorization — an already-resolved Result for this same name, when the
caller has one. Saves walking the chain twice; nil means
we resolve it ourselves.
logger — optional, anything responding to #warn.
63 64 65 66 |
# File 'lib/mailertogo/spf/merge_plan.rb', line 63 def self.call(name, record:, sender:, resolver:, authorization: nil, logger: nil) new(name, record: record, sender: sender, resolver: resolver, authorization: , logger: logger).run end |
Instance Method Details
#run ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/mailertogo/spf/merge_plan.rb', line 80 def run txts = @lookup.call(@name) return plan(:publish, resolved: false) if txts.nil? # DNS did not answer records = Array(txts).map { |t| Record.normalize_txt(t) }.select { |t| Record.spf_record?(t) } return plan(:publish) if records.empty? # A single record that already reaches the sender — directly or through # an outer alias — is healthy. Never rewrite a working record. return plan(:satisfied, existing_records: records) if records.one? && merged = merge(records) # Nothing of ours to add (a single record that already names the sender # but has an unrelated defect of its own — that is a different warning, # not a merge instruction). return plan(:satisfied, existing_records: records) if records.one? && same_terms?(merged, records.first) budget = measure(merged) plan(records.size > 1 ? :deduplicate : :merge, existing_records: records, merged_record: merged, all_qualifier: @all_qualifier, lookups: budget[:lookups], over_lookup_limit: budget[:over_limit]) end |