File Coverage

lib/Mail/AuthenticationResults/Parser.pm
Criterion Covered Total %
statement 180 180 100.0
branch 85 86 98.8
condition 6 6 100.0
subroutine 20 20 100.0
pod 4 4 100.0
total 295 296 99.6


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 23     23   1576731 use strict;
  23         294  
  23         654  
6 23     23   122 use warnings;
  23         40  
  23         904  
7             our $VERSION = '2.20210915'; # VERSION
8 23     23   127 use Carp;
  23         38  
  23         1611  
9              
10 23     23   8662 use Mail::AuthenticationResults::Header;
  23         65  
  23         744  
11 23     23   132 use Mail::AuthenticationResults::Header::AuthServID;
  23         44  
  23         463  
12 23     23   8871 use Mail::AuthenticationResults::Header::Comment;
  23         49  
  23         639  
13 23     23   8354 use Mail::AuthenticationResults::Header::Entry;
  23         57  
  23         609  
14 23     23   8174 use Mail::AuthenticationResults::Header::SubEntry;
  23         92  
  23         612  
15 23     23   9588 use Mail::AuthenticationResults::Header::Version;
  23         53  
  23         622  
16              
17 23     23   125 use Mail::AuthenticationResults::Token::Assignment;
  23         41  
  23         465  
18 23     23   99 use Mail::AuthenticationResults::Token::Comment;
  23         42  
  23         455  
19 23     23   8461 use Mail::AuthenticationResults::Token::QuotedString;
  23         53  
  23         605  
20 23     23   118 use Mail::AuthenticationResults::Token::Separator;
  23         41  
  23         436  
21 23     23   104 use Mail::AuthenticationResults::Token::String;
  23         42  
  23         37994  
22              
23              
24             sub new {
25 59     59 1 29625 my ( $class, $auth_header ) = @_;
26 59         125 my $self = {};
27 59         130 bless $self, $class;
28              
29 59 100       183 if ( $auth_header ) {
30 11         35 $self->parse( $auth_header );
31             }
32              
33 59         212 return $self;
34             }
35              
36              
37             sub parse {
38 59     59 1 136 my ( $self, $header ) = @_;
39              
40 59         247 $self->tokenise( $header );
41              
42 55         240 $self->_parse_authservid();
43              
44 48         87 while ( @{ $self->{ 'tokenised' } } ) {
  148         365  
45 105         232 $self->_parse_entry();
46             }
47              
48 43         121 return $self->parsed();
49             }
50              
51              
52             sub tokenise {
53 59     59 1 132 my ( $self, $header ) = @_;
54              
55 59         98 my @tokenised;
56              
57 59         233 $header =~ s/\n/ /g;
58 59         124 $header =~ s/\r/ /g;
59 59         203 $header =~ s/^\s+//;
60              
61             # Remove Header part if present
62 59 100       375 if ( $header =~ /^Authentication-Results:/i ) {
63 13         52 $header =~ s/^Authentication-Results://i;
64             }
65              
66 59         118 my $args = {};
67 59         180 while ( length($header) > 0 ) {
68              
69 1170         1377 my $token;
70 1170         2975 $header =~ s/^\s+//;
71              
72 1170 100       3102 my $last_non_comment_type = exists( $args->{ 'last_non_comment_type' } ) ? $args->{ 'last_non_comment_type' }->is() : 'none';
73              
74 1170 100 100     7316 if ( length( $header ) == 0 ) {
    100 100        
    100          
    100          
    100          
    100          
    100          
    100          
75 2         5 last;
76             }
77             elsif ( $header =~ /^\(/ ) {
78 67         347 $token = Mail::AuthenticationResults::Token::Comment->new( $header, $args );
79             }
80             elsif ( $header =~ /^;/ ) {
81 123         455 $token = Mail::AuthenticationResults::Token::Separator->new( $header, $args );
82 123         230 $args->{ 'last_non_comment_type' } = $token;
83             }
84             elsif ( $header =~ /^"/ ) {
85 38         112 $token = Mail::AuthenticationResults::Token::QuotedString->new( $header, $args );
86 38         59 $args->{ 'last_non_comment_type' } = $token;
87             }
88             elsif ( $last_non_comment_type ne 'assignment' && $header =~ /^\./ ) {
89 77         218 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
90 77         134 $args->{ 'last_non_comment_type' } = $token;
91             }
92             elsif ( $last_non_comment_type eq 'assignment' && $header =~ /^\./ ) {
93             # a . after an assignment cannot be another assignment, likely an unquoted string.
94 4         11 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
95 2         3 $args->{ 'last_non_comment_type' } = $token;
96             }
97             elsif ( $header =~ /^\// ) {
98 5         22 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
99 5         9 $args->{ 'last_non_comment_type' } = $token;
100             }
101             elsif ( $header =~ /^=/ ) {
102 251         772 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
103 251         401 $args->{ 'last_non_comment_type' } = $token;
104             }
105             else {
106 603         1604 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
107 603         960 $args->{ 'last_non_comment_type' } = $token;
108             }
109              
110 1166         2360 $header = $token->remainder();
111 1166         2875 push @tokenised, $token;
112             }
113              
114 57 100       245 croak 'Nothing to parse' if ! @tokenised;
115              
116 55         229 $self->{ 'tokenised' } = \@tokenised;
117              
118 55         141 return;
119             }
120              
121             sub _parse_authservid {
122 55     55   113 my ( $self ) = @_;
123 55         117 my $tokenised = $self->{ 'tokenised' };
124 55         79 my $token;
125              
126 55         612 my $authserv_id = Mail::AuthenticationResults::Header::AuthServID->new();
127              
128             # Find the ServID
129 55         211 while ( @$tokenised ) {
130 56         119 $token = shift @$tokenised;
131 56 100       188 if ( $token->is() eq 'string' ) {
    100          
132 54         240 $authserv_id->set_value( $token->value() );
133 54         135 last;
134             }
135             elsif ( $token->is() eq 'comment' ) {
136 1         3 $authserv_id->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
137             }
138             else {
139             # assignment or separator, both are bogus
140 1         18 croak 'Invalid AuthServ-ID';
141             }
142             }
143              
144 54         109 my $expecting = 'key';
145 54         155 my $key;
146              
147             TOKEN:
148 54         162 while ( @$tokenised ) {
149 68         339 $token = shift @$tokenised;
150              
151 68 100       243 if ( $token->is() eq 'assignment' ) {
    100          
    100          
152 7 100       16 if ( $expecting eq 'assignment' ) {
153 4 100       11 if ( $token->value() eq '=' ) {
154 2         5 $expecting = 'value';
155             }
156             else {
157 2         20 croak 'unexpected token';
158             }
159             }
160             else {
161 3         38 croak 'not expecting an assignment';
162             }
163             }
164             elsif ( $token->is() eq 'comment' ) {
165 3         14 $authserv_id->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
166             }
167             elsif ( $token->is() eq 'separator' ) {
168 47         108 last TOKEN;
169             }
170 16 100       38 if ( $token->is() eq 'string' ) {
171 11 100       36 if ( $expecting eq 'key' ) {
    100          
172 9         17 $key = $token;
173 9         20 $expecting = 'assignment';
174             }
175             elsif ( $expecting eq 'value' ) {
176 1         12 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() )->set_value( $token->value() ) );
177 1         3 $expecting = 'key';
178 1         4 undef $key;
179             }
180             else {
181 1         10 croak 'not expecting a string';
182             }
183             }
184              
185             }
186 48 100       159 if ( $expecting ne 'key' ) {
187 4 100       11 if ( $key->value() =~ /^[0-9]+$/ ) {
188             # Looks like a version
189 2         19 $authserv_id->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $key->value() ) );
190             }
191             else {
192             # Probably bogus, but who knows!
193 2         32 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() ) );
194             }
195             }
196              
197 48         251 $self->{ 'header' } = Mail::AuthenticationResults::Header->new()->set_value( $authserv_id );
198 48         112 $self->{ 'tokenised' } = $tokenised;
199              
200 48         217 return;
201             }
202              
203             sub _parse_entry {
204 105     105   191 my ( $self ) = @_;
205 105         173 my $tokenised = $self->{ 'tokenised' };
206              
207 105         364 my $entry = Mail::AuthenticationResults::Header::Entry->new();
208 105         166 my $working_on = $entry;
209              
210 105         178 my $expecting = 'key';
211 105         132 my $is_subentry = 0;
212             TOKEN:
213 105         236 while ( @$tokenised ) {
214 1002         1355 my $token = shift @$tokenised;
215              
216 1002 100       1939 if ( $token->is() eq 'assignment' ) {
    100          
    100          
217 316 100       540 if ( $expecting eq 'assignment' ) {
218 314 100       529 if ( $token->value() eq '=' ) {
    100          
    50          
219 238         341 $expecting = 'value';
220             }
221             elsif ( $token->value() eq '.' ) {
222 74         149 $expecting = 'keymod';
223             }
224             elsif ( $token->value() eq '/' ) {
225 2         11 $expecting = 'version';
226             }
227             }
228             else {
229 2         19 croak 'not expecting an assignment';
230             }
231             }
232             elsif ( $token->is() eq 'comment' ) {
233 63         303 $working_on->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
234             }
235             elsif ( $token->is() eq 'separator' ) {
236 64         175 last TOKEN;
237             }
238 936 100       1703 if ( $token->is() eq 'string' ) {
239 559 100       1270 if ( $expecting eq 'key' ) {
    100          
    100          
    100          
240 250 100       445 if ( ! $is_subentry ) {
241 105 100       222 if ( $token->value() eq 'none' ) {
242             # Special case the none
243 7         22 $expecting = 'no_more_after_none';
244             }
245             else {
246 98         222 $entry->set_key( $token->value() );
247 98         391 $expecting = 'assignment';
248             }
249             }
250             else {
251 145         408 $working_on = Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $token->value() );
252 145         427 $expecting = 'assignment';
253             }
254             }
255             elsif ( $expecting eq 'keymod' ) {
256 73         217 $working_on->set_key( $working_on->key() . '.' . $token->value() );
257 73         239 $expecting = 'assignment';
258             }
259             elsif ( $expecting eq 'version' ) {
260 2 100       6 if ( $token->value() =~ /^[0-9]+$/ ) {
261             # Looks like a version
262 1         5 $working_on->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $token->value() ) );
263             }
264             else {
265 1         11 croak 'bad version token';
266             }
267 1         4 $expecting = 'assignment';
268             }
269             elsif ( $expecting eq 'value' ) {
270 232 100       399 if ( ! $is_subentry ) {
271 90         185 $entry->set_value( $token->value() );
272 90         147 $is_subentry = 1;
273             }
274             else {
275 142         292 $entry->add_child( $working_on->set_value( $token->value() ) );
276             }
277 232         691 $expecting = 'key';
278             }
279             else {
280 2         25 croak 'not expecting a string';
281             }
282             }
283              
284             }
285              
286 100 100       250 if ( $expecting eq 'no_more_after_none' ) {
287 5         7 $self->{ 'tokenised' } = $tokenised;
288             # We may have comment entries, if so add those to the header object
289 5         16 foreach my $child ( @{ $entry->children() } ) {
  5         17  
290 2         4 delete $child->{ 'parent' };
291 2         6 $self->{ 'header' }->add_child( $child );
292             }
293 5         22 return;
294             }
295              
296 95 100       195 if ( $expecting ne 'key' ) {
297 8 100       22 if ( $is_subentry ) {
298 3         10 $entry->add_child( $working_on );
299             }
300             }
301              
302 95         321 $self->{ 'header' }->add_child( $entry );
303 95         151 $self->{ 'tokenised' } = $tokenised;
304              
305 95         173 return;
306             }
307              
308              
309             sub parsed {
310 54     54 1 3236 my ( $self ) = @_;
311 54         289 return $self->{ 'header' };
312             }
313              
314             1;
315              
316             __END__