File Coverage

blib/lib/Mail/SPF/Mech.pm
Criterion Covered Total %
statement 52 119 43.7
branch 0 48 0.0
condition 0 9 0.0
subroutine 17 32 53.1
pod 6 12 50.0
total 75 220 34.0


line stmt bran cond sub pod time code
1             #
2             # Mail::SPF::Mech
3             # SPF record mechanism class.
4             #
5             # (C) 2005-2012 Julian Mehnle
6             # 2005 Shevek
7             # $Id: Mech.pm 57 2012-01-30 08:15:31Z julian $
8             #
9             ##############################################################################
10              
11             package Mail::SPF::Mech;
12              
13             =head1 NAME
14              
15             Mail::SPF::Mech - SPF record mechanism base class
16              
17             =head1 VERSION
18              
19             version 3.20250505
20              
21             =cut
22              
23 1     1   1240 use warnings;
  1         2  
  1         85  
24 1     1   6 use strict;
  1         1  
  1         21  
25              
26 1     1   4 use utf8; # Hack to keep Perl 5.6 from whining about /[\p{}]/.
  1         1  
  1         6  
27              
28 1     1   23 use base 'Mail::SPF::Term';
  1         1  
  1         148  
29              
30 1     1   5 use Error ':try';
  1         1  
  1         8  
31 1     1   140 use NetAddr::IP;
  1         1  
  1         6  
32              
33 1     1   494 use Mail::SPF::Record;
  1         3  
  1         30  
34 1     1   646 use Mail::SPF::MacroString;
  1         5  
  1         51  
35 1     1   7 use Mail::SPF::Util;
  1         2  
  1         24  
36              
37 1     1   4 use constant TRUE => (0 == 0);
  1         2  
  1         71  
38 1     1   5 use constant FALSE => not TRUE;
  1         2  
  1         38  
39              
40 1     1   3 use constant default_qualifier => Mail::SPF::Record->default_qualifier;
  1         1  
  1         81  
41 1     1   3 use constant default_ipv4_prefix_length => 32;
  1         28  
  1         33  
42 1     1   4 use constant default_ipv6_prefix_length => 128;
  1         1  
  1         57  
43              
44 1     1   3 use constant qualifier_pattern => qr/[+\-~?]/;
  1         2  
  1         61  
45 1     1   3 use constant name_pattern => qr/ ${\__PACKAGE__->SUPER::name_pattern} (?= [:\/\x20] | $ ) /x;
  1         1  
  1         2  
  1         198  
46              
47 1         1410 use constant explanation_templates_by_result_code => {
48             pass => "Sender is authorized to use '%{s}' in '%{_scope}' identity",
49             fail => "Sender is not authorized to use '%{s}' in '%{_scope}' identity",
50             softfail => "Sender is not authorized to use '%{s}' in '%{_scope}' identity, however domain is not currently prepared for false failures",
51             neutral => "Domain does not state whether sender is authorized to use '%{s}' in '%{_scope}' identity"
52 1     1   6 };
  1         2  
53              
54             =head1 DESCRIPTION
55              
56             An object of class B represents a mechanism within an SPF
57             record. Mail::SPF::Mech cannot be instantiated directly. Create an instance
58             of a concrete sub-class instead.
59              
60             =head2 Constructors
61              
62             The following constructors are provided:
63              
64             =over
65              
66             =item B: returns I
67              
68             I. Creates a new SPF record mechanism object.
69              
70             %options is a list of key/value pairs representing any of the following
71             options:
72              
73             =over
74              
75             =item B
76              
77             A I denoting the unparsed text of the mechanism.
78              
79             =item B
80              
81             A single-character I denoting the qualifier of the mechanism. Any of
82             the following may be specified: B<'+'> (C), B<'-'> (C),
83             B<'~'> (C), B<'?'> (C). See RFC 4408, 4.6.2 and 2.5, for
84             their meanings. Defaults to B<'+'>.
85              
86             =item B
87              
88             A I denoting the name of the mechanism. I if a generic
89             I object (as opposed to a specific sub-class) is being
90             constructed.
91              
92             =item B
93              
94             A I object denoting an optional IP address network parameter of
95             the mechanism. Can be either an IPv4 or an IPv6 address, with an optional
96             network prefix length. IPv4-mapped IPv6 addresses (e.g. '::ffff:192.168.0.1')
97             must I be specified directly, but as plain IPv4 addresses.
98              
99             =item B
100              
101             Either a plain I or a I object denoting an
102             optional C parameter of the mechanism.
103              
104             =item B
105              
106             =item B
107              
108             A I denoting an optional IPv4 or IPv6 network prefix length for the
109             C of the mechanism. Note that these options do not apply to the
110             C option, which already includes an optional network prefix
111             length.
112              
113             =back
114              
115             Other options may be specified by sub-classes of Mail::SPF::Mech.
116              
117             =cut
118              
119             sub new {
120 0     0 1   my ($self, %options) = @_;
121 0 0         $self->class ne __PACKAGE__
122             or throw Mail::SPF::EAbstractClass;
123 0           $self = $self->SUPER::new(%options);
124 0 0         $self->{parse_text} = $self->{text} if not defined($self->{parse_text});
125             $self->{domain_spec} = Mail::SPF::MacroString->new(text => $self->{domain_spec})
126             if defined($self->{domain_spec})
127 0 0 0       and not UNIVERSAL::isa($self->{domain_spec}, 'Mail::SPF::MacroString');
128 0           return $self;
129             }
130              
131             =item B: returns I;
132             throws I, I
133              
134             I. Creates a new SPF record mechanism object by parsing the string and
135             any options given.
136              
137             =back
138              
139             =head2 Class methods
140              
141             The following class methods are provided:
142              
143             =over
144              
145             =item B: returns I
146              
147             Returns the default qualifier, i.e. B<'+'>.
148              
149             =item B: returns I
150              
151             Returns the default IPv4 network prefix length, i.e. B<32>.
152              
153             =item B: returns I
154              
155             Returns the default IPv6 network prefix length, i.e. B<128>.
156              
157             =item B: returns I
158              
159             Returns a regular expression that matches any legal mechanism qualifier, i.e. B<'+'>,
160             B<'-'>, B<'~'>, or B<'?'>.
161              
162             =item B: returns I
163              
164             I. Returns the name of the mechanism.
165              
166             This method is abstract and must be implemented by sub-classes of
167             Mail::SPF::Mech.
168              
169             =item B: returns I
170              
171             Returns a regular expression that matches any legal mechanism name.
172              
173             =back
174              
175             =head2 Instance methods
176              
177             The following instance methods are provided:
178              
179             =over
180              
181             =cut
182              
183             sub parse {
184 0     0 0   my ($self) = @_;
185             defined($self->{parse_text})
186 0 0         or throw Mail::SPF::ENothingToParse('Nothing to parse for mechanism');
187 0           $self->parse_qualifier();
188 0           $self->parse_name();
189 0           $self->parse_params();
190 0           $self->parse_end();
191 0           return;
192             }
193              
194             sub parse_qualifier {
195 0     0 0   my ($self) = @_;
196 0 0         if ($self->{parse_text} =~ s/^(${\$self->qualifier_pattern})?//) {
  0            
197 0   0       $self->{qualifier} = $1 || $self->default_qualifier;
198             }
199             else {
200 0           throw Mail::SPF::EInvalidMechQualifier(
201             "Invalid qualifier encountered in '" . $self->text . "'");
202             }
203 0           return;
204             }
205              
206             sub parse_name {
207 0     0 0   my ($self) = @_;
208 0 0         if ($self->{parse_text} =~ s/^ (${\$self->name_pattern}) (?: : (?=.) )? //x) {
  0            
209 0           $self->{name} = $1;
210             }
211             else {
212 0           throw Mail::SPF::EInvalidMech(
213             "Unexpected mechanism name encountered in '" . $self->text . "'");
214             }
215 0           return;
216             }
217              
218             sub parse_params {
219 0     0 0   my ($self) = @_;
220             # Parse generic string of parameters text (should be overridden in sub-classes):
221 0 0         if ($self->{parse_text} =~ s/^(.*)//) {
222 0           $self->{params_text} = $1;
223             }
224 0           return;
225             }
226              
227             sub parse_end {
228 0     0 0   my ($self) = @_;
229 0 0         $self->{parse_text} eq ''
230             or throw Mail::SPF::EJunkInTerm("Junk encountered in mechanism '" . $self->text . "'");
231 0           delete($self->{parse_text});
232 0           return;
233             }
234              
235             =item B: returns I; throws I
236              
237             Returns the unparsed text of the mechanism. Throws a
238             I exception if the mechanism was created
239             synthetically instead of being parsed, and no text was provided.
240              
241             =item B: returns I
242              
243             Returns the qualifier of the mechanism. See the description of the C
244             constructor's C option.
245              
246             =cut
247              
248             sub qualifier {
249 0     0 1   my ($self) = @_;
250             # Read-only!
251 0   0       return $self->{qualifier} || $self->default_qualifier;
252             }
253              
254             =item B: returns I
255              
256             I. Returns the mechanism's parameters formatted as a string.
257              
258             A sub-class of Mail::SPF::Mech does not have to implement this method if it
259             supports no parameters.
260              
261             =item B: returns I
262              
263             Formats the mechanism's qualifier, name, and parameters as a string and returns
264             it. (A qualifier that matches the default of B<'+'> is omitted.) You can
265             simply use a Mail::SPF::Mech object as a string for the same effect, see
266             L<"OVERLOADING">.
267              
268             =cut
269              
270             sub stringify {
271 0     0 0   my ($self) = @_;
272 0 0         my $params = $self->can('params') ? $self->params : undef;
273 0 0         return sprintf(
    0          
274             '%s%s%s',
275             $self->qualifier eq $self->default_qualifier ? '' : $self->qualifier,
276             $self->name,
277             defined($params) ? $params : ''
278             );
279             }
280              
281             =item B: returns I
282              
283             Returns the target domain of the mechanism. Depending on whether the mechanism
284             does have an explicit C parameter, this is either the
285             macro-expanded C parameter, or the request's authority domain
286             (see L) otherwise. Both a
287             I and a I object are required for
288             resolving the target domain.
289              
290             =cut
291              
292             sub domain {
293 0     0 1   my ($self, $server, $request) = @_;
294 0 0         defined($server)
295             or throw Mail::SPF::EOptionRequired('Mail::SPF server object required for target domain resolution');
296 0 0         defined($request)
297             or throw Mail::SPF::EOptionRequired('Request object required for target domain resolution');
298             return $self->{domain_spec}->new(server => $server, request => $request)
299 0 0         if defined($self->{domain_spec});
300 0           return $request->authority_domain;
301             }
302              
303             =item B: returns I; throws I
304              
305             I. Checks whether the mechanism matches the parameters of the given
306             request (see L) and returns B if it does, or B
307             otherwise. In any case, takes both a I and a
308             I object.
309              
310             This method is abstract and must be implemented by sub-classes of
311             Mail::SPF::Mech.
312              
313             =item B: returns I;
314             throws I
315              
316             =item B: returns I;
317             throws I
318              
319             Checks whether the mechanism's target domain name (that is, any of its DNS C
320             or C records) matches the given request's IP address (see
321             L), and returns B if it does, or B
322             otherwise. If an explicit domain is specified, it is used instead of the
323             mechanism's target domain. The mechanism's IP network prefix lengths are
324             respected when matching DNS address records against the request's IP address.
325             See RFC 4408, 5, for the exact algorithm used.
326              
327             This method exists mainly for the convenience of sub-classes of
328             Mail::SPF::Mech.
329              
330             =cut
331              
332             sub match_in_domain {
333 0     0 1   my ($self, $server, $request, $domain) = @_;
334              
335 0 0         $domain = $self->domain($server, $request)
336             if not defined($domain);
337              
338 0           my $ipv4_prefix_length = $self->ipv4_prefix_length;
339 0           my $ipv6_prefix_length = $self->ipv6_prefix_length;
340 0 0         my $addr_rr_type = $request->ip_address->version == 4 ? 'A' : 'AAAA';
341              
342 0           my $packet = $server->dns_lookup($domain, $addr_rr_type);
343 0 0         my @rrs = $packet->answer
344             or $server->count_void_dns_lookup($request);
345              
346 0           foreach my $rr (@rrs) {
347 0 0         if ($rr->type eq 'A') {
    0          
    0          
348 0           my $network = NetAddr::IP->new($rr->address, $ipv4_prefix_length);
349 0 0         return TRUE
350             if $network->contains($request->ip_address);
351             }
352             elsif ($rr->type eq 'AAAA') {
353 0           my $network = NetAddr::IP->new($rr->address, $ipv6_prefix_length);
354 0 0         return TRUE
355             if $network->contains($request->ip_address_v6);
356             }
357             elsif ($rr->type eq 'CNAME') {
358             # Ignore -- we should have gotten the A/AAAA records anyway.
359             }
360             else {
361             # Unexpected RR type.
362             # TODO Generate debug info or ignore silently.
363             }
364             }
365 0           return FALSE;
366             }
367              
368             =item B
369              
370             Locally generates an explanation for why the mechanism caused the given result,
371             and stores it in the given request object's state.
372              
373             There is no need to override this method in sub-classes. See the
374             L method.
375              
376             =cut
377              
378             sub explain {
379 0     0 1   my ($self, $server, $request, $result) = @_;
380 0           my $explanation_template = $self->explanation_template($server, $request, $result);
381             return
382 0 0         if not defined($explanation_template);
383             try {
384 0     0     my $explanation = Mail::SPF::MacroString->new(
385             text => $explanation_template,
386             server => $server,
387             request => $request,
388             is_explanation => TRUE
389             );
390 0           $request->state('local_explanation', $explanation);
391             }
392       0     catch Mail::SPF::Exception with {}
393 0     0     catch Mail::SPF::Result with {};
394 0           return;
395             }
396              
397             =item B: returns I
398              
399             Returns a macro string template for a locally generated explanation for why the
400             mechanism caused the given result object.
401              
402             Sub-classes should either define an C
403             hash constant with their own templates, or override this method.
404              
405             =cut
406              
407             sub explanation_template {
408 0     0 1   my ($self, $server, $request, $result) = @_;
409             return undef
410 0 0         if not $self->can('explanation_templates_by_result_code');
411 0           return $self->explanation_templates_by_result_code->{$result->code};
412             }
413              
414             =back
415              
416             =head1 OVERLOADING
417              
418             If a Mail::SPF::Mech object is used as a I, the C method is
419             used to convert the object into a string.
420              
421             =head1 SEE ALSO
422              
423             L,
424             L,
425             L,
426             L,
427             L,
428             L,
429             L,
430             L
431              
432             L, L, L
433              
434             L
435              
436             For availability, support, and license information, see the README file
437             included with Mail::SPF.
438              
439             =head1 AUTHORS
440              
441             Julian Mehnle , Shevek
442              
443             =cut
444              
445             TRUE;