File Coverage

blib/lib/Mail/SPF/Record.pm
Criterion Covered Total %
statement 30 118 25.4
branch 0 38 0.0
condition 0 4 0.0
subroutine 10 25 40.0
pod 8 13 61.5
total 48 198 24.2


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