spf 0.0.0

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