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.authorize("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

Class Method Summary collapse

Class Attribute Details

.aliasesObject



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

Parameters:

  • value

    the value to set the attribute include to.



53
54
55
# File 'lib/mailertogo/spf.rb', line 53

def include=(value)
  @include = value
end

.loggerObject

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

.resolverObject

Resolving is I/O, so the default is built once and shared. Pass your own (any object responding to #call(name)) to change how DNS happens.



65
66
67
# File 'lib/mailertogo/spf.rb', line 65

def resolver
  @resolver ||= Resolver.new
end

Class Method Details

.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.

published: evaluates a supplied record as if it were published at , which is how you price a record that does not exist yet.

Raises:

  • (ArgumentError)


79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/mailertogo/spf.rb', line 79

def authorize(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

Yields:

  • (_self)

Yield Parameters:



69
70
71
72
# File 'lib/mailertogo/spf.rb', line 69

def configure
  yield self
  self
end

.hostname?(input) ⇒ Boolean

Returns:

  • (Boolean)


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

def hostname?(input) = Hostname.valid?(input)

.include_nameObject



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: 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