File Coverage

lib/Mail/AuthenticationResults/Parser.pm
Criterion Covered Total %
statement 201 201 100.0
branch 101 104 97.1
condition 6 6 100.0
subroutine 23 23 100.0
pod 5 5 100.0
total 336 339 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   1751269 use strict;
  24         302  
  24         724  
6 24     24   123 use warnings;
  24         60  
  24         875  
7             our $VERSION = '2.20230112'; # VERSION
8 24     24   148 use Carp;
  24         53  
  24         1706  
9              
10 24     24   9761 use Mail::AuthenticationResults::Header;
  24         78  
  24         744  
11 24     24   135 use Mail::AuthenticationResults::Header::AuthServID;
  24         46  
  24         462  
12 24     24   9969 use Mail::AuthenticationResults::Header::Comment;
  24         58  
  24         691  
13 24     24   9736 use Mail::AuthenticationResults::Header::Entry;
  24         56  
  24         652  
14 24     24   9744 use Mail::AuthenticationResults::Header::SubEntry;
  24         59  
  24         661  
15 24     24   9299 use Mail::AuthenticationResults::Header::Version;
  24         61  
  24         661  
16              
17 24     24   130 use Mail::AuthenticationResults::Token::Assignment;
  24         42  
  24         467  
18 24     24   102 use Mail::AuthenticationResults::Token::Comment;
  24         44  
  24         491  
19 24     24   9512 use Mail::AuthenticationResults::Token::QuotedString;
  24         53  
  24         646  
20 24     24   124 use Mail::AuthenticationResults::Token::Separator;
  24         46  
  24         474  
21 24     24   122 use Mail::AuthenticationResults::Token::String;
  24         42  
  24         392  
22              
23 24     24   115 use JSON;
  24         40  
  24         150  
24              
25              
26             sub new {
27 61     61 1 29410 my ( $class, $auth_header ) = @_;
28 61         144 my $self = {};
29 61         135 bless $self, $class;
30              
31 61 100       176 if ( $auth_header ) {
32 12         41 $self->parse( $auth_header );
33             }
34              
35 61         241 return $self;
36             }
37              
38              
39             sub parse {
40 60     60 1 152 my ( $self, $header ) = @_;
41              
42 60         176 $self->tokenise( $header );
43              
44 56         391 $self->_parse_authservid();
45              
46 49         105 while ( @{ $self->{ 'tokenised' } } ) {
  157         393  
47 113         270 $self->_parse_entry();
48             }
49              
50 44         183 return $self->parsed();
51             }
52              
53              
54             sub from_authentication_results_json {
55 1     1 1 3 my ( $self, $json ) = @_;
56 1         5 my $j = JSON->new();
57 1         41 my $hashref = $j->decode( $json );
58 1         16 return $self->_from_hashref( $hashref );
59             }
60              
61             sub _from_hashref {
62 24     24   40 my ( $self, $hashref ) = @_;
63 24         33 my $type = $hashref->{'type'};
64 24 50       98 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       55 if ( $type eq 'header' ) {
73 1         9 my $authserv_id = $self->_from_hashref( $hashref->{ 'authserv_id' } );
74 1         4 $object->set_value( $authserv_id );
75             }
76             else {
77 23 100       57 $object->set_key( $hashref->{'key'} ) if exists $hashref->{'key'};
78 23 50       69 $object->safe_set_value( $hashref->{'value'} ) if exists $hashref->{'value'};
79             }
80              
81 24 100       63 if ( exists $hashref->{'children'} ) {
82 18         22 for my $child ( @{ $hashref->{'children'} } ) {
  18         34  
83 22         49 my $child_object = $self->_from_hashref( $child );
84 22         47 $object->add_child( $child_object );
85             }
86             }
87              
88 24         47 return $object;
89             }
90              
91              
92              
93             sub tokenise {
94 60     60 1 129 my ( $self, $header ) = @_;
95              
96 60         94 my @tokenised;
97              
98 60         244 $header =~ s/\n/ /g;
99 60         128 $header =~ s/\r/ /g;
100 60         187 $header =~ s/^\s+//;
101              
102             # Remove Header part if present
103 60 100       254 if ( $header =~ /^Authentication-Results:/i ) {
104 13         56 $header =~ s/^Authentication-Results://i;
105             }
106              
107 60         131 my $args = {};
108 60         217 while ( length($header) > 0 ) {
109              
110 1244         1514 my $token;
111 1244         3128 $header =~ s/^\s+//;
112              
113 1244 100       3313 my $last_non_comment_type = exists( $args->{ 'last_non_comment_type' } ) ? $args->{ 'last_non_comment_type' }->is() : 'none';
114              
115 1244 100 100     7694 if ( length( $header ) == 0 ) {
    100 100        
    100          
    100          
    100          
    100          
    100          
    100          
116 2         5 last;
117             }
118             elsif ( $header =~ /^\(/ ) {
119 73         396 $token = Mail::AuthenticationResults::Token::Comment->new( $header, $args );
120             }
121             elsif ( $header =~ /^;/ ) {
122 131         575 $token = Mail::AuthenticationResults::Token::Separator->new( $header, $args );
123 131         230 $args->{ 'last_non_comment_type' } = $token;
124             }
125             elsif ( $header =~ /^"/ ) {
126 38         113 $token = Mail::AuthenticationResults::Token::QuotedString->new( $header, $args );
127 38         57 $args->{ 'last_non_comment_type' } = $token;
128             }
129             elsif ( $last_non_comment_type ne 'assignment' && $header =~ /^\./ ) {
130 83         270 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
131 83         141 $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         19 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
136 2         3 $args->{ 'last_non_comment_type' } = $token;
137             }
138             elsif ( $header =~ /^\// ) {
139 5         28 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
140 5         17 $args->{ 'last_non_comment_type' } = $token;
141             }
142             elsif ( $header =~ /^=/ ) {
143 267         803 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
144 267         403 $args->{ 'last_non_comment_type' } = $token;
145             }
146             else {
147 641         1636 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
148 641         1009 $args->{ 'last_non_comment_type' } = $token;
149             }
150              
151 1240         2587 $header = $token->remainder();
152 1240         3049 push @tokenised, $token;
153             }
154              
155 58 100       287 croak 'Nothing to parse' if ! @tokenised;
156              
157 56         239 $self->{ 'tokenised' } = \@tokenised;
158              
159 56         167 return;
160             }
161              
162             sub _parse_authservid {
163 56     56   127 my ( $self ) = @_;
164 56         104 my $tokenised = $self->{ 'tokenised' };
165 56         90 my $token;
166              
167 56         536 my $authserv_id = Mail::AuthenticationResults::Header::AuthServID->new();
168              
169             # Find the ServID
170 56         177 while ( @$tokenised ) {
171 57         108 $token = shift @$tokenised;
172 57 100       218 if ( $token->is() eq 'string' ) {
    100          
173 55         231 $authserv_id->set_value( $token->value() );
174 55         116 last;
175             }
176             elsif ( $token->is() eq 'comment' ) {
177 1         5 $authserv_id->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
178             }
179             else {
180             # assignment or separator, both are bogus
181 1         25 croak 'Invalid AuthServ-ID';
182             }
183             }
184              
185 55         165 my $expecting = 'key';
186 55         87 my $key;
187              
188             TOKEN:
189 55         169 while ( @$tokenised ) {
190 69         326 $token = shift @$tokenised;
191              
192 69 100       226 if ( $token->is() eq 'assignment' ) {
    100          
    100          
193 7 100       42 if ( $expecting eq 'assignment' ) {
194 4 100       12 if ( $token->value() eq '=' ) {
195 2         5 $expecting = 'value';
196             }
197             else {
198 2         24 croak 'unexpected token';
199             }
200             }
201             else {
202 3         38 croak 'not expecting an assignment';
203             }
204             }
205             elsif ( $token->is() eq 'comment' ) {
206 3         17 $authserv_id->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
207             }
208             elsif ( $token->is() eq 'separator' ) {
209 48         122 last TOKEN;
210             }
211 16 100       37 if ( $token->is() eq 'string' ) {
212 11 100       41 if ( $expecting eq 'key' ) {
    100          
213 9         16 $key = $token;
214 9         19 $expecting = 'assignment';
215             }
216             elsif ( $expecting eq 'value' ) {
217 1         11 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() )->set_value( $token->value() ) );
218 1         3 $expecting = 'key';
219 1         5 undef $key;
220             }
221             else {
222 1         11 croak 'not expecting a string';
223             }
224             }
225              
226             }
227 49 100       204 if ( $expecting ne 'key' ) {
228 4 100       11 if ( $key->value() =~ /^[0-9]+$/ ) {
229             # Looks like a version
230 2         19 $authserv_id->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $key->value() ) );
231             }
232             else {
233             # Probably bogus, but who knows!
234 2         11 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() ) );
235             }
236             }
237              
238 49         284 $self->{ 'header' } = Mail::AuthenticationResults::Header->new()->set_value( $authserv_id );
239 49         136 $self->{ 'tokenised' } = $tokenised;
240              
241 49         272 return;
242             }
243              
244             sub _parse_entry {
245 113     113   209 my ( $self ) = @_;
246 113         205 my $tokenised = $self->{ 'tokenised' };
247              
248 113         403 my $entry = Mail::AuthenticationResults::Header::Entry->new();
249 113         188 my $working_on = $entry;
250              
251 113         200 my $expecting = 'key';
252 113         152 my $is_subentry = 0;
253             TOKEN:
254 113         248 while ( @$tokenised ) {
255 1074         1439 my $token = shift @$tokenised;
256              
257 1074 100       2062 if ( $token->is() eq 'assignment' ) {
    100          
    100          
258 338 100       612 if ( $expecting eq 'assignment' ) {
259 336 100       643 if ( $token->value() eq '=' ) {
    100          
    50          
260 254         360 $expecting = 'value';
261             }
262             elsif ( $token->value() eq '.' ) {
263 80         121 $expecting = 'keymod';
264             }
265             elsif ( $token->value() eq '/' ) {
266 2         4 $expecting = 'version';
267             }
268             }
269             else {
270 2         21 croak 'not expecting an assignment';
271             }
272             }
273             elsif ( $token->is() eq 'comment' ) {
274 69         335 $working_on->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
275             }
276             elsif ( $token->is() eq 'separator' ) {
277 71         207 last TOKEN;
278             }
279 1001 100       1859 if ( $token->is() eq 'string' ) {
280 596 100       1465 if ( $expecting eq 'key' ) {
    100          
    100          
    100          
281 266 100       461 if ( ! $is_subentry ) {
282 113 100       284 if ( $token->value() eq 'none' ) {
283             # Special case the none
284 7         21 $expecting = 'no_more_after_none';
285             }
286             else {
287 106         226 $entry->set_key( $token->value() );
288 106         381 $expecting = 'assignment';
289             }
290             }
291             else {
292 153         492 $working_on = Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $token->value() );
293 153         448 $expecting = 'assignment';
294             }
295             }
296             elsif ( $expecting eq 'keymod' ) {
297 79         282 $working_on->set_key( $working_on->key() . '.' . $token->value() );
298 79         235 $expecting = 'assignment';
299             }
300             elsif ( $expecting eq 'version' ) {
301 2 100       6 if ( $token->value() =~ /^[0-9]+$/ ) {
302             # Looks like a version
303 1         3 $working_on->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $token->value() ) );
304             }
305             else {
306 1         11 croak 'bad version token';
307             }
308 1         4 $expecting = 'assignment';
309             }
310             elsif ( $expecting eq 'value' ) {
311 247 100       430 if ( ! $is_subentry ) {
312 98         213 $entry->set_value( $token->value() );
313 98         164 $is_subentry = 1;
314             }
315             else {
316 149         303 $entry->add_child( $working_on->set_value( $token->value() ) );
317             }
318 247         689 $expecting = 'key';
319             }
320             else {
321 2         25 croak 'not expecting a string';
322             }
323             }
324              
325             }
326              
327 108 100       301 if ( $expecting eq 'no_more_after_none' ) {
328 5         7 $self->{ 'tokenised' } = $tokenised;
329             # We may have comment entries, if so add those to the header object
330 5         6 foreach my $child ( @{ $entry->children() } ) {
  5         14  
331 2         10 delete $child->{ 'parent' };
332 2         20 $self->{ 'header' }->add_child( $child );
333             }
334 5         20 return;
335             }
336              
337 103 100       253 if ( $expecting ne 'key' ) {
338 9 100       40 if ( $is_subentry ) {
339 4         16 $entry->add_child( $working_on );
340             }
341             }
342              
343 103         356 $self->{ 'header' }->add_child( $entry );
344 103         190 $self->{ 'tokenised' } = $tokenised;
345              
346 103         200 return;
347             }
348              
349              
350             sub parsed {
351 56     56 1 5680 my ( $self ) = @_;
352 56         289 return $self->{ 'header' };
353             }
354              
355             1;
356              
357             __END__