spf 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|