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/lib/spf/request.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'ip'
|
2
|
+
|
3
|
+
require 'spf/error'
|
4
|
+
|
5
|
+
class SPF::Request
|
6
|
+
|
7
|
+
attr_reader :scope, :identity, :domain, :localpart, :ip_address, :ip_address_v6, :helo_identity, :versions
|
8
|
+
attr_accessor :record, :opt, :root_request, :super_request
|
9
|
+
|
10
|
+
VERSIONS_FOR_SCOPE = {
|
11
|
+
:helo => [1 ],
|
12
|
+
:mfrom => [1, 2],
|
13
|
+
:pra => [ 2]
|
14
|
+
}
|
15
|
+
|
16
|
+
SCOPES_BY_VERSION = {
|
17
|
+
1 => [:helo, :mfrom ],
|
18
|
+
2 => [ :mfrom, :pra]
|
19
|
+
}
|
20
|
+
|
21
|
+
DEFAULT_LOCALPART = 'postmaster'
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@opt = options
|
25
|
+
@state = {}
|
26
|
+
@versions = options[:versions]
|
27
|
+
@scope = options[:scope] || :mfrom
|
28
|
+
@scope = @scope.to_sym if @scope.is_a?(String)
|
29
|
+
@_authority_domain = options[:authority_domain]
|
30
|
+
@identity = options[:identity]
|
31
|
+
@ip_address = options[:ip_address]
|
32
|
+
@helo_identity = options[:helo_identity]
|
33
|
+
@root_request = self
|
34
|
+
@super_request = self
|
35
|
+
@record = nil
|
36
|
+
|
37
|
+
# Scope:
|
38
|
+
versions_for_scope = VERSIONS_FOR_SCOPE[@scope] or
|
39
|
+
raise SPF::InvalidScopeError.new("Invalid scope '#{@scope}'")
|
40
|
+
|
41
|
+
# Versions:
|
42
|
+
if self.instance_variable_defined?(:@versions)
|
43
|
+
if @versions.is_a?(Symbol)
|
44
|
+
# Single version specified as a symbol:
|
45
|
+
@versions = [@versions]
|
46
|
+
elsif not @versions.is_a?(Array)
|
47
|
+
# Something other than symbol or array specified:
|
48
|
+
raise SPF::InvalidOptionValueError.new(
|
49
|
+
"'versions' option must be symbol or array")
|
50
|
+
end
|
51
|
+
|
52
|
+
# All requested record versions must be supported:
|
53
|
+
unsupported_versions = @versions.select { |x|
|
54
|
+
not SCOPES_BY_VERSION[x]
|
55
|
+
}
|
56
|
+
if unsupported_versions.any?
|
57
|
+
raise SPF::InvalidOptionValueError.new(
|
58
|
+
"Unsupported record version(s): " +
|
59
|
+
unsupported_versions.map { |x| "'#{x}'" }.join(', '))
|
60
|
+
end
|
61
|
+
else
|
62
|
+
# No versions specified, use all versions relevant to scope:
|
63
|
+
@versions = versions_for_scope
|
64
|
+
end
|
65
|
+
|
66
|
+
# Identity:
|
67
|
+
raise SPF::OptionRequiredError.new(
|
68
|
+
"Missing required 'identity' option") unless @identity
|
69
|
+
raise SPF::InvalidOptionValueError.new(
|
70
|
+
"'identity' option must not be empty") if @identity.empty?
|
71
|
+
|
72
|
+
# Extract domain and localpart from identity:
|
73
|
+
if ((@scope == :mfrom or @scope == :pra) and
|
74
|
+
@identity =~ /^(.*)@(.*?)$/)
|
75
|
+
@localpart = $1
|
76
|
+
@domain = $2
|
77
|
+
else
|
78
|
+
@domain = @identity
|
79
|
+
end
|
80
|
+
# Lower-case domain and removee eventual trailing dot.
|
81
|
+
@domain.downcase!
|
82
|
+
@domain.chomp!('.')
|
83
|
+
if (not self.instance_variable_defined?(:@localpart) or
|
84
|
+
not @localpart or not @localpart.length > 0)
|
85
|
+
@localpart = DEFAULT_LOCALPART
|
86
|
+
end
|
87
|
+
|
88
|
+
# HELO identity:
|
89
|
+
if @scope == :helo
|
90
|
+
@helo_identity ||= @identity
|
91
|
+
end
|
92
|
+
|
93
|
+
# IP address:
|
94
|
+
if [:helo, :mfrom, :pra].find(@scope) and not self.instance_variable_defined?(:@ip_address)
|
95
|
+
raise SPF::OptionRequiredError.new("Missing required 'ip_address' option")
|
96
|
+
end
|
97
|
+
|
98
|
+
# Ensure ip_address is an IP object:
|
99
|
+
unless @ip_address.is_a?(IP)
|
100
|
+
@ip_address = IP.new(@ip_address)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Convert IPv4 address to IPv4-mapped IPv6 address:
|
104
|
+
|
105
|
+
if SPF::Util.ipv6_address_is_ipv4_mapped(self.ip_address)
|
106
|
+
@ip_address_v6 = @ip_address # Accept as IPv6 address as-is
|
107
|
+
@ip_address = SPF::Util.ipv6_address_to_ipv4(@ip_address)
|
108
|
+
elsif @ip_address.is_a?(IP::V4)
|
109
|
+
@ip_address_v6 = SPF::Util.ipv4_address_to_ipv6(@ip_address)
|
110
|
+
elsif @ip_address.is_a?(IP::V6)
|
111
|
+
@ip_address_v6 = @ip_address
|
112
|
+
else
|
113
|
+
raise SPF::InvalidOptionValueError.new("Unexpected IP address version");
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def new_sub_request(options)
|
118
|
+
obj = self.class.new(opt.merge(options))
|
119
|
+
obj.super_request = self
|
120
|
+
obj.root_request = super_request.root_request
|
121
|
+
return obj
|
122
|
+
end
|
123
|
+
|
124
|
+
def authority_domain
|
125
|
+
return (@_authority_domain or @domain)
|
126
|
+
end
|
127
|
+
|
128
|
+
def state(field, value = nil)
|
129
|
+
unless field
|
130
|
+
raise SPF::OptionRequiredError.new('Field name required')
|
131
|
+
end
|
132
|
+
if value and value === Fixnum
|
133
|
+
@state[field] = 0 unless @state[field]
|
134
|
+
@state[field] += value
|
135
|
+
else
|
136
|
+
@state[field] = value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
data/lib/spf/result.rb
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'spf/model'
|
2
|
+
require 'spf/util'
|
3
|
+
|
4
|
+
class SPF::Result < Exception
|
5
|
+
|
6
|
+
attr_reader :server, :request
|
7
|
+
|
8
|
+
class SPF::Result::Pass < SPF::Result
|
9
|
+
def code
|
10
|
+
:pass
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class SPF::Result::Fail < SPF::Result
|
15
|
+
def code
|
16
|
+
:fail
|
17
|
+
end
|
18
|
+
|
19
|
+
def authority_explanation
|
20
|
+
if self.instance_variable_defined?(:@authority_explanation)
|
21
|
+
return @authority_explanation
|
22
|
+
end
|
23
|
+
|
24
|
+
@authority_explanation = nil
|
25
|
+
|
26
|
+
server = @server
|
27
|
+
request = @request
|
28
|
+
|
29
|
+
authority_explanation_macrostring = request.state('authority_explanation')
|
30
|
+
|
31
|
+
# If an explicit explanation was specified by the authority domain...
|
32
|
+
if authority_explanation_macrostring
|
33
|
+
begin
|
34
|
+
# ... then try to expand it:
|
35
|
+
@authority_explanation = authority_explanation_macrostring.expand
|
36
|
+
rescue SPF::InvalidMacroString
|
37
|
+
# Igonre expansion errors and leave authority explanation undefined.
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# If no authority explanation could be determined so far...
|
42
|
+
unless @authority_explanation
|
43
|
+
@authority_explanation = server.default_authority_explanation.new({:request => request}).expand
|
44
|
+
end
|
45
|
+
return @authority_explanation
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class SPF::Result::SoftFail < SPF::Result
|
50
|
+
def code
|
51
|
+
:softfail
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class SPF::Result::Neutral < SPF::Result
|
56
|
+
def code
|
57
|
+
:neutral
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class SPF::Result::NeutralByDefault < SPF::Result::Neutral
|
62
|
+
# This is a special-case of the Neutral result that is thrown as a default
|
63
|
+
# when "falling off" the end of the record. See SPF::Record.eval().
|
64
|
+
NAME = :neutral_by_default
|
65
|
+
def code
|
66
|
+
:neutral_by_default
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class SPF::Result::None < SPF::Result
|
71
|
+
def code
|
72
|
+
:none
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class SPF::Result::Error < SPF::Result
|
77
|
+
def code
|
78
|
+
:error
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class SPF::Result::TempError < SPF::Result::Error
|
83
|
+
def code
|
84
|
+
:temperror
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class SPF::Result::PermError < SPF::Result::Error
|
89
|
+
def code
|
90
|
+
:permerror
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
RESULT_CLASSES = {
|
96
|
+
:pass => SPF::Result::Pass,
|
97
|
+
:fail => SPF::Result::Fail,
|
98
|
+
:softfail => SPF::Result::SoftFail,
|
99
|
+
:neutral => SPF::Result::Neutral,
|
100
|
+
:neutral_by_default => SPF::Result::NeutralByDefault,
|
101
|
+
:none => SPF::Result::None,
|
102
|
+
:error => SPF::Result::Error,
|
103
|
+
:permerror => SPF::Result::PermError,
|
104
|
+
:temperror => SPF::Result::TempError
|
105
|
+
}
|
106
|
+
|
107
|
+
RECEIVED_SPF_HEADER_NAME = 'Received-SPF'
|
108
|
+
|
109
|
+
RECEIVED_SPF_HEADER_SCOPE_NAMES_BY_SCOPE = {
|
110
|
+
:helo => 'helo',
|
111
|
+
:mfrom => 'envelope-from',
|
112
|
+
:pra => 'pra'
|
113
|
+
}
|
114
|
+
|
115
|
+
RECEIVED_SPF_HEADER_IDENTITY_KEY_NAMES_BY_SCOPE = {
|
116
|
+
:helo => 'helo',
|
117
|
+
:mfrom => 'envelope-from',
|
118
|
+
:pra => 'pra'
|
119
|
+
}
|
120
|
+
|
121
|
+
ATEXT_PATTERN = /[[:alnum:]!#\$%&'*+\-\/=?^_`{|}~]/
|
122
|
+
DOT_ATOM_PATTERN = /
|
123
|
+
(#{ATEXT_PATTERN})+ ( \. (#{ATEXT_PATTERN})+ )*
|
124
|
+
/x
|
125
|
+
|
126
|
+
def initialize(args = [])
|
127
|
+
@server = args.shift if args.any?
|
128
|
+
unless self.instance_variable_defined?(:@server)
|
129
|
+
raise SPF::OptionRequiredError.new('SPF server object required')
|
130
|
+
end
|
131
|
+
@request = args.shift if args.any?
|
132
|
+
unless self.instance_variable_defined?(:@request)
|
133
|
+
raise SPF::OptionRequiredError.new('Request object required')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def name
|
138
|
+
return self.code
|
139
|
+
end
|
140
|
+
|
141
|
+
def klass(name=nil)
|
142
|
+
if name
|
143
|
+
name = name.to_sym if name.is_a?(String)
|
144
|
+
return self.RESULT_CLASSES[name]
|
145
|
+
else
|
146
|
+
return name
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def isa_by_name(name)
|
151
|
+
suspect_class = self.klass(name)
|
152
|
+
return false unless suspect_class
|
153
|
+
return self.is_a?(suspect_class)
|
154
|
+
end
|
155
|
+
|
156
|
+
def is_code(code)
|
157
|
+
return self.isa_by_name(code)
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_s
|
161
|
+
return sprintf('%s (%s)', self.name, SPF::Util.sanitize_string(super.to_s))
|
162
|
+
end
|
163
|
+
|
164
|
+
def local_explanation
|
165
|
+
return @local_explanation if self.instance_variable_defined?(:@local_explanation)
|
166
|
+
|
167
|
+
# Prepare local explanation:
|
168
|
+
request = self.request
|
169
|
+
local_explanation = request.state(:local_explanation)
|
170
|
+
if local_explanation
|
171
|
+
local_explanation = sprintf('%s (%s)', local_explanation.expand, @text)
|
172
|
+
else
|
173
|
+
local_explanation = @text
|
174
|
+
end
|
175
|
+
|
176
|
+
# Resolve authority domains of root-request and bottom sub-requests:
|
177
|
+
root_request = request.root_request
|
178
|
+
local_explanation = (request == root_request or not root_request) ?
|
179
|
+
sprintf('%s: %s', request.authority_domain, local_explanation) :
|
180
|
+
sprintf('%s ... %s: %s', root_request.authority_domain, request.authority_domain, local_explanation)
|
181
|
+
|
182
|
+
return @local_explanation = SPF::Util.sanitize_string(local_explanation)
|
183
|
+
end
|
184
|
+
|
185
|
+
def received_spf_header
|
186
|
+
return @received_spf_header if self.instance_variable_defined?(:@received_spf_header)
|
187
|
+
scope_name = self.received_spf_header_scope_names_by_scope[@request.scope]
|
188
|
+
identify_key_name = self.received_spf_header_identity_key_names_by_scope[@request.scope]
|
189
|
+
info_pairs = [
|
190
|
+
:receiver => @server.hostname || 'unknown',
|
191
|
+
:identity => scope_name,
|
192
|
+
identity_key_name.to_sym => @request.identity,
|
193
|
+
:client_ip => SPF::Util.ip_address_to_string(@request.ip_address)
|
194
|
+
]
|
195
|
+
if @request.scope != :helo and @request.helo_identity
|
196
|
+
info_pairs[:helo] = @request.helo_identity
|
197
|
+
end
|
198
|
+
info_string = ''
|
199
|
+
while info_pairs.any?
|
200
|
+
key = info_pairs.shift
|
201
|
+
value = info_pairs.shift
|
202
|
+
info_string += '; ' unless info_string.blank?
|
203
|
+
if value !~ /^#{DOT_ATOM_PATTERN}$/o
|
204
|
+
value.gsub!(/(["\\])/, "\\#{$1}") # Escape '\' and '"' characters.
|
205
|
+
value = "\"#{value}\"" # Double-quote value.
|
206
|
+
end
|
207
|
+
info_string += "#{key}=#{value}"
|
208
|
+
end
|
209
|
+
return @received_spf_header = sprintf(
|
210
|
+
'%s: %s (%s) %s',
|
211
|
+
@received_spf_header_name,
|
212
|
+
self.code,
|
213
|
+
self.local_explanation,
|
214
|
+
info_string
|
215
|
+
)
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
data/lib/spf/util.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'ip'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
require 'spf/error'
|
5
|
+
|
6
|
+
#
|
7
|
+
# == SPF utility class
|
8
|
+
#
|
9
|
+
|
10
|
+
# Interface:
|
11
|
+
# ##############################################################################
|
12
|
+
|
13
|
+
|
14
|
+
# == SYNOPSIS
|
15
|
+
#
|
16
|
+
# require 'spf/util'
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# hostname = SPF::Util.hostname
|
20
|
+
#
|
21
|
+
# ipv6_address_v4mapped = SPF::Util.ipv4_address_to_ipv6(ipv4_address)
|
22
|
+
#
|
23
|
+
# ipv4_address = SPF::Util->ipv6_address_to_ipv4($ipv6_address_v4mapped)
|
24
|
+
#
|
25
|
+
# is_v4mapped = SPF::Util->ipv6_address_is_ipv4_mapped(ipv6_address)
|
26
|
+
#
|
27
|
+
# ip_address_string = SPF::Util->ip_address_to_string(ip_address)
|
28
|
+
#
|
29
|
+
# reverse_name = SPF::Util->ip_address_reverse(ip_address)
|
30
|
+
#
|
31
|
+
# validated_domain = SPF::Util->valid_domain_for_ip_address(
|
32
|
+
# spf_server, request, ip_address, domain,
|
33
|
+
# find_best_match, # Defaults to false
|
34
|
+
# accept_any_domain # Defaults to false
|
35
|
+
# )
|
36
|
+
# sanitized_string = SPF::Util->sanitize_string(string)
|
37
|
+
#
|
38
|
+
|
39
|
+
module SPF
|
40
|
+
module Util
|
41
|
+
|
42
|
+
def self.ipv4_mapped_ipv6_address_pattern
|
43
|
+
/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})/i
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.hostname
|
47
|
+
return @hostname ||= Socket.gethostbyname(Socket.gethostname).first
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.ipv4_address_to_ipv6(ipv4_address)
|
51
|
+
unless IP::V4 === ipv4_address
|
52
|
+
raise SPF::InvalidOptionValueError.new('IP::V4 address expected')
|
53
|
+
end
|
54
|
+
return IP.new("::ffff:#{ipv4_address.to_addr}/#{ipv4_address.pfxlen - 32 + 128}")
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.ipv6_address_to_ipv4(ipv6_address)
|
58
|
+
unless IP::V6 === ipv6_address and ipv6_address.ipv4_mapped?
|
59
|
+
raise SPF::InvalidOptionValueError, 'IPv4-mapped IP::V6 address expected'
|
60
|
+
end
|
61
|
+
return ipv6_address.native
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.ipv6_address_is_ipv4_mapped(ipv6_address)
|
65
|
+
return ipv6_address.ipv4_mapped?
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.ip_address_to_string(ip_address)
|
69
|
+
unless IP::V4 === ip_address or IP::V6 === ip_address
|
70
|
+
raise SPF::InvalidOptionValueError.new('IP::V4 or IP::V6 address expected')
|
71
|
+
end
|
72
|
+
return ip_address.to_addr
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.ip_address_reverse(ip_address)
|
76
|
+
unless IP::V4 === ip_address or IP::V6 === ip_address
|
77
|
+
raise SPF::InvalidOptionValueError.new('IP::V4 or IP::V6 address expected')
|
78
|
+
end
|
79
|
+
# Treat IPv4-mapped IPv6 addresses as IPv4 addresses:
|
80
|
+
ip_address = ipv6_address_to_ipv4(ip_address) if ip_address.ipv4_mapped?
|
81
|
+
case ip_address
|
82
|
+
when IP::V4
|
83
|
+
octets = ip_address.to_addr.split('.').first(ip_address.pfxlen / 8)
|
84
|
+
return "#{octets .reverse.join('.')}.in-addr.arpa."
|
85
|
+
when IP::V6
|
86
|
+
nibbles = ip_address.to_hex .split('') .first(ip_address.pfxlen / 4)
|
87
|
+
return "#{nibbles.reverse.join('.')}.ip6.arpa."
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.valid_domain_for_ip_address(
|
92
|
+
sever, request, ip_address, domain,
|
93
|
+
find_best_match = false,
|
94
|
+
accept_any_domain = false
|
95
|
+
)
|
96
|
+
# TODO
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.sanitize_string(string)
|
100
|
+
return \
|
101
|
+
string &&
|
102
|
+
string.
|
103
|
+
gsub(/([\x00-\x1f\x7f-\xff])/) { |c| sprintf('\x%02x', c.ord) }.
|
104
|
+
gsub(/([\u{0100}-\u{ffff}])/) { |u| sprintf('\x{%04x}', u.ord) }
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# vim:sw=2 sts=2
|
data/lib/spf/version.rb
ADDED
data/lib/spf.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spf/error'
|
2
|
+
require 'spf/model'
|
3
|
+
require 'spf/request'
|
4
|
+
require 'spf/eval'
|
5
|
+
require 'spf/macro_string'
|
6
|
+
require 'spf/util'
|
7
|
+
|
8
|
+
#
|
9
|
+
# == SPF - An object-oriented implementation of Sender Policy Framework
|
10
|
+
#
|
11
|
+
# == SYNOPSIS
|
12
|
+
#
|
13
|
+
# <tt>
|
14
|
+
#
|
15
|
+
# require 'spf'
|
16
|
+
#
|
17
|
+
# spf_server = SPF::Server.new
|
18
|
+
#
|
19
|
+
# request = SPF::Request.new({
|
20
|
+
# :versions => [1, 2], # optional
|
21
|
+
# :scope => 'mfrom', # or 'helo', 'pra'
|
22
|
+
# :identity => 'fred@example.com',
|
23
|
+
# :ip_address => '192.168.0.1',
|
24
|
+
# :helo_identity => 'mta.example.com' # optional,
|
25
|
+
# # for %{h} macro expansion
|
26
|
+
# })
|
27
|
+
#
|
28
|
+
# result = spf_server.process(request)
|
29
|
+
# puts result
|
30
|
+
# result_code = result.code
|
31
|
+
# local_exp = result.local_explanation
|
32
|
+
# authority_exp = result.authority_explanation
|
33
|
+
# if result.is_code(:fail)
|
34
|
+
# spf_header = result.received_spf_header
|
35
|
+
#
|
36
|
+
# </tt>
|
37
|
+
#
|
38
|
+
# == DESCRIPTION
|
39
|
+
#
|
40
|
+
# <b>SPF</b> is an object-oriented implementation of Sender Policy Framework
|
41
|
+
# (SPF). See http://www.openspf.org for more information about SPF.
|
42
|
+
#
|
43
|
+
# This class collection aims to fully conform to the SPF specification (RFC
|
44
|
+
# 4408 so as to serve both as a production quality SPF implementation and as a
|
45
|
+
# reference for other developers of SPF implementations.
|
46
|
+
#
|
47
|
+
#
|
48
|
+
# vim:sw=2 sts=2
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'spf-ruby'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
data/spec/spf_spec.rb
ADDED
data/spf.gemspec
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "spf"
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Andrew Flury", "Julian Mehnle"]
|
12
|
+
s.date = "2013-10-14"
|
13
|
+
s.description = " An object-oriented Ruby implementation of the Sender Policy Framework (SPF)\n e-mail sender authentication system, fully compliant with RFC 4408.\n"
|
14
|
+
s.email = ["code@agari.com", "aflury@agari.com", "jmehnle@agari.com"]
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".document",
|
20
|
+
".rspec",
|
21
|
+
".ruby-version",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"lib/spf.rb",
|
27
|
+
"lib/spf/error.rb",
|
28
|
+
"lib/spf/eval.rb",
|
29
|
+
"lib/spf/macro_string.rb",
|
30
|
+
"lib/spf/model.rb",
|
31
|
+
"lib/spf/request.rb",
|
32
|
+
"lib/spf/result.rb",
|
33
|
+
"lib/spf/util.rb",
|
34
|
+
"lib/spf/version.rb",
|
35
|
+
"spec/spec_helper.rb",
|
36
|
+
"spec/spf_spec.rb",
|
37
|
+
"spf.gemspec"
|
38
|
+
]
|
39
|
+
s.homepage = "https://github.com/agaridata/spf-ruby"
|
40
|
+
s.licenses = ["none (all rights reserved)"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubygems_version = "1.8.23"
|
43
|
+
s.summary = "Implementation of the Sender Policy Framework"
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
|
50
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
51
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
52
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.7"])
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<rspec>, ["~> 2.8.0"])
|
55
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
56
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
57
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<rspec>, ["~> 2.8.0"])
|
61
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
62
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
63
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|