spf 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +6 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +63 -0
- data/README.rdoc +13 -0
- data/Rakefile +56 -0
- data/lib/spf/error.rb +50 -0
- data/lib/spf/eval.rb +285 -0
- data/lib/spf/macro_string.rb +73 -0
- data/lib/spf/model.rb +985 -0
- data/lib/spf/request.rb +140 -0
- data/lib/spf/result.rb +218 -0
- data/lib/spf/util.rb +110 -0
- data/lib/spf/version.rb +5 -0
- data/lib/spf.rb +48 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/spf_spec.rb +7 -0
- data/spf.gemspec +66 -0
- metadata +134 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "rspec", "~> 2.8.0"
|
10
|
+
gem "rdoc", "~> 3.12"
|
11
|
+
gem "bundler", "~> 1.0"
|
12
|
+
gem "jeweler", "~> 1.8.7"
|
13
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.3.5)
|
5
|
+
builder (3.2.2)
|
6
|
+
diff-lcs (1.1.3)
|
7
|
+
faraday (0.8.8)
|
8
|
+
multipart-post (~> 1.2.0)
|
9
|
+
git (1.2.6)
|
10
|
+
github_api (0.10.1)
|
11
|
+
addressable
|
12
|
+
faraday (~> 0.8.1)
|
13
|
+
hashie (>= 1.2)
|
14
|
+
multi_json (~> 1.4)
|
15
|
+
nokogiri (~> 1.5.2)
|
16
|
+
oauth2
|
17
|
+
hashie (2.0.5)
|
18
|
+
highline (1.6.19)
|
19
|
+
httpauth (0.2.0)
|
20
|
+
jeweler (1.8.8)
|
21
|
+
builder
|
22
|
+
bundler (~> 1.0)
|
23
|
+
git (>= 1.2.5)
|
24
|
+
github_api (= 0.10.1)
|
25
|
+
highline (>= 1.6.15)
|
26
|
+
nokogiri (= 1.5.10)
|
27
|
+
rake
|
28
|
+
rdoc
|
29
|
+
json (1.8.0)
|
30
|
+
jwt (0.1.8)
|
31
|
+
multi_json (>= 1.5)
|
32
|
+
multi_json (1.8.2)
|
33
|
+
multi_xml (0.5.5)
|
34
|
+
multipart-post (1.2.0)
|
35
|
+
nokogiri (1.5.10)
|
36
|
+
oauth2 (0.9.2)
|
37
|
+
faraday (~> 0.8)
|
38
|
+
httpauth (~> 0.2)
|
39
|
+
jwt (~> 0.1.4)
|
40
|
+
multi_json (~> 1.0)
|
41
|
+
multi_xml (~> 0.5)
|
42
|
+
rack (~> 1.2)
|
43
|
+
rack (1.5.2)
|
44
|
+
rake (10.1.0)
|
45
|
+
rdoc (3.12.2)
|
46
|
+
json (~> 1.4)
|
47
|
+
rspec (2.8.0)
|
48
|
+
rspec-core (~> 2.8.0)
|
49
|
+
rspec-expectations (~> 2.8.0)
|
50
|
+
rspec-mocks (~> 2.8.0)
|
51
|
+
rspec-core (2.8.0)
|
52
|
+
rspec-expectations (2.8.0)
|
53
|
+
diff-lcs (~> 1.1.2)
|
54
|
+
rspec-mocks (2.8.0)
|
55
|
+
|
56
|
+
PLATFORMS
|
57
|
+
ruby
|
58
|
+
|
59
|
+
DEPENDENCIES
|
60
|
+
bundler (~> 1.0)
|
61
|
+
jeweler (~> 1.8.7)
|
62
|
+
rdoc (~> 3.12)
|
63
|
+
rspec (~> 2.8.0)
|
data/README.rdoc
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
= SPF
|
2
|
+
|
3
|
+
The +spf+ Ruby gem, also known as +spf-ruby+, is an implementation of the Sender Policy Framework (SPF) e-mail sender authentication system.
|
4
|
+
|
5
|
+
See <http://www.openspf.org> for more information about SPF.
|
6
|
+
|
7
|
+
== Contributing
|
8
|
+
|
9
|
+
Agari currently does not accept contributions of code to spf-ruby.
|
10
|
+
|
11
|
+
== Copyright
|
12
|
+
|
13
|
+
(C) 2013 Agari Data, Inc. All rights reserved. Distribution prohibited.
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
require './lib/spf/version.rb'
|
16
|
+
Jeweler::Tasks.new do |gem|
|
17
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
18
|
+
gem.name = 'spf'
|
19
|
+
gem.version = SPF::VERSION
|
20
|
+
gem.homepage = 'https://github.com/agaridata/spf-ruby'
|
21
|
+
gem.authors = ['Andrew Flury', 'Julian Mehnle']
|
22
|
+
gem.email = ['code@agari.com', 'aflury@agari.com', 'jmehnle@agari.com']
|
23
|
+
gem.license = 'none (all rights reserved)'
|
24
|
+
gem.summary = 'Implementation of the Sender Policy Framework'
|
25
|
+
gem.description = <<-DESCRIPTION
|
26
|
+
An object-oriented Ruby implementation of the Sender Policy Framework (SPF)
|
27
|
+
e-mail sender authentication system, fully compliant with RFC 4408.
|
28
|
+
DESCRIPTION
|
29
|
+
# dependencies defined in Gemfile
|
30
|
+
end
|
31
|
+
Jeweler::RubygemsDotOrgTasks.new
|
32
|
+
|
33
|
+
require 'rspec/core'
|
34
|
+
require 'rspec/core/rake_task'
|
35
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
36
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
37
|
+
end
|
38
|
+
|
39
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
40
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
41
|
+
spec.rcov = true
|
42
|
+
end
|
43
|
+
|
44
|
+
task :default => :spec
|
45
|
+
|
46
|
+
require 'rdoc/task'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "spf-ruby #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
55
|
+
|
56
|
+
# vim:sw=2 sts=2
|
data/lib/spf/error.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module SPF
|
2
|
+
|
3
|
+
# Generic Exceptions
|
4
|
+
##############################################################################
|
5
|
+
|
6
|
+
class Error < StandardError; end
|
7
|
+
class OptionRequiredError < Error; end # Missing required method option
|
8
|
+
# XXX Replace with ArgumentError?
|
9
|
+
class InvalidOptionValueError < Error; end # Invalid value for method option
|
10
|
+
# XXX Replace with ArgumentError!
|
11
|
+
|
12
|
+
# Miscellaneous Errors
|
13
|
+
##############################################################################
|
14
|
+
|
15
|
+
class DNSError < Error; end # DNS error
|
16
|
+
class DNSTimeoutError < DNSError; end # DNS timeout
|
17
|
+
class RecordSelectionError < Error; end # Record selection error
|
18
|
+
class NoAcceptableRecordError < RecordSelectionError; end # No acceptable record found
|
19
|
+
class RedundantAcceptableRecordsError < RecordSelectionError; end # Redundant acceptable records found
|
20
|
+
class NoUnparsedTextError < Error; end # No unparsed text available
|
21
|
+
class UnexpectedTermObjectError < Error; end # Unexpected term object encountered
|
22
|
+
class ProcessingLimitExceededError < Error; end # Processing limit exceeded
|
23
|
+
class MacroExpansionCtxRequiredError < OptionRequiredError; end # Missing required context for macro expansion
|
24
|
+
|
25
|
+
# Parser Errors
|
26
|
+
##############################################################################
|
27
|
+
|
28
|
+
class NothingToParseError < Error; end # Nothing to parse
|
29
|
+
class SyntaxError < Error; end # Generic syntax error
|
30
|
+
class InvalidRecordVersionError < SyntaxError; end # Invalid record version
|
31
|
+
class InvalidScopeError < SyntaxError; end # Invalid scope
|
32
|
+
class JunkInRecordError < SyntaxError; end # Junk encountered in record
|
33
|
+
class InvalidModError < SyntaxError; end # Invalid modifier
|
34
|
+
class InvalidTermError < SyntaxError; end # Invalid term
|
35
|
+
class JunkInTermError < SyntaxError; end # Junk encountered in term
|
36
|
+
class DuplicateGlobalMod < InvalidModError; end # Duplicate global modifier
|
37
|
+
class InvalidMechError < InvalidTermError; end # Invalid mechanism
|
38
|
+
class InvalidMechQualifierError < InvalidMechError; end # Invalid mechanism qualifier
|
39
|
+
class TermDomainSpecExpectedError < SyntaxError; end # Missing required <domain-spec> in term
|
40
|
+
class TermIPv4AddressExpectedError < SyntaxError; end # Missing required <ip4-network> in term
|
41
|
+
class TermIPv4PrefixLengthExpected < SyntaxError; end # Missing required <ip4-cidr-length> in term
|
42
|
+
class TermIPv6AddressExpected < SyntaxError; end # Missing required <ip6-network> in term
|
43
|
+
class TermIPv6PrefixLengthExpected < SyntaxError; end # Missing required <ip6-cidr-length> in term
|
44
|
+
class InvalidMacroStringError < SyntaxError; end # Invalid macro string
|
45
|
+
class InvalidMacroError < InvalidMacroStringError
|
46
|
+
end # Invalid macro
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
# vim:sw=2 sts=2
|
data/lib/spf/eval.rb
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'ip'
|
2
|
+
require 'resolv'
|
3
|
+
|
4
|
+
require 'spf/model'
|
5
|
+
require 'spf/result'
|
6
|
+
|
7
|
+
class Resolv::DNS::Resource::IN::SPF < Resolv::DNS::Resource::IN::TXT
|
8
|
+
# resolv.rb doesn't define an SPF resource type.
|
9
|
+
TypeValue = 99
|
10
|
+
end
|
11
|
+
|
12
|
+
class SPF::Server
|
13
|
+
|
14
|
+
attr_accessor \
|
15
|
+
:default_authority_explanation,
|
16
|
+
:hostname,
|
17
|
+
:dns_resolver,
|
18
|
+
:query_rr_types,
|
19
|
+
:max_dns_interactive_terms,
|
20
|
+
:max_name_lookups_per_term,
|
21
|
+
:max_name_lookups_per_mx_mech,
|
22
|
+
:max_name_lookups_per_ptr_mech,
|
23
|
+
:max_void_dns_lookups
|
24
|
+
|
25
|
+
RECORD_CLASSES_BY_VERSION = {
|
26
|
+
1 => SPF::Record::V1,
|
27
|
+
2 => SPF::Record::V2
|
28
|
+
}
|
29
|
+
|
30
|
+
RESULT_BASE_CLASS = SPF::Result
|
31
|
+
|
32
|
+
QUERY_RR_TYPE_ALL = 0
|
33
|
+
QUERY_RR_TYPE_TXT = 1
|
34
|
+
QUERY_RR_TYPE_SPF = 2
|
35
|
+
|
36
|
+
DEFAULT_DEFAULT_AUTHORITY_EXPLANATION =
|
37
|
+
'Please see http://www.openspf.org/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}'
|
38
|
+
|
39
|
+
DEFAULT_MAX_DNS_INTERACTIVE_TERMS = 10 # RFC 4408, 10.1/6
|
40
|
+
DEFAULT_MAX_NAME_LOOKUPS_PER_TERM = 10 # RFC 4408, 10.1/7
|
41
|
+
DEFAULT_QUERY_RR_TYPES = QUERY_RR_TYPE_TXT
|
42
|
+
DEFAULT_MAX_NAME_LOOKUPS_PER_MX_MECH = DEFAULT_MAX_NAME_LOOKUPS_PER_TERM
|
43
|
+
DEFAULT_MAX_NAME_LOOKUPS_PER_PTR_MECH = DEFAULT_MAX_NAME_LOOKUPS_PER_TERM
|
44
|
+
DEFAULT_MAX_VOID_DNS_LOOKUPS = 2
|
45
|
+
|
46
|
+
def initialize(options = {})
|
47
|
+
@default_authority_explanation = options[:default_authority_explanation] ||
|
48
|
+
DEFAULT_DEFAULT_AUTHORITY_EXPLANATION
|
49
|
+
unless @default_authority_explanation.is_a?(SPF::MacroString)
|
50
|
+
@default_authority_explanation = SPF::MacroString.new({
|
51
|
+
:text => @default_authority_explanation,
|
52
|
+
:server => self,
|
53
|
+
:is_explanation => true
|
54
|
+
})
|
55
|
+
end
|
56
|
+
@hostname = options[:hostname] || SPF::Util.hostname
|
57
|
+
@dns_resolver = options[:dns_resolver] || Resolv::DNS.new
|
58
|
+
@query_rr_types = options[:query_rr_types] ||
|
59
|
+
DEFAULT_QUERY_RR_TYPES
|
60
|
+
@max_dns_interactive_terms = options[:max_dns_interactive_terms] ||
|
61
|
+
DEFAULT_MAX_DNS_INTERACTIVE_TERMS
|
62
|
+
@max_name_lookups_per_term = options[:max_name_lookups_per_term] ||
|
63
|
+
DEFAULT_MAX_NAME_LOOKUPS_PER_TERM
|
64
|
+
@max_name_lookups_per_mx_mech = options[:max_name_lookups_per_mx_mech] ||
|
65
|
+
DEFAULT_MAX_NAME_LOOKUPS_PER_MX_MECH
|
66
|
+
@max_name_lookups_per_ptr_mech = options[:max_name_lookups_per_ptr_mech] ||
|
67
|
+
DEFAULT_MAX_NAME_LOOKUPS_PER_PTR_MECH
|
68
|
+
@max_void_dns_lookups = options[:max_void_dns_lookups] ||
|
69
|
+
DEFAULT_MAX_VOID_DNS_LOOKUPS
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
def result_class(name = nil)
|
74
|
+
if name
|
75
|
+
return RESULT_BASE_CLASS::RESULT_CLASSES[name]
|
76
|
+
else
|
77
|
+
return RESULT_BASE_CLASS
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def throw_result(name, request, text)
|
82
|
+
raise self.result_class(name).new(self, request, text)
|
83
|
+
end
|
84
|
+
|
85
|
+
def process(request)
|
86
|
+
request.state(:authority_explanation, nil)
|
87
|
+
request.state(:dns_interactive_term_count, 0)
|
88
|
+
request.state(:void_dns_lookups_count, 0)
|
89
|
+
|
90
|
+
result = nil
|
91
|
+
|
92
|
+
begin
|
93
|
+
record = self.select_record(request)
|
94
|
+
request.record = record
|
95
|
+
record.eval(self, request)
|
96
|
+
rescue SPF::Result => r
|
97
|
+
result = r
|
98
|
+
rescue SPF::DNSError => e
|
99
|
+
result = self.result_class(:temperror).new(self, request, e.message)
|
100
|
+
rescue SPF::NoAcceptableRecordError => e
|
101
|
+
result = self.result_class(:none ).new(self, request, e.message)
|
102
|
+
rescue SPF::RedundantAcceptableRecordsError, SPF::SyntaxError, SPF::ProcessingLimitExceededError => e
|
103
|
+
result = self.result_class(:permerror).new([self, request, e.message])
|
104
|
+
end
|
105
|
+
# Propagate other, unknown errors.
|
106
|
+
# This should not happen, but if it does, it helps exposing the bug!
|
107
|
+
|
108
|
+
return result
|
109
|
+
end
|
110
|
+
|
111
|
+
def resource_typeclass_for_rr_type(rr_type)
|
112
|
+
return case rr_type
|
113
|
+
when 'TXT' then Resolv::DNS::Resource::IN::TXT
|
114
|
+
when 'SPF' then Resolv::DNS::Resource::IN::SPF
|
115
|
+
when 'ANY' then Resolv::DNS::Resource::IN::ANY
|
116
|
+
when 'A' then Resolv::DNS::Resource::IN::A
|
117
|
+
when 'AAAA' then Resolv::DNS::Resource::IN::AAAA
|
118
|
+
when 'PTR' then Resolv::DNS::Resource::IN::PTR
|
119
|
+
else
|
120
|
+
raise ArgumentError, "Uknown RR type: #{rr_type}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def dns_lookup(domain, rr_type)
|
125
|
+
if domain.is_a?(SPF::MacroString)
|
126
|
+
domain = domain.expand
|
127
|
+
# Truncate overlong labels at 63 bytes (RFC 4408, 8.1/27)
|
128
|
+
domain.gsub!(/([^.]{63})[^.]+/, "#{$1}")
|
129
|
+
# Drop labels from the head of domain if longer than 253 bytes (RFC 4408, 8.1/25):
|
130
|
+
domain.sub!(/^[^.]+\.(.*)$/, "#{$1}") while domain.length > 253
|
131
|
+
end
|
132
|
+
|
133
|
+
rr_type = self.resource_typeclass_for_rr_type(rr_type)
|
134
|
+
|
135
|
+
domain.sub(/^(.*?)\.?$/, $1 ? "#{$1}".downcase : '')
|
136
|
+
|
137
|
+
packet = @dns_resolver.getresources(domain, rr_type)
|
138
|
+
|
139
|
+
# Raise DNS exception unless an answer packet with RCODE 0 or 3 (NXDOMAIN)
|
140
|
+
# was received (thereby treating NXDOMAIN as an acceptable but empty answer packet):
|
141
|
+
#if @dns_resolver.errorstring =~ /^(timeout|query timed out)$/
|
142
|
+
# raise SPF::DNSTimeoutError.new(
|
143
|
+
# "Time-out on DNS '#{rr_type}' lookup of '#{domain}'")
|
144
|
+
#end
|
145
|
+
|
146
|
+
unless packet
|
147
|
+
raise SPF::DNSError.new(
|
148
|
+
"Unknown error on DNS '#{rr_type}' lookup of '#{domain}'")
|
149
|
+
end
|
150
|
+
|
151
|
+
#unless packet.header.rcode =~ /^(NOERROR|NXDOMAIN)$/
|
152
|
+
# raise SPF::DNSError.new(
|
153
|
+
# "'#{packet.header.rcode}' error on DNS '#{rr_type}' lookup of '#{domain}'")
|
154
|
+
#end
|
155
|
+
return packet
|
156
|
+
end
|
157
|
+
|
158
|
+
def select_record(request)
|
159
|
+
domain = request.authority_domain
|
160
|
+
versions = request.versions
|
161
|
+
scope = request.scope
|
162
|
+
|
163
|
+
# Employ identical behavior for 'v=spf1' and 'spf2.0' records, both of
|
164
|
+
# which support SPF (code 99) and TXT type records (this may be different
|
165
|
+
# in future revisions of SPF):
|
166
|
+
# Query for SPF type records first, then fall back to TXT type records.
|
167
|
+
|
168
|
+
records = []
|
169
|
+
query_count = 0
|
170
|
+
dns_errors = []
|
171
|
+
|
172
|
+
# Query for SPF-type RRs first:
|
173
|
+
if (@query_rr_types == QUERY_RR_TYPE_ALL or
|
174
|
+
@query_rr_types & QUERY_RR_TYPE_SPF)
|
175
|
+
begin
|
176
|
+
query_count += 1
|
177
|
+
packet = self.dns_lookup(domain, 'SPF')
|
178
|
+
records << self.get_acceptable_records_from_packet(
|
179
|
+
packet, 'SPF', versions, scope, domain)
|
180
|
+
rescue SPF::DNSError => e
|
181
|
+
dns_errors << e
|
182
|
+
#rescue SPF::DNSTimeout => e
|
183
|
+
# # FIXME: Ignore DNS timeouts on SPF type lookups?
|
184
|
+
# # Apparently some brain-dead DNS servers time out on SPF-type queries.
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
if (not records.any? and
|
189
|
+
@query_rr_types == QUERY_RR_TYPES_ALL or
|
190
|
+
@query_rr_types & QUERY_RR_TYPE_TXT)
|
191
|
+
# NOTE:
|
192
|
+
# This deliberately violates RFC 4406 (Sender ID), 4.4/3 (4.4.1):
|
193
|
+
# TXT-type RRs are still tried if there _are_ SPF-type RRs but all
|
194
|
+
# of them are inapplicable (e.g. "Hi!", or even "spf2/pra" for an
|
195
|
+
# 'mfrom' scope request). This conforms to the spirit of the more
|
196
|
+
# sensible algorithm in RFC 4408 (SPF), 4.5.
|
197
|
+
# Implication: Sender ID processing may make use of existing TXT-
|
198
|
+
# type records where a result of "None" would normally be returned
|
199
|
+
# under a strict interpretation of RFC 4406.
|
200
|
+
|
201
|
+
begin
|
202
|
+
query_count += 1
|
203
|
+
packet = self.dns_lookup(domain, 'TXT')
|
204
|
+
records << self.get_acceptable_records_from_packet(
|
205
|
+
packet, 'TXT', versions, scope, domain)
|
206
|
+
rescue SPF::DNSError => e
|
207
|
+
dns_errors << e
|
208
|
+
end
|
209
|
+
|
210
|
+
# Unless at least one query succeeded, re-raise the first DNS error that occured.
|
211
|
+
raise dns_errors[0] unless dns_errors.length < query_count
|
212
|
+
|
213
|
+
records.flatten!
|
214
|
+
|
215
|
+
if records.empty?
|
216
|
+
# RFC 4408, 4.5/7
|
217
|
+
raise SPF::NoAcceptableRecordError('No applicable sender policy available')
|
218
|
+
end
|
219
|
+
|
220
|
+
# Discard all records but the highest acceptable version:
|
221
|
+
preferred_record_class = records[0].class
|
222
|
+
|
223
|
+
records = records.select { |record| record.is_a?(preferred_record_class) }
|
224
|
+
|
225
|
+
if records.length != 1
|
226
|
+
# RFC 4408, 4.5/6
|
227
|
+
raise SPF::RedundantAcceptableRecordsError.new(
|
228
|
+
"Redundant applicable '#{preferred_record_class.version_tag}' sender policies found"
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
return records[0]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def get_acceptable_records_from_packet(packet, rr_type, versions, scope, domain)
|
237
|
+
|
238
|
+
# Try higher record versions first.
|
239
|
+
# (This may be too simplistic for future revisions of SPF.)
|
240
|
+
versions = versions.sort { |x, y| y <=> x }
|
241
|
+
|
242
|
+
rr_type = resource_typeclass_for_rr_type(rr_type)
|
243
|
+
records = []
|
244
|
+
packet.each do |rr|
|
245
|
+
next unless rr.is_a?(rr_type)
|
246
|
+
text = rr.strings.join('')
|
247
|
+
record = false
|
248
|
+
versions.each do |version|
|
249
|
+
klass = RECORD_CLASSES_BY_VERSION[version]
|
250
|
+
begin
|
251
|
+
record = klass.new_from_string(text)
|
252
|
+
rescue SPF::InvalidRecordVersionError
|
253
|
+
# Ignore non-SPF and unknown-version records.
|
254
|
+
# Propagate other errors (including syntax errors), though.
|
255
|
+
end
|
256
|
+
end
|
257
|
+
if record
|
258
|
+
if record.scopes.select{|x| scope == x}.any?
|
259
|
+
# Record covers requested scope.
|
260
|
+
records << record
|
261
|
+
end
|
262
|
+
break
|
263
|
+
end
|
264
|
+
end
|
265
|
+
return records
|
266
|
+
end
|
267
|
+
|
268
|
+
def count_dns_interactive_term(request)
|
269
|
+
dns_interactive_terms_count = request.root_request.state(:dns_interactive_terms_count, 1)
|
270
|
+
if (@max_dns_interactive_terms and
|
271
|
+
dns_interactive_terms_count > @max_dns_interactive_terms)
|
272
|
+
raise SPF::ProcessingLimitExceededError.new(
|
273
|
+
"Maximum DNS-interactive terms limit (#{@max_dns_interactive_terms}) exceeded")
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def count_void_dns_lookup(request)
|
278
|
+
void_dns_lookups_count = request.root_request.state(:void_dns_lookups_count, 1)
|
279
|
+
if (@max_void_dns_lookups and
|
280
|
+
void_dns_lookups_count > @max_void_dns_lookups)
|
281
|
+
raise SPF::ProcessingLimitExceeded.new(
|
282
|
+
"Maximum void DNS look-ups limit (#{@max_void_dns_lookups}) exceeded")
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spf/util'
|
2
|
+
|
3
|
+
module SPF
|
4
|
+
class MacroString
|
5
|
+
|
6
|
+
def self.default_split_delimiters
|
7
|
+
'.'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.default_join_delimiter
|
11
|
+
'.'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.uri_unreserved_chars
|
15
|
+
'A-Za-z0-9\-._~'
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
super()
|
20
|
+
@text = options[:text] \
|
21
|
+
or raise ArgumentError, "Missing required 'text' option"
|
22
|
+
@server = options[:server]
|
23
|
+
@request = options[:request]
|
24
|
+
@expanded = nil
|
25
|
+
self.expand
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :text, :server, :request
|
29
|
+
|
30
|
+
def context(server, request)
|
31
|
+
valid_context(true, server, request)
|
32
|
+
@server = server
|
33
|
+
@request = request
|
34
|
+
@expanded = nil
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
def expand(context = nil)
|
39
|
+
return @expanded if @expanded
|
40
|
+
|
41
|
+
return nil unless @text
|
42
|
+
return (@expanded = @text) unless @text =~ /%/
|
43
|
+
# Short-circuit expansion if text has no '%' characters.
|
44
|
+
|
45
|
+
expanded = ''
|
46
|
+
# TODO
|
47
|
+
return (@expanded = @text)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
if valid_context(false)
|
52
|
+
return expand
|
53
|
+
else
|
54
|
+
return @text
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_context(required, server = self.server, request = self.request)
|
59
|
+
if not SPF::Server === server
|
60
|
+
raise MacroExpansionCtxRequired, 'SPF server object required' if required
|
61
|
+
return false
|
62
|
+
end
|
63
|
+
if not SPF::Request === request
|
64
|
+
raise MacroExpansionCtxRequired, 'SPF request object required' if required
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
return true
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# vim:sw=2 sts=2
|