Module: MailerToGo::SPF
- Defined in:
- lib/mailertogo/spf.rb,
lib/mailertogo/spf/plan.rb,
lib/mailertogo/spf/term.rb,
lib/mailertogo/spf/record.rb,
lib/mailertogo/spf/result.rb,
lib/mailertogo/spf/sender.rb,
lib/mailertogo/spf/version.rb,
lib/mailertogo/spf/hostname.rb,
lib/mailertogo/spf/resolver.rb,
lib/mailertogo/spf/merge_plan.rb,
lib/mailertogo/spf/chain_audit.rb,
lib/mailertogo/spf/authorization.rb
Overview
An SPF engine that reads a record the way a receiving MTA does: it resolves the include chain, stops where the receiver stops, and counts DNS lookups against the RFC 7208 §4.6.4 cap.
Three questions, three entry points:
MailerToGo::SPF.authorize("example.com")
→ does this domain's published SPF authorize me?
MailerToGo::SPF.merge_plan("example.com")
→ what should I tell them to publish, given what is already there?
MailerToGo::SPF.chain_audit("example.com")
→ what does this record cost a receiver, term by term?
Both take include: (the mechanism you want authorized) and aliases:
(other names that mean the same sender). Both default to MailerToGo's own
names, so the zero-argument form is the useful one for MailerToGo customers
and one keyword makes it work for anybody else:
MailerToGo::SPF.("example.com", include: "spf.example.net")
Defined Under Namespace
Modules: Hostname, Record Classes: Authorization, CachingResolver, ChainAudit, MergePlan, Plan, Resolver, Result, Sender, Term
Constant Summary collapse
- DEFAULT_INCLUDE =
MailerToGo publishes an outer alias and a leaf. Customers are told to include the leaf, but either one authorizes us, so both count as "me".
"_spf.mailertogo.net"- DEFAULT_ALIASES =
["mailertogo.net"].freeze
- VERSION =
"0.2.1"
Class Attribute Summary collapse
- .aliases ⇒ Object
-
.include ⇒ Object
writeonly
Sets the attribute include.
-
.logger ⇒ Object
Process-wide defaults.
-
.resolver ⇒ Object
Resolving is I/O, so the default is built once and shared.
Class Method Summary collapse
-
.authorize(hostname, include: nil, aliases: nil, resolver: nil, sender: nil, published: nil, logger: nil) ⇒ Object
Does
's published SPF authorize the sender? Returns a Result (see result.rb) — never raises for a DNS failure. -
.chain_audit(hostname, record: nil, resolver: nil, term_class: nil) ⇒ Object
What does this record cost a receiver that evaluates all of it? Returns a ChainAudit (see chain_audit.rb).
- .configure {|_self| ... } ⇒ Object
- .hostname?(input) ⇒ Boolean
- .include_name ⇒ Object
-
.merge_plan(name, record: nil, include: nil, aliases: nil, resolver: nil, sender: nil, authorization: nil, logger: nil) ⇒ Object
What should this domain publish, given what is already at that name? Returns a Plan (see plan.rb).
-
.normalize_hostname(input) ⇒ Object
Is this untrusted input something we should resolve at all? Returns the normalized hostname, or nil.
-
.sender(include: nil, aliases: nil) ⇒ Object
The names that mean "me".
Class Attribute Details
.aliases ⇒ Object
59 60 61 |
# File 'lib/mailertogo/spf.rb', line 59 def aliases defined?(@aliases) && @aliases ? @aliases : DEFAULT_ALIASES end |
.include=(value) ⇒ Object (writeonly)
Sets the attribute include
53 54 55 |
# File 'lib/mailertogo/spf.rb', line 53 def include=(value) @include = value end |
.logger ⇒ Object
Process-wide defaults. Everything here can also be passed per call.
MailerToGo::SPF.configure do |c|
c.include = "spf.example.net"
c.aliases = ["example.net"]
c.resolver = MailerToGo::SPF::CachingResolver.new(MailerToGo::SPF::Resolver.new)
c.logger = Logger.new($stdout)
end
52 53 54 |
# File 'lib/mailertogo/spf.rb', line 52 def logger @logger end |
Class Method Details
.authorize(hostname, include: nil, aliases: nil, resolver: nil, sender: nil, published: nil, logger: nil) ⇒ Object
Does
published: evaluates a supplied record as if it were published at
79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/mailertogo/spf.rb', line 79 def (hostname, include: nil, aliases: nil, resolver: nil, sender: nil, published: nil, logger: nil) sender ||= sender_for(include || include_name, aliases) raise ArgumentError, "no include name given or configured" if sender.nil? Authorization.call( hostname, sender: sender, resolver: resolver || self.resolver, published: published, logger: logger || self.logger ) end |
.chain_audit(hostname, record: nil, resolver: nil, term_class: nil) ⇒ Object
What does this record cost a receiver that evaluates all of it? Returns a ChainAudit (see chain_audit.rb).
This is a different number from Result#lookups and is meant to be: the
authorization walk stops where the receiver stops (§4.6.2), while this
prices the whole tree against the §4.6.4 budget — the number other SPF
checkers report. Notably sender-agnostic; there is no include: here,
because the record's cost has nothing to do with who is asking.
record: price this record instead of resolving one at hostname, which
is how you price a record that is not published yet.
126 127 128 129 |
# File 'lib/mailertogo/spf.rb', line 126 def chain_audit(hostname, record: nil, resolver: nil, term_class: nil) ChainAudit.call(hostname: hostname, record: record, resolver: resolver || self.resolver, term_class: term_class) end |
.configure {|_self| ... } ⇒ Object
69 70 71 72 |
# File 'lib/mailertogo/spf.rb', line 69 def configure yield self self end |
.hostname?(input) ⇒ Boolean
137 |
# File 'lib/mailertogo/spf.rb', line 137 def hostname?(input) = Hostname.valid?(input) |
.include_name ⇒ Object
55 56 57 |
# File 'lib/mailertogo/spf.rb', line 55 def include_name defined?(@include) && @include ? @include : DEFAULT_INCLUDE end |
.merge_plan(name, record: nil, include: nil, aliases: nil, resolver: nil, sender: nil, authorization: nil, logger: nil) ⇒ Object
What should this domain publish, given what is already at that name? Returns a Plan (see plan.rb).
record: the standalone record you would otherwise hand them; defaults to
the sender's own v=spf1 include:… ~all.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/mailertogo/spf.rb', line 98 def merge_plan(name, record: nil, include: nil, aliases: nil, resolver: nil, sender: nil, authorization: nil, logger: nil) # A record with no include mechanism gives us no identity to merge in — # hand back a null plan rather than inventing one. sender ||= sender_for(include || (record.nil? ? include_name : Record.include_target(record)), aliases) return Plan.none(name: name, record: record) if sender.nil? MergePlan.call( name, record: record || sender.record, sender: sender, resolver: resolver || self.resolver, authorization: , logger: logger || self.logger ) end |
.normalize_hostname(input) ⇒ Object
Is this untrusted input something we should resolve at all? Returns the normalized hostname, or nil. See hostname.rb — it is a gate, kept separate from the entry points above on purpose, because "that is not a hostname" is a fact about the INPUT and must not be dressed up as a fact about somebody's DNS.
136 |
# File 'lib/mailertogo/spf.rb', line 136 def normalize_hostname(input) = Hostname.parse(input) |
.sender(include: nil, aliases: nil) ⇒ Object
The names that mean "me". Public because a caller that asks both questions about the same sender should build it once.
141 142 143 |
# File 'lib/mailertogo/spf.rb', line 141 def sender(include: nil, aliases: nil) sender_for(include || include_name, aliases) end |