File Coverage

blib/lib/Mail/SPF/MacroString.pm
Criterion Covered Total %
statement 100 129 77.5
branch 39 88 44.3
condition 8 18 44.4
subroutine 19 21 90.4
pod 3 4 75.0
total 169 260 65.0


line stmt bran cond sub pod time code
1             #
2             # Mail::SPF::MacroString
3             # SPF record macro string class.
4             #
5             # (C) 2005-2012 Julian Mehnle
6             # 2005 Shevek
7             # $Id: MacroString.pm 57 2012-01-30 08:15:31Z julian $
8             #
9             ##############################################################################
10              
11             package Mail::SPF::MacroString;
12              
13             =head1 NAME
14              
15             Mail::SPF::MacroString - SPF record macro string class
16              
17             =head1 VERSION
18              
19             version 3.20250505
20              
21             =cut
22              
23 3     3   20 use warnings;
  3         6  
  3         177  
24 3     3   17 use strict;
  3         8  
  3         91  
25              
26 3     3   1177 use utf8; # Hack to keep Perl 5.6 from whining about /[\p{}]/.
  3         703  
  3         19  
27              
28 3     3   132 use base 'Mail::SPF::Base';
  3         7  
  3         426  
29              
30             use overload
31 3         36 '""' => 'stringify',
32 3     3   21 fallback => 1;
  3         6  
33              
34 3     3   324 use Error ':try';
  3         7  
  3         18  
35 3     3   2173 use URI::Escape 1.13 ();
  3         6813  
  3         116  
36              
37 3     3   1729 use Mail::SPF::Util;
  3         51  
  3         231  
38              
39 3     3   23 use constant TRUE => (0 == 0);
  3         6  
  3         235  
40 3     3   20 use constant FALSE => not TRUE;
  3         7  
  3         152  
41              
42 3     3   85 use constant default_split_delimiters => '.';
  3         7  
  3         154  
43 3     3   15 use constant default_join_delimiter => '.';
  3         5  
  3         131  
44              
45 3     3   14 use constant uri_unreserved_chars => 'A-Za-z0-9\-._~';
  3         91  
  3         207  
46             # "unreserved" characters according to RFC 3986 -- not the "uric" chars!
47             # This deliberately deviates from what RFC 4408 says. This is a bug in
48             # RFC 4408.
49              
50 3     3   43 use constant macos_epoch_offset => ((1970 - 1904) * 365 + 17) * 24 * 3600;
  3         55  
  3         6283  
51             # This is a hack because the MacOS Classic epoch is relative to the local
52             # timezone. Get a real OS!
53              
54             # Interface:
55             ##############################################################################
56              
57             =head1 SYNOPSIS
58              
59             =head2 Providing the expansion context early
60              
61             use Mail::SPF::MacroString;
62              
63             my $macrostring = Mail::SPF::MacroString->new(
64             text => '%{ir}.%{v}._spf.%{d2}',
65             server => $server,
66             request => $request
67             );
68              
69             my $expanded = $macrostring->expand;
70              
71             =head2 Providing the expansion context late
72              
73             use Mail::SPF::MacroString;
74              
75             my $macrostring = Mail::SPF::MacroString->new(
76             text => '%{ir}.%{v}._spf.%{d2}'
77             );
78              
79             my $expanded1 = $macrostring->expand($server, $request1);
80              
81             $macrostring->context($server, $request2);
82             my $expanded2 = $macrostring->expand;
83              
84             =cut
85              
86             # Implementation:
87             ##############################################################################
88              
89             =head1 DESCRIPTION
90              
91             An object of class B represents a macro string that
92             can be expanded to a plain string in the context of an SPF request.
93              
94             =head2 Constructor
95              
96             The following constructor is provided:
97              
98             =over
99              
100             =item B: returns I
101              
102             Creates a new SPF record macro string object.
103              
104             %options is a list of key/value pairs representing any of the following
105             options:
106              
107             =over
108              
109             =item B
110              
111             I. The unexpanded text of the new macro string.
112              
113             =item B
114              
115             The I object that is to be used when expanding the macro
116             string. A server object need not be attached statically to the macro string;
117             it can be specified dynamically when calling the C method.
118              
119             =item B
120              
121             The I object that is to be used when expanding the macro
122             string. A request object need not be attached statically to the macro string;
123             it can be specified dynamically when calling the C method.
124              
125             =item B
126              
127             A I denoting whether the macro string is an explanation string
128             obtained via an C modifier. If B, the C, C, and C macros
129             may appear in the macro string, otherwise they may not, and if they do, a
130             I exception will be thrown when the macro string is
131             expanded. Defaults to B.
132              
133             =back
134              
135             =cut
136              
137             sub new {
138 2     2 1 274 my ($self, %options) = @_;
139 2         12 $self = $self->SUPER::new(%options);
140             defined($self->{text})
141 2 50       34 or throw Mail::SPF::EOptionRequired("Missing required 'text' option");
142 2         6 return $self;
143             }
144              
145             =back
146              
147             =head2 Instance methods
148              
149             The following instance methods are provided:
150              
151             =over
152              
153             =item B: returns I
154              
155             Returns the unexpanded text of the macro string.
156              
157             =cut
158              
159             # Read-only accessor:
160             __PACKAGE__->make_accessor('text', TRUE);
161              
162             =item B: throws I
163              
164             Attaches the given I and I objects as
165             the context for the macro string.
166              
167             =cut
168              
169             sub context {
170 1     1 1 3 my ($self, $server, $request) = @_;
171 1         3 $self->_is_valid_context(TRUE, $server, $request);
172 1         3 $self->{server} = $server;
173 1         2 $self->{request} = $request;
174 1         3 $self->{expanded} = undef;
175 1         3 return;
176             }
177              
178             =item B: returns I;
179             throws I, I, I
180              
181             =item B: returns I;
182             throws I, I, I
183              
184             Expands the text of the macro string using either the context specified through
185             an earlier call to the C method, or the given context, and returns
186             the resulting string. See RFC 4408, 8, for how macros are expanded.
187              
188             =cut
189              
190             sub expand {
191 6     6 1 609 my ($self, @context) = @_;
192              
193             return $self->{expanded}
194 6 100       18 if defined($self->{expanded});
195              
196 4         5 my $text = $self->{text};
197             return undef
198 4 50       8 if not defined($text);
199              
200 4 50       15 return $self->{expanded} = $text
201             if $text !~ /%/; # Short-circuit expansion if text has no '%' character.
202              
203 4 100       12 my ($server, $request) = @context ? @context : ($self->{server}, $self->{request});
204 4         11 $self->_is_valid_context(TRUE, $server, $request);
205              
206 3         4 my $expanded = '';
207 3         9 pos($text) = 0;
208              
209 3         19 while ($text =~ m/ \G (.*?) %(.) /cgx) {
210 9         19 $expanded .= $1;
211 9         18 my $key = $2;
212 9         12 my $pos = pos($text) - 2;
213              
214 9 50       37 if ($key eq '{') {
    0          
    0          
    0          
215 9 50       40 if ($text =~ m/ \G (\w|_\p{IsAlpha}+) ([0-9]+)? (r)? ([.\-+,\/_=]*)? } /cgx) {
216 9         31 my ($char, $rh_parts, $reverse, $delimiters) = ($1, $2, $3, $4);
217              
218             # Upper-case macro chars trigger URL-escaping AKA percent-encoding
219             # (RFC 4408, 8.1/26):
220 9         15 my $do_percent_encode = $char =~ tr/A-Z/a-z/;
221              
222 9         12 my $value;
223              
224 9 50       40 if ($char eq 's') { # RFC 4408, 8.1/19
    50          
    50          
    100          
    100          
    50          
    50          
    0          
    0          
    0          
    0          
    0          
225 0         0 $value = $request->identity;
226             }
227             elsif ($char eq 'l') { # RFC 4408, 8.1/19
228 0         0 $value = $request->localpart;
229             }
230             elsif ($char eq 'o') { # RFC 4408, 8.1/19
231 0         0 $value = $request->domain;
232             }
233             elsif ($char eq 'd') { # RFC 4408, 8.1/6/4
234 3         9 $value = $request->authority_domain;
235             }
236             elsif ($char eq 'i') { # RFC 4408, 8.1/20, 8.1/21
237 3         12 my $ip_address = $request->ip_address;
238 3 50       16 $ip_address = Mail::SPF::Util->ipv6_address_to_ipv4($ip_address)
239             if Mail::SPF::Util->ipv6_address_is_ipv4_mapped($ip_address);
240 3         59 my $ip_address_version = $ip_address->version;
241 3 50       13 if ($ip_address_version == 4) {
    0          
242 3         7 $value = $ip_address->addr;
243             }
244             elsif ($ip_address_version == 6) {
245 0         0 $value = join(".", split(//, unpack("H32", $ip_address->aton)));
246             }
247             else {
248             # Unexpected IP address version.
249 0         0 $server->throw_result('permerror', $request,
250             "Unexpected IP address version '$ip_address_version' in request");
251             }
252             }
253             elsif ($char eq 'p') { # RFC 4408, 8.1/22
254             try {
255 0     0   0 $value = Mail::SPF::Util->valid_domain_for_ip_address(
256             $server, $request, $request->ip_address, $request->authority_domain,
257             TRUE, TRUE
258             );
259             }
260 0     0   0 catch Mail::SPF::EDNSError with {};
261 0   0     0 $value ||= 'unknown';
262             }
263             elsif ($char eq 'v') { # RFC 4408, 8.1/6/7
264 3         8 my $ip_address_version = $request->ip_address->version;
265 3 50       19 if ($ip_address_version == 4) {
    0          
266 3         4 $value = 'in-addr';
267             }
268             elsif ($ip_address_version == 6) {
269 0         0 $value = 'ip6';
270             }
271             else {
272             # Unexpected IP address version.
273 0         0 $server->throw_result('permerror', $request,
274             "Unexpected IP address version '$ip_address_version' in request");
275             }
276             }
277             elsif ($char eq 'h') { # RFC 4408, 8.1/6/8
278 0   0     0 $value = $request->helo_identity || 'unknown';
279             }
280             elsif ($char eq 'c') { # RFC 4408, 8.1/20, 8.1/21
281             $self->{is_explanation}
282 0 0       0 or throw Mail::SPF::EInvalidMacro(
283             "Illegal 'c' macro in non-explanation macro string '$text'");
284 0         0 my $ip_address = $request->ip_address;
285 0 0       0 $ip_address = Mail::SPF::Util->ipv6_address_to_ipv4($ip_address)
286             if Mail::SPF::Util->ipv6_address_is_ipv4_mapped($ip_address);
287 0         0 $value = Mail::SPF::Util->ip_address_to_string($ip_address);
288             }
289             elsif ($char eq 'r') { # RFC 4408, 8.1/23
290             $self->{is_explanation}
291 0 0       0 or throw Mail::SPF::EInvalidMacro(
292             "Illegal 'r' macro in non-explanation macro string '$text'");
293 0   0     0 $value = $server->hostname || 'unknown';
294             }
295             elsif ($char eq 't') { # RFC 4408, 8.1/24
296             $self->{is_explanation}
297 0 0       0 or throw Mail::SPF::EInvalidMacro(
298             "Illegal 't' macro in non-explanation macro string '$text'");
299 0 0       0 $value = $^O ne 'MacOS' ? time() : time() + $self->macos_epoch_offset;
300             }
301             elsif ($char eq '_scope') {
302             # Scope pseudo macro for internal use only!
303 0         0 $value = $request->scope;
304             }
305             else {
306             # Unknown macro character.
307 0         0 throw Mail::SPF::EInvalidMacro(
308             "Unknown macro character '$char' at pos $pos in macro string '$text'");
309             }
310              
311 9 100 100     398 if (defined($rh_parts) or defined($reverse)) {
312 6   33     26 $delimiters ||= $self->default_split_delimiters;
313 6         52 my @list = split(/[\Q$delimiters\E]/, $value);
314 6 100       13 @list = reverse(@list) if defined($reverse);
315              
316             # Extract desired parts:
317 6 100 66     28 if (defined($rh_parts) and $rh_parts > 0) {
318 3 50       8 splice(@list, 0, @list >= $rh_parts ? @list - $rh_parts : 0);
319             }
320 6 50 66     14 if (defined($rh_parts) and $rh_parts == 0) {
321 0         0 throw Mail::SPF::EInvalidMacro(
322             "Illegal selection of 0 (zero) right-hand parts at pos $pos in macro string '$text'");
323             }
324              
325 6         19 $value = join($self->default_join_delimiter, @list);
326             }
327              
328 9 50       14 $value = URI::Escape::uri_escape($value, '^' . $self->uri_unreserved_chars)
329             # Note the comment about the set of safe/unsafe characters at the
330             # definition of the "uri_unreserved_chars" constant above.
331             if $do_percent_encode;
332              
333 9         36 $expanded .= $value;
334             }
335             else {
336             # Invalid macro expression.
337 0         0 throw Mail::SPF::EInvalidMacro(
338             "Invalid macro expression at pos $pos in macro string '$text'");
339             }
340             }
341             elsif ($key eq '-') {
342 0         0 $expanded .= '%20';
343             }
344             elsif ($key eq '_') {
345 0         0 $expanded .= ' ';
346             }
347             elsif ($key eq '%') {
348 0         0 $expanded .= '%';
349             }
350             else {
351             # Invalid macro expression.
352 0         0 throw Mail::SPF::EInvalidMacro(
353             "Invalid macro expression at pos $pos in macro string '$text'");
354             }
355             }
356              
357 3         6 $expanded .= substr($text, pos($text)); # Append remaining unmatched characters.
358              
359             #print("DEBUG: Expand $text -> $expanded\n");
360             #printf("DEBUG: Caller: %s() (line %d)\n", (caller(1))[3, 2]);
361 3 100       22 return @context ? $expanded : ($self->{expanded} = $expanded);
362             }
363              
364             =item B: returns I
365              
366             Returns B if the macro string is an explanation string obtained via an
367             C modifier. See the description of the L constructor's
368             C option.
369              
370             =cut
371              
372             # Make read-only accessor:
373             __PACKAGE__->make_accessor('is_explanation', TRUE);
374              
375             =item B: returns I
376              
377             Returns the expanded text of the macro string if a context is attached to the
378             object. Returns the unexpanded text otherwise. You can simply use a
379             Mail::SPF::MacroString object as a string for the same effect, see
380             L<"OVERLOADING">.
381              
382             =cut
383              
384             sub stringify {
385 4     4 0 1737 my ($self) = @_;
386             return
387 4 100       13 $self->_is_valid_context(FALSE, $self->{server}, $self->{request}) ?
388             $self->expand # Context availabe, expand.
389             : $self->text; # Context unavailable, do not expand.
390             }
391              
392             =back
393              
394             =cut
395              
396             sub _is_valid_context {
397 9     9   20 my ($self, $require, $server, $request) = @_;
398 9 100       30 if (not UNIVERSAL::isa($server, 'Mail::SPF::Server')) {
399 3 100       19 throw Mail::SPF::EMacroExpansionCtxRequired('Mail::SPF server object required') if $require;
400 2         10 return FALSE;
401             }
402 6 50       13 if (not UNIVERSAL::isa($request, 'Mail::SPF::Request')) {
403 0 0       0 throw Mail::SPF::EMacroExpansionCtxRequired('Request object required') if $require;
404 0         0 return FALSE;
405             }
406 6         16 return TRUE;
407             }
408              
409             =head1 OVERLOADING
410              
411             If a Mail::SPF::MacroString object is used as a I, the C
412             method is used to convert the object into a string.
413              
414             =head1 SEE ALSO
415              
416             L, L, L, L
417              
418             L
419              
420             For availability, support, and license information, see the README file
421             included with Mail::SPF.
422              
423             =head1 AUTHORS
424              
425             Julian Mehnle , Shevek
426              
427             =cut
428              
429             TRUE;