File Coverage

blib/lib/Mail/SPF/Record.pm
Criterion Covered Total %
statement 30 136 22.0
branch 0 50 0.0
condition 0 4 0.0
subroutine 10 25 40.0
pod 8 13 61.5
total 48 228 21.0


line stmt bran cond sub pod time code
1             #
2             # Mail::SPF::Record
3             # Abstract base class for SPF records.
4             #
5             # (C) 2005-2012 Julian Mehnle
6             # 2005 Shevek
7             # $Id: Record.pm 57 2012-01-30 08:15:31Z julian $
8             #
9             ##############################################################################
10              
11             package Mail::SPF::Record;
12              
13             =head1 NAME
14              
15             Mail::SPF::Record - Abstract base class for SPF records
16              
17             =head1 VERSION
18              
19             version 3.20250505
20              
21             =cut
22              
23 3     3   21 use warnings;
  3         8  
  3         373  
24 3     3   20 use strict;
  3         5  
  3         77  
25              
26 3     3   12 use utf8; # Hack to keep Perl 5.6 from whining about /[\p{}]/.
  3         7  
  3         20  
27              
28 3     3   112 use base 'Mail::SPF::Base';
  3         6  
  3         560  
29              
30             use overload
31 3         26 '""' => 'stringify',
32 3     3   23 fallback => 1;
  3         5  
33              
34 3     3   317 use Error ':try';
  3         6  
  3         51  
35              
36 3     3   548 use constant TRUE => (0 == 0);
  3         6  
  3         298  
37 3     3   19 use constant FALSE => not TRUE;
  3         6  
  3         217  
38              
39 3     3   35 use constant default_qualifier => '+';
  3         12  
  3         256  
40              
41 3         6130 use constant results_by_qualifier => {
42             '' => 'pass',
43             '+' => 'pass',
44             '-' => 'fail',
45             '~' => 'softfail',
46             '?' => 'neutral'
47 3     3   19 };
  3         7  
48              
49             # Interface:
50             ##############################################################################
51              
52             =head1 SYNOPSIS
53              
54             =head2 Creating a record from a string
55              
56             use Mail::SPF::v1::Record;
57              
58             my $record = Mail::SPF::v1::Record->new_from_string("v=spf1 a mx -all");
59              
60             =head2 Creating a record synthetically
61              
62             use Mail::SPF::v2::Record;
63              
64             my $record = Mail::SPF::v2::Record->new(
65             scopes => ['mfrom', 'pra'],
66             terms => [
67             Mail::SPF::Mech::A->new(),
68             Mail::SPF::Mech::MX->new(),
69             Mail::SPF::Mech::All->new(qualifier => '-')
70             ],
71             global_mods => [
72             Mail::SPF::Mod::Exp->new(domain_spec => 'spf-exp.example.com')
73             ]
74             );
75              
76             =cut
77              
78             # Implementation:
79             ##############################################################################
80              
81             =head1 DESCRIPTION
82              
83             B is an abstract base class for SPF records. It cannot be
84             instantiated directly. Create an instance of a concrete sub-class instead.
85              
86             =head2 Constructor
87              
88             The following constructors are provided:
89              
90             =over
91              
92             =item B: returns I
93              
94             Creates a new SPF record object.
95              
96             %options is a list of key/value pairs representing any of the following
97             options:
98              
99             =over
100              
101             =item B
102              
103             A I denoting the unparsed text of the record.
104              
105             =item B
106              
107             A reference to an I of Is denoting the scopes that are covered
108             by the record (see the description of the C option of
109             L constructor|Mail::SPF::Request/new>).
110              
111             =item B
112              
113             A reference to an I of I (i.e. I or
114             I) objects that make up the record. I
115             objects must not be included here, but should be specified using the
116             C option instead.
117              
118             =item B
119              
120             A reference to an I of I objects that are global
121             modifiers of the record.
122              
123             =back
124              
125             =cut
126              
127             sub new {
128 0     0 1   my ($self, %options) = @_;
129 0 0         $self->class ne __PACKAGE__
130             or throw Mail::SPF::EAbstractClass;
131 0           $self = $self->SUPER::new(%options);
132 0 0         $self->{parse_text} = $self->{text} if not defined($self->{parse_text});
133 0   0       $self->{terms} ||= [];
134 0   0       $self->{global_mods} ||= {};
135 0           return $self;
136             }
137              
138             =item B: returns I;
139             throws I, I,
140             I
141              
142             Creates a new SPF record object by parsing the string and any options given.
143              
144             =cut
145              
146             sub new_from_string {
147 0     0 1   my ($self, $text, %options) = @_;
148 0           $self = $self->new(%options, text => $text);
149 0           $self->parse();
150 0           return $self;
151             }
152              
153             =back
154              
155             =head2 Class methods
156              
157             The following class methods are provided:
158              
159             =over
160              
161             =item B: returns I
162              
163             I. Returns a regular expression that matches a legal version tag.
164              
165             This method is abstract and must be implemented by sub-classes of
166             Mail::SPF::Record.
167              
168             =item B: returns I
169              
170             Returns the default qualifier, i.e. B<'+'>.
171              
172             =item B: returns I of I
173              
174             Returns a reference to a hash that maps qualifiers to result codes as follows:
175              
176             Qualifier | Result code
177             -----------+-------------
178             + | pass
179             - | fail
180             ~ | softfail
181             ? | neutral
182              
183             =back
184              
185             =head2 Instance methods
186              
187             The following instance methods are provided:
188              
189             =over
190              
191             =cut
192              
193             sub parse {
194 0     0 0   my ($self) = @_;
195             defined($self->{parse_text})
196 0 0         or throw Mail::SPF::ENothingToParse('Nothing to parse for record');
197 0           $self->parse_version_tag();
198 0           $self->parse_term() while length($self->{parse_text});
199 0           $self->parse_end();
200 0           return;
201             }
202              
203             sub parse_version_tag {
204 0     0 0   my ($self) = @_;
205 0 0         if (not $self->{parse_text} =~ s/^${\$self->version_tag_pattern}(?:\x20+|$)//) {
  0            
206 0           throw Mail::SPF::EInvalidRecordVersion(
207             "Not a '" . $self->version_tag . "' record: '" . $self->text . "'");
208             }
209             }
210              
211             sub parse_term {
212 0     0 0   my ($self) = @_;
213              
214 0           eval {
215 0           require Mail::SPF::Mech;
216             };
217 0 0         if($@) {
218 0           throw Mail::SPF::ENothingToParse('Cannot load Mail::SPF::Mech class');
219             }
220 0 0         if (
    0          
221             $self->{parse_text} =~ s/
222             ^
223             (
224 0           ${\Mail::SPF::Mech->qualifier_pattern}?
225 0           (${\Mail::SPF::Mech->name_pattern})
226             [^\x20]*
227             )
228             (?: \x20+ | $ )
229             //x
230             ) {
231             # Looks like a mechanism:
232 0           my ($mech_text, $mech_name) = ($1, lc($2));
233 0           my $mech_class = $self->mech_classes->{$mech_name};
234 0 0         throw Mail::SPF::EInvalidMech("Unknown mechanism type '$mech_name' in '" . $self->version_tag . "' record")
235             if not defined($mech_class);
236 0           my $mech = $mech_class->new_from_string($mech_text);
237 0           push(@{$self->{terms}}, $mech);
  0            
238             }
239             elsif (
240             $self->{parse_text} =~ s/
241             ^
242             (
243 0           (${\Mail::SPF::Mod->name_pattern}) =
244             [^\x20]*
245             )
246             (?: \x20+ | $ )
247             //x
248             ) {
249             # Looks like a modifier:
250 0           my ($mod_text, $mod_name) = ($1, lc($2));
251 0           my $mod_class = $self->mod_classes->{$mod_name};
252 0 0         if (defined($mod_class)) {
253             # Known modifier.
254 0           my $mod = $mod_class->new_from_string($mod_text);
255 0 0         if ($mod->isa('Mail::SPF::GlobalMod')) {
    0          
256             # Global modifier.
257 0 0         not defined($self->{global_mods}->{$mod_name}) or
258             throw Mail::SPF::EDuplicateGlobalMod("Duplicate global modifier '$mod_name' encountered");
259 0           $self->{global_mods}->{$mod_name} = $mod;
260             }
261             elsif ($mod->isa('Mail::SPF::PositionalMod')) {
262             # Positional modifier, queue normally:
263 0           push(@{$self->{terms}}, $mod);
  0            
264             }
265             else {
266             # Huh? This should not happen.
267             }
268             }
269             else {
270             # Unknown modifier.
271 0           my $mod = Mail::SPF::UnknownMod->new_from_string($mod_text);
272 0           push(@{$self->{terms}}, $mod);
  0            
273             }
274             }
275             else {
276 0           throw Mail::SPF::EJunkInRecord("Junk encountered in record '" . $self->text . "'");
277             }
278 0           return;
279             }
280              
281             sub parse_end {
282 0     0 0   my ($self) = @_;
283             throw Mail::SPF::EJunkInRecord("Junk encountered in record '" . $self->text . "'")
284 0 0         if $self->{parse_text} ne '';
285 0           delete($self->{parse_text});
286 0           return;
287             }
288              
289             =item B: returns I; throws I
290              
291             Returns the unparsed text of the record. Throws a I
292             exception if the record was created synthetically instead of being parsed, and
293             no text was provided.
294              
295             =cut
296              
297             sub text {
298 0     0 1   my ($self) = @_;
299             defined($self->{text})
300 0 0         or throw Mail::SPF::ENoUnparsedText;
301 0           return $self->{text};
302             }
303              
304             =item B: returns I
305              
306             I. Returns the version tag of the record.
307              
308             This method is abstract and must be implemented by sub-classes of
309             Mail::SPF::Record.
310              
311             =item B: returns I of I
312              
313             Returns a list of the scopes that are covered by the record. See the
314             description of the L constructor's C option.
315              
316             =cut
317              
318             sub scopes {
319 0     0 1   my ($self) = @_;
320 0           return @{$self->{scopes}};
  0            
321             }
322              
323             =item B: returns I of I
324              
325             Returns a list of the terms that make up the record, excluding any global
326             modifiers, which are returned by the C method. See the
327             description of the L constructor's C option.
328              
329             =cut
330              
331             sub terms {
332 0     0 1   my ($self) = @_;
333 0           return @{$self->{terms}};
  0            
334             }
335              
336             =item B: returns I of I
337              
338             Returns a list of the global modifiers of the record, ordered ascending by
339             modifier precedence. See the description of the L constructor's
340             C option.
341              
342             =cut
343              
344             sub global_mods {
345 0     0 1   my ($self) = @_;
346 0           return sort { $a->precedence <=> $b->precedence } values(%{$self->{global_mods}});
  0            
  0            
347             }
348              
349             =item B: returns I
350              
351             Returns the global modifier of the given name if it is present in the record.
352             Returns B otherwise. Use this method if you wish to retrieve a specific
353             global modifier as opposed to getting all of them.
354              
355             =cut
356              
357             sub global_mod {
358 0     0 1   my ($self, $mod_name) = @_;
359 0           return $self->{global_mods}->{$mod_name};
360             }
361              
362             =item B: returns I
363              
364             Returns the record's version tag and terms (including the global modifiers)
365             formatted as a string. You can simply use a Mail::SPF::Record object as a
366             string for the same effect, see L<"OVERLOADING">.
367              
368             =cut
369              
370             sub stringify {
371 0     0 0   my ($self) = @_;
372 0           return join(' ', $self->version_tag, $self->terms, $self->global_mods);
373             }
374              
375             =item B: throws I
376              
377             Evaluates the SPF record in the context of the request parameters represented
378             by the given I object. The given I
379             object is used for performing DNS look-ups. Throws a I
380             object matching the outcome of the evaluation; see L. See
381             RFC 4408, 4.6 and 4.7, for the exact algorithm used.
382              
383             =cut
384              
385             sub eval {
386 0     0 1   my ($self, $server, $request) = @_;
387              
388 0 0         defined($server)
389             or throw Mail::SPF::EOptionRequired('Mail::SPF server object required for record evaluation');
390 0 0         defined($request)
391             or throw Mail::SPF::EOptionRequired('Request object required for record evaluation');
392              
393             try {
394 0     0     my @include_domains;
395             my %inc_domain_cname;
396 0           foreach my $term ($self->terms) {
397 0 0         if ($term->isa('Mail::SPF::Mech')) {
    0          
    0          
398             # Term is a mechanism.
399 0           my $mech = $term;
400 0 0         if($term->name eq "include") {
401 0           push(@include_domains, $term->{domain_spec}->{text});
402 0 0         if(scalar @include_domains > 1) {
403 0           foreach my $dom ( @include_domains ) {
404 0           my $packet;
405 0 0         if(not exists $inc_domain_cname{$dom}) {
406 0           $packet = $server->dns_lookup($dom, 'CNAME');
407 0           $inc_domain_cname{$dom} = $packet->{answer}[0]->{cname}->{name};
408             }
409 0 0         if(defined $inc_domain_cname{$dom}) {
410 0 0         if(grep /^$inc_domain_cname{$dom}$/, @include_domains) {
411 0           $server->throw_result('fail', $request,
412             'referencing the same TXT record through multiple CNAME aliases is not permitted');
413             }
414             }
415             }
416             }
417             }
418 0 0         if ($mech->match($server, $request)) {
419 0           my $result_name = $self->results_by_qualifier->{$mech->qualifier};
420 0           my $result_class = $server->result_class($result_name);
421 0           my $result = $result_class->new($server, $request, "Mechanism '$term' matched");
422 0           $mech->explain($server, $request, $result);
423 0           $result->throw();
424             }
425             }
426             elsif ($term->isa('Mail::SPF::PositionalMod')) {
427             # Term is a positional modifier.
428 0           my $mod = $term;
429 0           $mod->process($server, $request);
430             }
431             elsif ($term->isa('Mail::SPF::UnknownMod')) {
432             # Term is an unknown modifier. Ignore it (RFC 4408, 6/3).
433             }
434             else {
435             # Invalid term object encountered:
436 0           throw Mail::SPF::EUnexpectedTermObject(
437             "Unexpected term object '$term' encountered");
438             }
439             }
440 0           undef @include_domains;
441 0           undef %inc_domain_cname;
442              
443             # Default result when "falling off" the end of the record (RFC 4408, 4.7/1):
444 0           $server->throw_result('neutral-by-default', $request,
445             'Default neutral result due to no mechanism matches');
446             }
447             catch Mail::SPF::Result with {
448 0     0     my ($result) = @_;
449              
450             # Process global modifiers in ascending order of precedence:
451 0           foreach my $global_mod ($self->global_mods) {
452 0           $global_mod->process($server, $request, $result);
453             }
454              
455 0           $result->throw();
456 0           };
457             }
458              
459             =back
460              
461             =head1 OVERLOADING
462              
463             If a Mail::SPF::Record object is used as a I, the C method
464             is used to convert the object into a string.
465              
466             =head1 SEE ALSO
467              
468             L, L, L,
469             L, L, L
470              
471             L
472              
473             For availability, support, and license information, see the README file
474             included with Mail::SPF.
475              
476             =head1 AUTHORS
477              
478             Julian Mehnle , Shevek
479              
480             =cut
481              
482             TRUE;