File Coverage

lib/Mail/AuthenticationResults/Parser.pm
Criterion Covered Total %
statement 203 203 100.0
branch 103 106 97.1
condition 12 12 100.0
subroutine 23 23 100.0
pod 5 5 100.0
total 346 349 99.1


line stmt bran cond sub pod time code
1             package Mail::AuthenticationResults::Parser;
2             # ABSTRACT: Class for parsing Authentication Results Headers
3              
4             require 5.008;
5 24     24   2943908 use strict;
  24         51  
  24         995  
6 24     24   117 use warnings;
  24         177  
  24         1650  
7             our $VERSION = '2.20260216'; # VERSION
8 24     24   151 use Carp;
  24         53  
  24         2069  
9              
10 24     24   11551 use Mail::AuthenticationResults::Header;
  24         72  
  24         898  
11 24     24   139 use Mail::AuthenticationResults::Header::AuthServID;
  24         42  
  24         544  
12 24     24   105 use Mail::AuthenticationResults::Header::Comment;
  24         38  
  24         420  
13 24     24   97 use Mail::AuthenticationResults::Header::Entry;
  24         38  
  24         413  
14 24     24   91 use Mail::AuthenticationResults::Header::SubEntry;
  24         55  
  24         446  
15 24     24   11813 use Mail::AuthenticationResults::Header::Version;
  24         71  
  24         854  
16              
17 24     24   138 use Mail::AuthenticationResults::Token::Assignment;
  24         46  
  24         608  
18 24     24   119 use Mail::AuthenticationResults::Token::Comment;
  24         37  
  24         786  
19 24     24   10479 use Mail::AuthenticationResults::Token::QuotedString;
  24         68  
  24         775  
20 24     24   137 use Mail::AuthenticationResults::Token::Separator;
  24         42  
  24         551  
21 24     24   99 use Mail::AuthenticationResults::Token::String;
  24         41  
  24         491  
22              
23 24     24   113 use JSON;
  24         33  
  24         171  
24              
25              
26             sub new {
27 62     62 1 4799444 my ( $class, $auth_header ) = @_;
28 62         130 my $self = {};
29 62         127 bless $self, $class;
30              
31 62 100       229 if ( $auth_header ) {
32 12         48 $self->parse( $auth_header );
33             }
34              
35 62         356 return $self;
36             }
37              
38              
39             sub parse {
40 61     61 1 137 my ( $self, $header ) = @_;
41              
42 61         258 $self->tokenise( $header );
43              
44 56         273 $self->_parse_authservid();
45              
46 49         92 while ( @{ $self->{ 'tokenised' } } ) {
  159         449  
47 114         328 $self->_parse_entry();
48             }
49              
50 45         155 return $self->parsed();
51             }
52              
53              
54             sub from_authentication_results_json {
55 1     1 1 3 my ( $self, $json ) = @_;
56 1         16 my $j = JSON->new();
57 1         53 my $hashref = $j->decode( $json );
58 1         5 return $self->_from_hashref( $hashref );
59             }
60              
61             sub _from_hashref {
62 24     24   42 my ( $self, $hashref ) = @_;
63 24         70 my $type = $hashref->{'type'};
64 24 50       121 my $object
    100          
    100          
    100          
    100          
65             = $type eq 'header' ? Mail::AuthenticationResults::Header->new()
66             : $type eq 'authservid' ? Mail::AuthenticationResults::Header::AuthServID->new()
67             : $type eq 'entry' ? Mail::AuthenticationResults::Header::Entry->new()
68             : $type eq 'subentry' ? Mail::AuthenticationResults::Header::SubEntry->new()
69             : $type eq 'comment' ? Mail::AuthenticationResults::Header::Comment->new()
70             : croak "unknown type $type";
71              
72 24 100       48 if ( $type eq 'header' ) {
73 1         17 my $authserv_id = $self->_from_hashref( $hashref->{ 'authserv_id' } );
74 1         5 $object->set_value( $authserv_id );
75             }
76             else {
77 23 100       84 $object->set_key( $hashref->{'key'} ) if exists $hashref->{'key'};
78 23 50       96 $object->safe_set_value( $hashref->{'value'} ) if exists $hashref->{'value'};
79             }
80              
81 24 100       60 if ( exists $hashref->{'children'} ) {
82 18         25 for my $child ( @{ $hashref->{'children'} } ) {
  18         51  
83 22         55 my $child_object = $self->_from_hashref( $child );
84 22         71 $object->add_child( $child_object );
85             }
86             }
87              
88 24         71 return $object;
89             }
90              
91              
92              
93             sub tokenise {
94 61     61 1 160 my ( $self, $header ) = @_;
95              
96 61         873 my @tokenised;
97              
98 61         502 $header =~ s/\n/ /g;
99 61         447 $header =~ s/\r/ /g;
100 61         274 $header =~ s/^\s+//;
101              
102             # Remove Header part if present
103 61 100       241 if ( $header =~ /^Authentication-Results:/i ) {
104 13         2082 $header =~ s/^Authentication-Results://i;
105             }
106              
107 61         117 my $args = {};
108 61         232 while ( length($header) > 0 ) {
109              
110 1252         1661 my $token;
111 1252         3885 $header =~ s/^\s+//;
112              
113 1252 100       4003 my $last_non_comment_type = exists( $args->{ 'last_non_comment_type' } ) ? $args->{ 'last_non_comment_type' }->is() : 'none';
114              
115 1252 100 100     12586 if ( length( $header ) == 0 ) {
    100 100        
    100 100        
    100 100        
    100          
    100          
    100          
    100          
    100          
116 2         3 last;
117             }
118             elsif ( $header =~ /^\(/ ) {
119 73         443 $token = Mail::AuthenticationResults::Token::Comment->new( $header, $args );
120             }
121             elsif ( $header =~ /^;/ ) {
122 132         623 $token = Mail::AuthenticationResults::Token::Separator->new( $header, $args );
123 132         240 $args->{ 'last_non_comment_type' } = $token;
124             }
125             elsif ( $header =~ /^"/ ) {
126 38         95 $token = Mail::AuthenticationResults::Token::QuotedString->new( $header, $args );
127 38         61 $args->{ 'last_non_comment_type' } = $token;
128             }
129             elsif ( $last_non_comment_type ne 'assignment' && $header =~ /^\./ ) {
130 83         244 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
131 83         160 $args->{ 'last_non_comment_type' } = $token;
132             }
133             elsif ( $last_non_comment_type eq 'assignment' && $header =~ /^\./ ) {
134             # a . after an assignment cannot be another assignment, likely an unquoted string.
135 4         12 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
136 2         5 $args->{ 'last_non_comment_type' } = $token;
137             }
138             elsif ( $last_non_comment_type ne 'assignment' && $header =~ /^\// ) {
139 4         21 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
140 4         8 $args->{ 'last_non_comment_type' } = $token;
141             }
142             elsif ( $last_non_comment_type eq 'assignment' && $header =~ /^\// ) {
143             # a / after an assignment cannot be another assignment, likely an unquoted string.
144 2         6 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
145 1         1 $args->{ 'last_non_comment_type' } = $token;
146             }
147             elsif ( $header =~ /^=/ ) {
148 269         980 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
149 269         442 $args->{ 'last_non_comment_type' } = $token;
150             }
151             else {
152 645         2096 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
153 645         1191 $args->{ 'last_non_comment_type' } = $token;
154             }
155              
156 1247         2854 $header = $token->remainder();
157 1247         3822 push @tokenised, $token;
158             }
159              
160 58 100       255 croak 'Nothing to parse' if ! @tokenised;
161              
162 56         391 $self->{ 'tokenised' } = \@tokenised;
163              
164 56         147 return;
165             }
166              
167             sub _parse_authservid {
168 56     56   115 my ( $self ) = @_;
169 56         108 my $tokenised = $self->{ 'tokenised' };
170 56         91 my $token;
171              
172 56         677 my $authserv_id = Mail::AuthenticationResults::Header::AuthServID->new();
173              
174             # Find the ServID
175 56         194 while ( @$tokenised ) {
176 57         125 $token = shift @$tokenised;
177 57 100       200 if ( $token->is() eq 'string' ) {
    100          
178 55         319 $authserv_id->set_value( $token->value() );
179 55         113 last;
180             }
181             elsif ( $token->is() eq 'comment' ) {
182 1         3 $authserv_id->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
183             }
184             else {
185             # assignment or separator, both are bogus
186 1         13 croak 'Invalid AuthServ-ID';
187             }
188             }
189              
190 55         120 my $expecting = 'key';
191 55         95 my $key;
192              
193             TOKEN:
194 55         230 while ( @$tokenised ) {
195 69         429 $token = shift @$tokenised;
196              
197 69 100       269 if ( $token->is() eq 'assignment' ) {
    100          
    100          
198 7 100       14 if ( $expecting eq 'assignment' ) {
199 4 100       8 if ( $token->value() eq '=' ) {
200 2         3 $expecting = 'value';
201             }
202             else {
203 2         17 croak 'unexpected token';
204             }
205             }
206             else {
207 3         48 croak 'not expecting an assignment';
208             }
209             }
210             elsif ( $token->is() eq 'comment' ) {
211 3         18 $authserv_id->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
212             }
213             elsif ( $token->is() eq 'separator' ) {
214 48         118 last TOKEN;
215             }
216 16 100       32 if ( $token->is() eq 'string' ) {
217 11 100       25 if ( $expecting eq 'key' ) {
    100          
218 9         8 $key = $token;
219 9         18 $expecting = 'assignment';
220             }
221             elsif ( $expecting eq 'value' ) {
222 1         10 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() )->set_value( $token->value() ) );
223 1         2 $expecting = 'key';
224 1         4 undef $key;
225             }
226             else {
227 1         8 croak 'not expecting a string';
228             }
229             }
230              
231             }
232 49 100       173 if ( $expecting ne 'key' ) {
233 4 100       10 if ( $key->value() =~ /^[0-9]+$/ ) {
234             # Looks like a version
235 2         21 $authserv_id->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $key->value() ) );
236             }
237             else {
238             # Probably bogus, but who knows!
239 2         13 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() ) );
240             }
241             }
242              
243 49         335 $self->{ 'header' } = Mail::AuthenticationResults::Header->new()->set_value( $authserv_id );
244 49         180 $self->{ 'tokenised' } = $tokenised;
245              
246 49         436 return;
247             }
248              
249             sub _parse_entry {
250 114     114   194 my ( $self ) = @_;
251 114         236 my $tokenised = $self->{ 'tokenised' };
252              
253 114         552 my $entry = Mail::AuthenticationResults::Header::Entry->new();
254 114         202 my $working_on = $entry;
255              
256 114         200 my $expecting = 'key';
257 114         181 my $is_subentry = 0;
258             TOKEN:
259 114         255 while ( @$tokenised ) {
260 1081         1706 my $token = shift @$tokenised;
261              
262 1081 100       2524 if ( $token->is() eq 'assignment' ) {
    100          
    100          
263 339 100       660 if ( $expecting eq 'assignment' ) {
264 338 100       859 if ( $token->value() eq '=' ) {
    100          
    50          
265 257         465 $expecting = 'value';
266             }
267             elsif ( $token->value() eq '.' ) {
268 79         150 $expecting = 'keymod';
269             }
270             elsif ( $token->value() eq '/' ) {
271 2         4 $expecting = 'version';
272             }
273             }
274             else {
275 1         11 croak 'not expecting an assignment';
276             }
277             }
278             elsif ( $token->is() eq 'comment' ) {
279 69         501 $working_on->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
280             }
281             elsif ( $token->is() eq 'separator' ) {
282 72         227 last TOKEN;
283             }
284 1008 100       2098 if ( $token->is() eq 'string' ) {
285 601 100       1722 if ( $expecting eq 'key' ) {
    100          
    100          
    100          
286 268 100       565 if ( ! $is_subentry ) {
287 114 100       342 if ( $token->value() eq 'none' ) {
288             # Special case the none
289 7         23 $expecting = 'no_more_after_none';
290             }
291             else {
292 107         332 $entry->set_key( $token->value() );
293 107         463 $expecting = 'assignment';
294             }
295             }
296             else {
297 154         597 $working_on = Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $token->value() );
298 154         615 $expecting = 'assignment';
299             }
300             }
301             elsif ( $expecting eq 'keymod' ) {
302 79         325 $working_on->set_key( $working_on->key() . '.' . $token->value() );
303 79         322 $expecting = 'assignment';
304             }
305             elsif ( $expecting eq 'version' ) {
306 2 100       7 if ( $token->value() =~ /^[0-9]+$/ ) {
307             # Looks like a version
308 1         5 $working_on->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $token->value() ) );
309             }
310             else {
311 1         8 croak 'bad version token';
312             }
313 1         6 $expecting = 'assignment';
314             }
315             elsif ( $expecting eq 'value' ) {
316 250 100       505 if ( ! $is_subentry ) {
317 100         310 $entry->set_value( $token->value() );
318 100         182 $is_subentry = 1;
319             }
320             else {
321 150         368 $entry->add_child( $working_on->set_value( $token->value() ) );
322             }
323 250         989 $expecting = 'key';
324             }
325             else {
326 2         37 croak 'not expecting a string';
327             }
328             }
329              
330             }
331              
332 110 100       451 if ( $expecting eq 'no_more_after_none' ) {
333 5         9 $self->{ 'tokenised' } = $tokenised;
334             # We may have comment entries, if so add those to the header object
335 5         7 foreach my $child ( @{ $entry->children() } ) {
  5         19  
336 2         5 delete $child->{ 'parent' };
337 2         9 $self->{ 'header' }->add_child( $child );
338             }
339 5         23 return;
340             }
341              
342 105 100       277 if ( $expecting ne 'key' ) {
343 9 100       45 if ( $is_subentry ) {
344 4         19 $entry->add_child( $working_on );
345             }
346             }
347              
348 105         498 $self->{ 'header' }->add_child( $entry );
349 105         178 $self->{ 'tokenised' } = $tokenised;
350              
351 105         311 return;
352             }
353              
354              
355             sub parsed {
356 57     57 1 6964 my ( $self ) = @_;
357 57         462 return $self->{ 'header' };
358             }
359              
360             1;
361              
362             __END__