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/lib/spf/model.rb ADDED
@@ -0,0 +1,985 @@
1
+ require 'ip'
2
+
3
+ require 'spf/util'
4
+
5
+
6
+ class IP
7
+ def contains?(ip_address)
8
+ return (
9
+ self.to_irange.first <= ip_address.to_i and
10
+ self.to_irange.last >= ip_address.to_i)
11
+ end
12
+ end
13
+
14
+
15
+ class SPF::Record
16
+ DEFAULT_QUALIFIER = '+';
17
+ end
18
+
19
+ class SPF::Term
20
+
21
+ NAME_PATTERN = '[[:alpha:]] [[:alnum:]\\-_\\.]*'
22
+
23
+ MACRO_LITERAL_PATTERN = "[!-$&-~]"
24
+ MACRO_DELIMITER = "[\\.\\-+,\\/_=]"
25
+ MACRO_TRANSFORMERS_PATTERN = "\\d*r?"
26
+ MACRO_EXPAND_PATTERN = "
27
+ %
28
+ (?:
29
+ { [[:alpha:]] } #{MACRO_TRANSFORMERS_PATTERN} #{MACRO_DELIMITER}* } |
30
+ [%_-]
31
+ )
32
+ "
33
+
34
+ MACRO_STRING_PATTERN = "
35
+ (?:
36
+ #{MACRO_EXPAND_PATTERN} |
37
+ #{MACRO_LITERAL_PATTERN}
38
+ )*
39
+ "
40
+
41
+ TOPLABEL_PATTERN = "
42
+ [[:alnum:]_-]+ - [[:alnum:]-]* [[:alnum:]] |
43
+ [[:alnum:]]* [[:alpha:]] [[:alnum:]]*
44
+ "
45
+
46
+ DOMAIN_END_PATTERN = "
47
+ (?: \\. #{TOPLABEL_PATTERN} \\.? |
48
+ #{MACRO_EXPAND_PATTERN}
49
+ )
50
+ "
51
+
52
+ DOMAIN_SPEC_PATTERN = " #{MACRO_STRING_PATTERN} #{DOMAIN_END_PATTERN} "
53
+
54
+ QNUM_PATTERN = " (?: 25[0-5] | 2[0-4]\\d | 1\\d\\d | [1-9]\\d | \\d ) "
55
+ IPV4_ADDRESS_PATTERN = " #{QNUM_PATTERN} (?: \\. #{QNUM_PATTERN}){3} "
56
+
57
+ HEXWORD_PATTERN = "[[:xdigit:]]{1,4}"
58
+
59
+ TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN = /
60
+ #{HEXWORD_PATTERN} : #{HEXWORD_PATTERN} | #{IPV4_ADDRESS_PATTERN}
61
+ /x
62
+
63
+ IPV6_ADDRESS_PATTERN = "
64
+ # x:x:x:x:x:x:x:x | x:x:x:x:x:x:n.n.n.n
65
+ (?: #{HEXWORD_PATTERN} : ){6} #{TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN} |
66
+ # x::x:x:x:x:x:x | x::x:x:x:x:n.n.n.n
67
+ (?: #{HEXWORD_PATTERN} : ){1} : (?: #{HEXWORD_PATTERN} : ){4} #{TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN} |
68
+ # x[:x]::x:x:x:x:x | x[:x]::x:x:x:n.n.n.n
69
+ (?: #{HEXWORD_PATTERN} : ){1,2} : (?: #{HEXWORD_PATTERN} : ){3} #{TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN} |
70
+ # x[:...]::x:x:x:x | x[:...]::x:x:n.n.n.n
71
+ (?: #{HEXWORD_PATTERN} : ){1,3} : (?: #{HEXWORD_PATTERN} : ){2} #{TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN} |
72
+ # x[:...]::x:x:x | x[:...]::x:n.n.n.n
73
+ (?: #{HEXWORD_PATTERN} : ){1,4} : (?: #{HEXWORD_PATTERN} : ){1} #{TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN} |
74
+ # x[:...]::x:x | x[:...]::n.n.n.n
75
+ (?: #{HEXWORD_PATTERN} : ){1,5} : #{TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN} |
76
+ # x[:...]::x | -
77
+ (?: #{HEXWORD_PATTERN} : ){1,6} : #{HEXWORD_PATTERN} |
78
+ # x[:...]:: |
79
+ (?: #{HEXWORD_PATTERN} : ){1,7} : |
80
+ # ::[...:]x | -
81
+ :: (?: #{HEXWORD_PATTERN} : ){0,6} #{HEXWORD_PATTERN} |
82
+ # - | ::[...:]n.n.n.n
83
+ :: (?: #{HEXWORD_PATTERN} : ){0,5} #{TWO_HEXWORDS_OR_IPV4_ADDRESS_PATTERN} |
84
+ # :: | -
85
+ ::
86
+ "
87
+
88
+ def self.new_from_string(text, options = {})
89
+ #term = SPF::Term.new(options, {:text => text})
90
+ options[:text] = text
91
+ term = self.new(options)
92
+ term.parse
93
+ return term
94
+ end
95
+
96
+ def parse_domain_spec(required = false)
97
+ if @parse_text.sub!(/^(#{DOMAIN_SPEC_PATTERN})/x, '')
98
+ domain_spec = $1
99
+ domain_spec.sub!(/^(.*?)\.?$/, $1)
100
+ @domain_spec = SPF::MacroString.new({:text => domain_spec})
101
+ elsif required
102
+ raise SPF::TermDomainSpecExpectedError.new(
103
+ "Missing required domain-spec in '#{@text}'")
104
+ end
105
+ end
106
+
107
+ def parse_ipv4_address(required = false)
108
+ if @parse_text.sub!(/^(#{IPV4_ADDRESS_PATTERN})/x, '')
109
+ @ip_address = $1
110
+ elsif required
111
+ raise SPF::TermIPv4AddressExpectedError.new(
112
+ "Missing required IPv4 address in '#{@text}'");
113
+ end
114
+ end
115
+
116
+ def parse_ipv4_prefix_length(required = false)
117
+ if @parse_text.sub!(/^\/(\d+)/, '')
118
+ bits = $1.to_i
119
+ unless bits and bits >= 0 and bits <= 32 and $1 !~ /^0./
120
+ raise SPF::TermIPv4PrefixLengthExpected.new(
121
+ "Invalid IPv4 prefix length encountered in '#{@text}'")
122
+ end
123
+ @ipv4_prefix_length = bits
124
+ elsif required
125
+ raise SPF::TermIPv4PrefixLengthExpected.new(
126
+ "Missing required IPv4 prefix length in '#{@text}")
127
+ else
128
+ @ipv4_prefix_length = self.default_ipv4_prefix_length
129
+ end
130
+ end
131
+
132
+ def parse_ipv4_network(required = false)
133
+ self.parse_ipv4_address(required)
134
+ self.parse_ipv4_prefix_length
135
+ @ip_network = IP.new("#{@ip_address}/#{@ipv4_prefix_length}")
136
+ end
137
+
138
+ def parse_ipv6_address(required = false)
139
+ if @parse_text.sub!(/(#{IPV6_ADDRESS_PATTERN})(?=\/|$)/x, '')
140
+ @ip_address = $1
141
+ elsif required
142
+ raise SPF::TermIPv6AddressExpected.new(
143
+ "Missing required IPv6 address in '#{@text}'")
144
+ end
145
+ end
146
+
147
+ def parse_ipv6_prefix_length(required = false)
148
+ if @parse_text.sub!(/^\/(\d+)/, '')
149
+ bits = $1.to_i
150
+ unless bits and bits >= 0 and bits <= 128 and $1 !~ /^0./
151
+ raise SPF::TermIPv6PrefixLengthExpectedError.new(
152
+ "Invalid IPv6 prefix length encountered in '#{@text}'")
153
+ @ipv6_prefix_length = bits
154
+ end
155
+ elsif required
156
+ raise SPF::TermIPvPrefixLengthExpected.new(
157
+ "Missing required IPv6 prefix length in '#{@text}'")
158
+ else
159
+ @ipv6_prefix_length = self.default_ipv6_prefix_length
160
+ end
161
+ end
162
+
163
+ def parse_ipv6_network(required = false)
164
+ self.parse_ipv6_address(required)
165
+ self.parse_ipv6_prefix_length
166
+ # XXX we shouldn't need to check for this.
167
+ @ipv6_prefix_length = self.default_ipv6_prefix_length unless @ipv6_prefix_length
168
+ @ip_network = IP.new("#{@ip_address}/#{@ipv6_prefix_length}")
169
+ end
170
+
171
+ def parse_ipv4_ipv6_prefix_lengths
172
+ self.parse_ipv4_prefix_length
173
+ if self.instance_variable_defined?(:@ipv4_prefix_length) and # An IPv4 prefix length has been parsed, and
174
+ @parse_text.sub!(/^\//, '') # another slash is following.
175
+ # Parse an IPv6 prefix length:
176
+ self.parse_ipv6_prefix_length(true)
177
+ end
178
+ end
179
+
180
+ def text
181
+ if self.instance_variable_defined?(:@text)
182
+ return @text
183
+ else
184
+ raise SPF::NoUnparsedTextError
185
+ end
186
+ end
187
+
188
+ end
189
+
190
+ class SPF::Mech < SPF::Term
191
+
192
+ DEFAULT_QUALIFIER = SPF::Record::DEFAULT_QUALIFIER
193
+ def default_ipv4_prefix_length; 32; end
194
+ def default_ipv6_prefix_length; 128; end
195
+
196
+ QUALIFIER_PATTERN = '[+\\-~\\?]'
197
+ NAME_PATTERN = "#{NAME_PATTERN} (?= [:\\/\\x20] | $ )"
198
+
199
+ EXPLANATION_TEMPLATES_BY_RESULT_CODE = {
200
+ :pass => "Sender is authorized to use '%{s}' in '%{_scope}' identity",
201
+ :fail => "Sender is not authorized to use '%{s}' in '%{_scope}' identity",
202
+ :softfail => "Sender is not authorized to use '%{s}' in '%{_scope}' identity, however domain is not currently prepared for false failures",
203
+ :neutral => "Domain does not state whether sender is authorized to use '%{s}' in '%{_scope}' identity"
204
+ }
205
+
206
+ def initialize(options)
207
+ super()
208
+ @text = options[:text]
209
+ if not self.instance_variable_defined?(:@parse_text)
210
+ @parse_text = @text
211
+ end
212
+ if self.instance_variable_defined?(:@domain_spec) and
213
+ not @domain_spec.is_a?(SPF::MacroString)
214
+ @domain_spec = SPF::MacroString.new({:text => @domain_spec})
215
+ end
216
+ end
217
+
218
+ def parse
219
+ if not @parse_text
220
+ raise SPF::NothingToParseError.new('Nothing to parse for mechanism')
221
+ end
222
+ parse_qualifier
223
+ parse_name
224
+ parse_params
225
+ parse_end
226
+ end
227
+
228
+ def parse_qualifier
229
+ if @parse_text.sub!(/(#{QUALIFIER_PATTERN})?/x, '')
230
+ @qualifier = $1 or DEFAULT_QUALIFIER
231
+ else
232
+ raise SPF::InvalidMechQualifierError.new(
233
+ "Invalid qualifier encountered in '#{@text}'")
234
+ end
235
+ end
236
+
237
+ def parse_name
238
+ if @parse_text.sub!(/^ (#{NAME_PATTERN}) (?: : (?=.) )? /x, '')
239
+ @name = $1
240
+ else
241
+ raise SPF::InvalidMech.new(
242
+ "Unexpected mechanism encountered in '#{@text}'")
243
+ end
244
+ end
245
+
246
+ def parse_params
247
+ # Parse generic string of parameters text (should be overridden in sub-classes):
248
+ if @parse_text.sub!(/^(.*)/, '')
249
+ @params_text = $1
250
+ end
251
+ end
252
+
253
+ def parse_end
254
+ unless @parse_text == ''
255
+ raise SPF::JunkInTermError.new("Junk encountered in mechanism '#{@text}'")
256
+ end
257
+ @parse_text = nil
258
+ end
259
+
260
+ def qualifier
261
+ # Read-only!
262
+ return @qualifier if self.instance_variable_defined?(:@qualifier) and @qualifier
263
+ return DEFAULT_QUALIFIER
264
+ end
265
+
266
+ def to_s
267
+ @params = nil unless self.instance_variable_defined?(:@params)
268
+
269
+ return sprintf(
270
+ '%s%s%s',
271
+ @qualifier == DEFAULT_QUALIFIER ? '' : @qualifier,
272
+ @name,
273
+ @params ? @params : ''
274
+ )
275
+ end
276
+
277
+ def domain(server, request)
278
+ if self.instance_variable_defined?(:@domain_spec) and @domain_spec
279
+ return @domain_spec
280
+ end
281
+ return request.authority_domain
282
+ end
283
+
284
+ def match_in_domain(server, request, domain)
285
+ domain = self.domain(server, request) unless domain
286
+
287
+ ipv4_prefix_length = @ipv4_prefix_length
288
+ ipv6_prefix_length = @ipv6_prefix_length
289
+ addr_rr_type = request.ip_address.version == 4 ? 'A' : 'AAAA'
290
+ packet = server.dns_lookup(domain, addr_rr_type)
291
+ server.count_void_dns_lookup(request) unless (rrs = packet.answer)
292
+
293
+ rrs.each do |rr|
294
+ if rr.type == 'A'
295
+ network = IP.new("#{rr.address}/#{ipv4_prefix_length}")
296
+ return true if network.contains?(request.ip_address)
297
+ elsif rr.type == 'AAAA'
298
+ network = IP.new("#{rr.address}/#{ipv6_prefix_length}")
299
+ return true if network.contains?(request.ip_address_v6)
300
+ elsif rr.type == 'CNAME'
301
+ # Ignore -- we should have gotten the A/AAAA records anyway.
302
+ else
303
+ # Unexpected RR type.
304
+ # TODO: Generate debug info or ignore silently.
305
+ end
306
+ end
307
+ return false
308
+ end
309
+
310
+ def explain(server, request, result)
311
+ explanation_template = self.explanation_template(server, request, result)
312
+ return unless explanation_template
313
+ begin
314
+ explanation = SPF::MacroString.new({
315
+ :text => explanation_template,
316
+ :server => server,
317
+ :request => request,
318
+ :is_explanation => true
319
+ })
320
+ request.state(:local_explanation, explanation)
321
+ rescue SPF::Error
322
+ rescue SPF::Result
323
+ end
324
+ end
325
+
326
+ def explanation_template(server, request, result)
327
+ return EXPLANATION_TEMPLATES_BY_RESULT_CODE[result.code]
328
+ end
329
+
330
+
331
+ class SPF::Mech::A < SPF::Mech
332
+
333
+ NAME = 'a'
334
+
335
+ def parse_params
336
+ self.parse_domain_spec
337
+ self.parse_ipv4_ipv6_prefix_lengths
338
+ end
339
+
340
+ def params
341
+ params = ''
342
+ if @domain_spec
343
+ params += ':' + @domain_spec if @domain_spec
344
+ end
345
+ if @ipv4_prefix_length and @ipv4_prefix_length != self.default_ipv4_prefix_length
346
+ params += '/' + @ipv4_prefix_length
347
+ end
348
+ if @ipv6_prefix_length and @ipv6_prefix_length != DEFAULT_IPV6_PREFIX_LENGTH
349
+ params += '//' + @ipv6_prefix_length
350
+ end
351
+ return params
352
+ end
353
+
354
+ def match(server, request)
355
+ server.count_dns_interactive_term(request)
356
+ return self.match_in_domain(server, request)
357
+ end
358
+
359
+ end
360
+
361
+ class SPF::Mech::All < SPF::Mech
362
+
363
+ NAME = 'all'
364
+
365
+ def parse_params
366
+ # No parameters.
367
+ end
368
+
369
+ def match(server, request)
370
+ return true
371
+ end
372
+
373
+ end
374
+
375
+ class SPF::Mech::Exists < SPF::Mech
376
+
377
+ NAME = 'exists'
378
+
379
+ def parse_params
380
+ self.parse_domain_spec(true)
381
+ end
382
+
383
+ def params
384
+ return @domain_spec ? ':' + @domain_spec : nill
385
+ end
386
+
387
+ def match(server, request)
388
+ server.count_dns_interactive_term(request)
389
+
390
+ domain = self.domain(server, request)
391
+ packet = server.dns_lookup(domain, 'A')
392
+ rrs = (packet.answer or server.count_void_dns_lookup(request))
393
+ rrs.each do |rr|
394
+ return true if rr.type == 'A'
395
+ end
396
+ return false
397
+ end
398
+
399
+ end
400
+
401
+ class SPF::Mech::IP4 < SPF::Mech
402
+
403
+ NAME = 'ip4'
404
+
405
+ def parse_params
406
+ self.parse_ipv4_network(true)
407
+ end
408
+
409
+ def params
410
+ result = @ip_network.addr
411
+ if @ip_network.masklen != @default_ipv4_prefix_length
412
+ result += "/#{@ip_network.masklen}"
413
+ end
414
+ return result
415
+ end
416
+
417
+ def match(server, request)
418
+ ip_network_v6 = @ip_network.is_a?(IP::V4) ?
419
+ SPF::Util.ipv4_address_to_ipv6(@ip_network) :
420
+ @ip_network
421
+ return ip_network_v6.contains?(request.ip_address_v6)
422
+ end
423
+
424
+ end
425
+
426
+ class SPF::Mech::IP6 < SPF::Mech
427
+
428
+ NAME = 'ip6'
429
+
430
+ def parse_params
431
+ self.parse_ipv6_network(true)
432
+ end
433
+
434
+ def params
435
+ params = ':' + @ip_network.short
436
+ params += '/' + @ip_network.masklen if
437
+ @ip_network.masklen != DEFAULT_IPV6_PREFIX_LENGTH
438
+ return params
439
+ end
440
+
441
+ def match(server, request)
442
+ return @ip_network.contains?(request.ip_address_v6)
443
+ end
444
+
445
+ end
446
+
447
+ class SPF::Mech::Include < SPF::Mech
448
+
449
+ NAME = 'include'
450
+
451
+ def parse_params
452
+ self.parse_domain_spec(true)
453
+ end
454
+
455
+ def params
456
+ return @domain_spec ? ':' + @domain_spec : nil
457
+ end
458
+
459
+ def match(server, request)
460
+ server.count_dns_interactive_term(request)
461
+
462
+ # Create sub-request with mutated authority domain:
463
+ authority_domain = self.domain(server, request)
464
+ sub_request = request.new_sub_request({:authority_domain => authority_domain})
465
+
466
+ # Process sub-request:
467
+ result = server.process(sub_request)
468
+
469
+ # Translate result of sub-request (RFC 4408, 5.9):
470
+
471
+ return true if
472
+ result.is_a?(SPF::Result::Pass)
473
+
474
+ return false if
475
+ result.is_a?(SPF::Result::Fail) or
476
+ result.is_a?(SPF::Result::SoftFail) or
477
+ result.is_a?(SPF::Result::Neutral)
478
+
479
+ server.throw_result('permerror', request,
480
+ "Include domain '#{authority_domain}' has no applicable sender policy") if
481
+ result.is_a?(SPF::Result::None)
482
+
483
+ # Propagate any other results (including {Perm,Temp}Error) as-is:
484
+ raise result
485
+ end
486
+ end
487
+
488
+ class SPF::Mech::MX < SPF::Mech
489
+
490
+ NAME = 'mx'
491
+
492
+ def parse_params
493
+ self.parse_domain_spec
494
+ self.parse_ipv4_ipv6_prefix_lengths
495
+ end
496
+
497
+ def params
498
+ params = ''
499
+ if @domain_spec
500
+ params += ':' + @domain_spec
501
+ end
502
+ if @ipv4_prefix_length and @ipv4_prefix_length != self.default_ipv4_prefix_length
503
+ params += '/' + @ipv4_prefix_length
504
+ end
505
+ if @ipv6_prefix_length and @ipv6_prefix_length != DEFAULT_IPV6_PREFIX_LENGTH
506
+ params += '//' + @ipv6_prefix_length
507
+ end
508
+ return params
509
+ end
510
+
511
+ def match(server, request)
512
+
513
+ server.count_dns_interactive_term(request)
514
+
515
+ target_domain = self.domain(server, request)
516
+ mx_packet = server.dns_lookup(target_domain, 'MX')
517
+ mx_rrs = (mx_packet.answer or server.count_void_dns_lookup(request))
518
+
519
+ # Respect the MX mechanism lookups limit (RFC 4408, 5.4/3/4):
520
+ if server.max_name_lookups_per_mx_mech
521
+ mx_rrs = max_rrs[0, server.max_name_lookups_per_mx_mech]
522
+ end
523
+
524
+ # TODO: Use A records from packet's "additional" section? Probably not.
525
+
526
+ # Check MX records:
527
+ mx_rrs.each do |rr|
528
+ if rr.type == 'MX'
529
+ return true if
530
+ self.match_in_domain(server, request, rr.exchange)
531
+ else
532
+ # Unexpected RR type.
533
+ # TODO: Generate debug info or ignore silently.
534
+ end
535
+ end
536
+ return false
537
+ end
538
+
539
+ end
540
+
541
+ class SPF::Mech::PTR < SPF::Mech
542
+ NAME = 'ptr'
543
+
544
+ def parse_params
545
+ self.parse_domain_spec
546
+ end
547
+
548
+ def params
549
+ return @domain_spec ? ':' + @domain_spec : nil
550
+ end
551
+
552
+ def match(server, request)
553
+ return SPF::Util.valid_domain_for_ip_address(
554
+ server, request, request.ip_address, self.domain(server, request)) ?
555
+ true : false
556
+ end
557
+ end
558
+ end
559
+
560
+ class SPF::Mod < SPF::Term
561
+
562
+ def initialize(options = {})
563
+ @parse_text = options[:parse_text]
564
+ @text = options[:text]
565
+ @domain_spec = options[:domain_spec]
566
+
567
+ @parse_text = @text unless @parse_text
568
+
569
+ if @domain_spec and not @domain_spec.is_a?(SPF::MacroString)
570
+ @domain_spec = SPF::MacroString.new({:text => @domain_spec})
571
+ end
572
+ end
573
+
574
+ def parse
575
+ raise SPF::NothingToParseError('Nothing to parse for modifier') unless @parse_text
576
+ self.parse_name
577
+ self.parse_params(true)
578
+ self.parse_end
579
+ end
580
+
581
+ def parse_name
582
+ @parse_text.sub!(/^(#{NAME})=/i, '')
583
+ if $1
584
+ @name = $1
585
+ else
586
+ raise SPF::InvalidModError.new(
587
+ "Unexpected modifier name encoutered in #{@text}")
588
+ end
589
+ end
590
+
591
+ def parse_params(required = false)
592
+ # Parse generic macro string of parameters text (should be overridden in sub-classes):
593
+ @parse_text.sub!(/^(#{MACRO_STRING_PATTERN})$/x, '')
594
+ if $1
595
+ @params_text = $1
596
+ elsif required
597
+ raise SPF::InvalidMacroStringError.new(
598
+ "Invalid macro string encountered in #{@text}")
599
+ end
600
+ end
601
+
602
+ def parse_end
603
+ unless @parse_text == ''
604
+ raise SPF::JunkInTermError("Junk encountered in modifier #{@text}")
605
+ end
606
+ @parse_text = nil
607
+ end
608
+
609
+ def to_s
610
+ return sprintf(
611
+ '%s=%s',
612
+ @name,
613
+ @params ? @params : ''
614
+ )
615
+ end
616
+
617
+ class SPF::GlobalMod < SPF::Mod
618
+ end
619
+
620
+ class SPF::PositionalMod < SPF::Mod
621
+ end
622
+
623
+ class SPF::UnknownMod < SPF::Mod
624
+ end
625
+
626
+ class SPF::Mod::Exp < SPF::Mod
627
+
628
+ attr_reader :domain_spec
629
+
630
+ NAME = 'exp'
631
+ PRECEDENCE = 0.2
632
+
633
+ def parse_params
634
+ self.parse_domain_spec(true)
635
+ end
636
+
637
+ def params
638
+ return @domain_spec
639
+ end
640
+
641
+ def process(server, request, result)
642
+ begin
643
+ exp_domain = @domain_spec.new({:server => server, :request => request})
644
+ txt_packet = server.dns_lookup(exp_domain, 'TXT')
645
+ txt_rrs = txt_packet.answer.select {|x| x.type == 'TXT'}.map {|x| x.answer}
646
+ unless text_rrs.length > 0
647
+ server.throw_result(:permerror, request,
648
+ "No authority explanation string available at domain '#{exp_domain}'") # RFC 4408, 6.2/4
649
+ end
650
+ unless text_rrs.length == 1
651
+ server.throw_result(:permerror, request,
652
+ "Redundant authority explanation strings found at domain '#{exp_domain}'") # RFC 4408, 6.2/4
653
+ end
654
+ explanation = SPF::MacroString.new(
655
+ :text => txt_rrs[0].char_str_list.join(''),
656
+ :server => server,
657
+ :request => request,
658
+ :is_explanation => true
659
+ )
660
+ request.state(:authority_explanation, explanation)
661
+ rescue SPF::DNSError, SPF::Result::Error
662
+ # Ignore DNS and other errors.
663
+ end
664
+ return request
665
+ end
666
+ end
667
+
668
+ class SPF::Mod::Redirect < SPF::GlobalMod
669
+
670
+ attr_reader :domain_spec
671
+
672
+ NAME = 'redirect'
673
+ PRECEDENCE = 0.8
674
+
675
+ def parse_params
676
+ self.parse_domain_spec(true)
677
+ end
678
+
679
+ def params
680
+ return @domain_spec
681
+ end
682
+
683
+ def process(server, request, result)
684
+ server.count_dns_interactive_term(request)
685
+
686
+ # Only perform redirection if no mechanism matched (RFC 4408, 6.1/1):
687
+ return unless result.is_a?(SPF::Result::NeutralByDefault)
688
+
689
+ # Create sub-request with mutated authorithy domain:
690
+ authority_domain = @domain_spec.new({:server => server, :request => request})
691
+ sub_request = request.new_sub_request({:authority_domain => authority_domain})
692
+
693
+ # Process sub-request:
694
+ result = server.process(sub_request)
695
+
696
+ # Translate result of sub-request (RFC 4408, 6.1/4):
697
+ if result.is_a?(SPF::Result::None)
698
+ server.throw_result(:permerror, request,
699
+ "Redirect domain '#{authority_domain}' has no applicable sender policy")
700
+ end
701
+
702
+ # Propagate any other results as-is:
703
+ result.throw
704
+ end
705
+ end
706
+ end
707
+
708
+ class SPF::Record
709
+
710
+ attr_reader :terms, :text
711
+
712
+ RESULTS_BY_QUALIFIER = {
713
+ '' => :pass,
714
+ '+' => :pass,
715
+ '-' => :fail,
716
+ '~' => :softfail,
717
+ '?' => :neutral
718
+ }
719
+
720
+ def initialize(options)
721
+ super()
722
+ @parse_text = @text = options[:text] if not self.instance_variable_defined?(:@parse_text)
723
+ @terms ||= []
724
+ @global_mods ||= {}
725
+ end
726
+
727
+ def self.new_from_string(text, options = {})
728
+ options[:text] = text
729
+ record = new(options)
730
+ record.parse
731
+ return record
732
+ end
733
+
734
+ def parse
735
+ unless self.instance_variable_defined?(:@parse_text) and @parse_text
736
+ raise SPF::NothingToParseError.new('Nothing to parse for record')
737
+ end
738
+ self.parse_version_tag
739
+ self.parse_term while @parse_text.length > 0
740
+ #self.parse_end
741
+ end
742
+
743
+ def parse_version_tag
744
+ #@parse_text.sub!(self.version_tag_pattern, '')
745
+ @parse_text.sub!(/^#{self.version_tag_pattern}\s+/ix, '')
746
+ unless $1
747
+ raise SPF::InvalidRecordVersionError.new(
748
+ "Not a '#{self.version_tag}' record: '#{@text}'")
749
+ end
750
+
751
+ end
752
+
753
+ def parse_term
754
+ regex = /
755
+ ^
756
+ (
757
+ #{SPF::Mech::QUALIFIER_PATTERN}?
758
+ (#{SPF::Mech::NAME_PATTERN})
759
+ [^\x20]*
760
+ )
761
+ (?: \x20+ | $ )
762
+ /x
763
+
764
+
765
+ if @parse_text.sub!(regex, '') and $&
766
+ # Looks like a mechanism:
767
+ mech_text = $1
768
+ mech_name = $2.downcase
769
+ mech_class = self.mech_classes[mech_name.to_sym]
770
+ unless mech_class
771
+ raise SPF::InvalidMech.new("Unknown mechanism type '#{mech_name}' in '#{@version_tag}' record")
772
+ end
773
+ mech = mech_class.new_from_string(mech_text)
774
+ @terms << mech
775
+ elsif (
776
+ @parse_text.sub!(/
777
+ ^
778
+ (
779
+ (#{SPF::Mod::NAME_PATTERN}) =
780
+ [^\x20]*
781
+ )
782
+ (?: \x20+ | $ )
783
+ /x, '') and $&
784
+ )
785
+ # Looks like a modifier:
786
+ mod_text = $1
787
+ mod_name = $2.downcase
788
+ mod_class = MOD_CLASSES[mod_name]
789
+ if mod_class
790
+ # Known modifier.
791
+ mod = mod_class.new_from_string(mod_text)
792
+ if mod.is_a?(SPF::GlobalMod)
793
+ # Global modifier.
794
+ unless @global_mods[mod_name]
795
+ raise SPF::DuplicateGlobalMod.new("Duplicate global modifier '#{mod_name}' encountered")
796
+ end
797
+ @global_mods[mod_name] = mod
798
+ elsif mod.is_a?(SPF::PositionalMod)
799
+ # Positional modifier, queue normally:
800
+ @terms << mod
801
+ end
802
+ end
803
+ else
804
+ raise SPF::JunkInRecordError.new("Junk encountered in record '#{@text}'")
805
+ end
806
+ end
807
+
808
+ def global_mods
809
+ return @global_mods.values.sort {|a,b| a.precedence <=> b.precedence }
810
+ end
811
+
812
+ def global_mod(mod_name)
813
+ return @global_mods[mod_name]
814
+ end
815
+
816
+ def to_s
817
+ return [version_tag, @terms, @global_mods].join(' ')
818
+ end
819
+
820
+ def eval(server, request)
821
+ raise SPF::OptionRequiredError.new('SPF server object required for record evaluation') unless server
822
+ raise SPF::OptionRequiredError.new('Request object required for record evaluation') unless request
823
+ begin
824
+ @terms.each do |term|
825
+ if term.is_a?(SPF::Mech)
826
+ # Term is a mechanism.
827
+ mech = term
828
+ if mech.match(server, request)
829
+ result_name = RESULTS_BY_QUALIFIER[mech.qualifier]
830
+ result_class = server.result_class(result_name)
831
+ result = result_class.new([server, request, "Mechanism '#{term}' matched"])
832
+ mech.explain(server, request, result)
833
+ raise result
834
+ end
835
+ elsif term.is_a?(SPF::PositionalMod)
836
+ # Term is a positional modifier.
837
+ mod = term
838
+ mod.process(server, request)
839
+ elsif term.is_a?(SPF::UnknownMod)
840
+ # Term is an unknown modifier. Ignore it (RFC 4408, 6/3).
841
+ else
842
+ # Invalid term object encountered:
843
+ raise SPF::UnexpectedTermObjectError.new(
844
+ "Unexpected term object '#{term}' encountered.")
845
+ end
846
+ end
847
+ rescue SPF::Result => result
848
+ # Process global modifiers in ascending order of precedence:
849
+ @global_mods.each do |global_mod|
850
+ global_mod.process(server, request, result)
851
+ end
852
+ raise result
853
+ end
854
+ end
855
+
856
+ class SPF::Record::V1 < SPF::Record
857
+
858
+ MECH_CLASSES = {
859
+ :all => SPF::Mech::All,
860
+ :ip4 => SPF::Mech::IP4,
861
+ :ip6 => SPF::Mech::IP6,
862
+ :a => SPF::Mech::A,
863
+ :mx => SPF::Mech::MX,
864
+ :ptr => SPF::Mech::PTR,
865
+ :exists => SPF::Mech::Exists,
866
+ :include => SPF::Mech::Include
867
+ }
868
+
869
+ MOD_CLASSES = {
870
+ :redirect => SPF::Mod::Redirect,
871
+ :exp => SPF::Mod::Exp
872
+ }
873
+
874
+
875
+ def scopes
876
+ [:helo, :mfrom]
877
+ end
878
+
879
+ def version_tag
880
+ 'v=spf1'
881
+ end
882
+
883
+ def version_tag_pattern
884
+ " v=spf(1) (?= \\x20+ | $ ) "
885
+ end
886
+
887
+ def mech_classes
888
+ MECH_CLASSES
889
+ end
890
+
891
+ def initialize(options = {})
892
+ super(options)
893
+
894
+ @scopes ||= options[:scopes]
895
+ if @scopes and scopes.any?
896
+ unless @scopes.length > 0
897
+ raise SPF::InvalidScopeError.new('No scopes for v=spf1 record')
898
+ end
899
+ if @scopes.length == 2
900
+ unless (
901
+ @scopes[0] == :helo and @scopes[1] == :mfrom or
902
+ @scopes[0] == :mfrom and @scopes[1] == :helo)
903
+ raise SPF::InvalidScope.new(
904
+ "Invalid set of scopes " + @scopes.map{|x| "'#{x}'"}.join(', ') + "for v=spf1 record")
905
+ end
906
+ end
907
+ end
908
+ end
909
+ end
910
+
911
+ class SPF::Record::V2 < SPF::Record
912
+
913
+ MECH_CLASSES = {
914
+ :all => SPF::Mech::All,
915
+ :ip4 => SPF::Mech::IP4,
916
+ :ip6 => SPF::Mech::IP6,
917
+ :a => SPF::Mech::A,
918
+ :mx => SPF::Mech::MX,
919
+ :ptr => SPF::Mech::PTR,
920
+ :exists => SPF::Mech::Exists,
921
+ :include => SPF::Mech::Include
922
+ }
923
+
924
+ MOD_CLASSES = {
925
+ :redirect => SPF::Mod::Redirect,
926
+ :exp => SPF::Mod::Exp
927
+ }
928
+
929
+ VALID_SCOPE = /^(?: mfrom | pra )$/x
930
+ def version_tag
931
+ 'v=spf2.0'
932
+ end
933
+
934
+ def version_tag_pattern
935
+ "
936
+ spf(2\.0)
937
+ \/
938
+ ( (?: mfrom | pra ) (?: , (?: mfrom | pra ) )* )
939
+ (?= \\x20 | $ )
940
+ "
941
+ end
942
+
943
+ def mech_classes
944
+ MECH_CLASSES
945
+ end
946
+
947
+ def initialize(options = {})
948
+ super(options)
949
+ unless @parse_text
950
+ scopes = @scopes || {}
951
+ raise SPF::InvalidScopeError.new('No scopes for spf2.0 record') if scopes.empty?
952
+ scopes.each do |scope|
953
+ if scope !~ VALID_SCOPE
954
+ raise SPF::InvalidScopeError.new("Invalid scope '#{scope}' for spf2.0 record")
955
+ end
956
+ end
957
+ end
958
+ end
959
+
960
+ def version_tag
961
+ return 'spf2.0' if not @scopes # no scopes parsed
962
+ return 'spf2.0/' + @scopes.join(',')
963
+ end
964
+
965
+ def parse_version_tag
966
+
967
+ @parse_text.sub!(/#{version_tag_pattern}(?:\x20+|$)/ix, '')
968
+ if $1
969
+ scopes = @scopes = "#{$2}".split(/,/)
970
+ if scopes.empty?
971
+ raise SPF::InvalidScopeError.new('No scopes for spf2.0 record')
972
+ end
973
+ scopes.each do |scope|
974
+ if scope !~ VALID_SCOPE
975
+ raise SPF::InvalidScopeError.new("Invalid scope '#{scope}' for spf2.0 record")
976
+ end
977
+ end
978
+ else
979
+ raise SPF::InvalidRecordVersionError.new(
980
+ "Not a 'spf2.0' record: '#{@text}'")
981
+ end
982
+ end
983
+ end
984
+ end
985
+