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   1725030 use strict;
  23         254  
  23         694  
6 23     23   130 use warnings;
  23         39  
  23         913  
7             our $VERSION = '2.20210914'; # VERSION
8 23     23   176 use Carp;
  23         74  
  23         1813  
9              
10 23     23   9393 use Mail::AuthenticationResults::Header;
  23         67  
  23         788  
11 23     23   132 use Mail::AuthenticationResults::Header::AuthServID;
  23         49  
  23         460  
12 23     23   10700 use Mail::AuthenticationResults::Header::Comment;
  23         57  
  23         671  
13 23     23   8883 use Mail::AuthenticationResults::Header::Entry;
  23         53  
  23         659  
14 23     23   9274 use Mail::AuthenticationResults::Header::SubEntry;
  23         68  
  23         713  
15 23     23   9057 use Mail::AuthenticationResults::Header::Version;
  23         53  
  23         643  
16              
17 23     23   126 use Mail::AuthenticationResults::Token::Assignment;
  23         41  
  23         528  
18 23     23   108 use Mail::AuthenticationResults::Token::Comment;
  23         42  
  23         465  
19 23     23   9083 use Mail::AuthenticationResults::Token::QuotedString;
  23         86  
  23         724  
20 23     23   138 use Mail::AuthenticationResults::Token::Separator;
  23         43  
  23         464  
21 23     23   104 use Mail::AuthenticationResults::Token::String;
  23         41  
  23         38198  
22              
23              
24             sub new {
25 59     59 1 34346 my ( $class, $auth_header ) = @_;
26 59         137 my $self = {};
27 59         136 bless $self, $class;
28              
29 59 100       196 if ( $auth_header ) {
30 11         39 $self->parse( $auth_header );
31             }
32              
33 59         230 return $self;
34             }
35              
36              
37             sub parse {
38 59     59 1 147 my ( $self, $header ) = @_;
39              
40 59         197 $self->tokenise( $header );
41              
42 55         275 $self->_parse_authservid();
43              
44 48         100 while ( @{ $self->{ 'tokenised' } } ) {
  148         376  
45 105         274 $self->_parse_entry();
46             }
47              
48 43         137 return $self->parsed();
49             }
50              
51              
52             sub tokenise {
53 59     59 1 145 my ( $self, $header ) = @_;
54              
55 59         96 my @tokenised;
56              
57 59         245 $header =~ s/\n/ /g;
58 59         131 $header =~ s/\r/ /g;
59 59         186 $header =~ s/^\s+//;
60              
61             # Remove Header part if present
62 59 100       233 if ( $header =~ /^Authentication-Results:/i ) {
63 13         52 $header =~ s/^Authentication-Results://i;
64             }
65              
66 59         126 my $args = {};
67 59         198 while ( length($header) > 0 ) {
68              
69 1170         1408 my $token;
70 1170         2994 $header =~ s/^\s+//;
71              
72 1170 100       3229 my $last_non_comment_type = exists( $args->{ 'last_non_comment_type' } ) ? $args->{ 'last_non_comment_type' }->is() : 'none';
73              
74 1170 100 100     7295 if ( length( $header ) == 0 ) {
    100 100        
    100          
    100          
    100          
    100          
    100          
    100          
75 2         5 last;
76             }
77             elsif ( $header =~ /^\(/ ) {
78 67         352 $token = Mail::AuthenticationResults::Token::Comment->new( $header, $args );
79             }
80             elsif ( $header =~ /^;/ ) {
81 123         484 $token = Mail::AuthenticationResults::Token::Separator->new( $header, $args );
82 123         227 $args->{ 'last_non_comment_type' } = $token;
83             }
84             elsif ( $header =~ /^"/ ) {
85 38         98 $token = Mail::AuthenticationResults::Token::QuotedString->new( $header, $args );
86 38         56 $args->{ 'last_non_comment_type' } = $token;
87             }
88             elsif ( $last_non_comment_type ne 'assignment' && $header =~ /^\./ ) {
89 77         213 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
90 77         126 $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         12 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
95 2         4 $args->{ 'last_non_comment_type' } = $token;
96             }
97             elsif ( $header =~ /^\// ) {
98 5         23 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
99 5         9 $args->{ 'last_non_comment_type' } = $token;
100             }
101             elsif ( $header =~ /^=/ ) {
102 251         1108 $token = Mail::AuthenticationResults::Token::Assignment->new( $header, $args );
103 251         434 $args->{ 'last_non_comment_type' } = $token;
104             }
105             else {
106 603         1565 $token = Mail::AuthenticationResults::Token::String->new( $header, $args );
107 603         1005 $args->{ 'last_non_comment_type' } = $token;
108             }
109              
110 1166         2741 $header = $token->remainder();
111 1166         2951 push @tokenised, $token;
112             }
113              
114 57 100       262 croak 'Nothing to parse' if ! @tokenised;
115              
116 55         277 $self->{ 'tokenised' } = \@tokenised;
117              
118 55         130 return;
119             }
120              
121             sub _parse_authservid {
122 55     55   125 my ( $self ) = @_;
123 55         169 my $tokenised = $self->{ 'tokenised' };
124 55         89 my $token;
125              
126 55         720 my $authserv_id = Mail::AuthenticationResults::Header::AuthServID->new();
127              
128             # Find the ServID
129 55         259 while ( @$tokenised ) {
130 56         122 $token = shift @$tokenised;
131 56 100       197 if ( $token->is() eq 'string' ) {
    100          
132 54         260 $authserv_id->set_value( $token->value() );
133 54         156 last;
134             }
135             elsif ( $token->is() eq 'comment' ) {
136 1         5 $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         20 croak 'Invalid AuthServ-ID';
141             }
142             }
143              
144 54         126 my $expecting = 'key';
145 54         189 my $key;
146              
147             TOKEN:
148 54         171 while ( @$tokenised ) {
149 68         395 $token = shift @$tokenised;
150              
151 68 100       214 if ( $token->is() eq 'assignment' ) {
    100          
    100          
152 7 100       20 if ( $expecting eq 'assignment' ) {
153 4 100       13 if ( $token->value() eq '=' ) {
154 2         5 $expecting = 'value';
155             }
156             else {
157 2         26 croak 'unexpected token';
158             }
159             }
160             else {
161 3         49 croak 'not expecting an assignment';
162             }
163             }
164             elsif ( $token->is() eq 'comment' ) {
165 3         13 $authserv_id->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
166             }
167             elsif ( $token->is() eq 'separator' ) {
168 47         113 last TOKEN;
169             }
170 16 100       40 if ( $token->is() eq 'string' ) {
171 11 100       47 if ( $expecting eq 'key' ) {
    100          
172 9         30 $key = $token;
173 9         23 $expecting = 'assignment';
174             }
175             elsif ( $expecting eq 'value' ) {
176 1         23 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() )->set_value( $token->value() ) );
177 1         2 $expecting = 'key';
178 1         6 undef $key;
179             }
180             else {
181 1         12 croak 'not expecting a string';
182             }
183             }
184              
185             }
186 48 100       178 if ( $expecting ne 'key' ) {
187 4 100       11 if ( $key->value() =~ /^[0-9]+$/ ) {
188             # Looks like a version
189 2         22 $authserv_id->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $key->value() ) );
190             }
191             else {
192             # Probably bogus, but who knows!
193 2         15 $authserv_id->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $key->value() ) );
194             }
195             }
196              
197 48         295 $self->{ 'header' } = Mail::AuthenticationResults::Header->new()->set_value( $authserv_id );
198 48         120 $self->{ 'tokenised' } = $tokenised;
199              
200 48         356 return;
201             }
202              
203             sub _parse_entry {
204 105     105   196 my ( $self ) = @_;
205 105         173 my $tokenised = $self->{ 'tokenised' };
206              
207 105         417 my $entry = Mail::AuthenticationResults::Header::Entry->new();
208 105         168 my $working_on = $entry;
209              
210 105         178 my $expecting = 'key';
211 105         177 my $is_subentry = 0;
212             TOKEN:
213 105         244 while ( @$tokenised ) {
214 1002         1384 my $token = shift @$tokenised;
215              
216 1002 100       1929 if ( $token->is() eq 'assignment' ) {
    100          
    100          
217 316 100       535 if ( $expecting eq 'assignment' ) {
218 314 100       562 if ( $token->value() eq '=' ) {
    100          
    50          
219 238         348 $expecting = 'value';
220             }
221             elsif ( $token->value() eq '.' ) {
222 74         110 $expecting = 'keymod';
223             }
224             elsif ( $token->value() eq '/' ) {
225 2         14 $expecting = 'version';
226             }
227             }
228             else {
229 2         24 croak 'not expecting an assignment';
230             }
231             }
232             elsif ( $token->is() eq 'comment' ) {
233 63         293 $working_on->add_child( Mail::AuthenticationResults::Header::Comment->new()->set_value( $token->value() ) );
234             }
235             elsif ( $token->is() eq 'separator' ) {
236 64         177 last TOKEN;
237             }
238 936 100       1677 if ( $token->is() eq 'string' ) {
239 559 100       1269 if ( $expecting eq 'key' ) {
    100          
    100          
    100          
240 250 100       450 if ( ! $is_subentry ) {
241 105 100       243 if ( $token->value() eq 'none' ) {
242             # Special case the none
243 7         23 $expecting = 'no_more_after_none';
244             }
245             else {
246 98         221 $entry->set_key( $token->value() );
247 98         351 $expecting = 'assignment';
248             }
249             }
250             else {
251 145         460 $working_on = Mail::AuthenticationResults::Header::SubEntry->new()->set_key( $token->value() );
252 145         432 $expecting = 'assignment';
253             }
254             }
255             elsif ( $expecting eq 'keymod' ) {
256 73         223 $working_on->set_key( $working_on->key() . '.' . $token->value() );
257 73         219 $expecting = 'assignment';
258             }
259             elsif ( $expecting eq 'version' ) {
260 2 100       6 if ( $token->value() =~ /^[0-9]+$/ ) {
261             # Looks like a version
262 1         4 $working_on->add_child( Mail::AuthenticationResults::Header::Version->new()->set_value( $token->value() ) );
263             }
264             else {
265 1         14 croak 'bad version token';
266             }
267 1         5 $expecting = 'assignment';
268             }
269             elsif ( $expecting eq 'value' ) {
270 232 100       375 if ( ! $is_subentry ) {
271 90         250 $entry->set_value( $token->value() );
272 90         146 $is_subentry = 1;
273             }
274             else {
275 142         287 $entry->add_child( $working_on->set_value( $token->value() ) );
276             }
277 232         684 $expecting = 'key';
278             }
279             else {
280 2         26 croak 'not expecting a string';
281             }
282             }
283              
284             }
285              
286 100 100       248 if ( $expecting eq 'no_more_after_none' ) {
287 5         9 $self->{ 'tokenised' } = $tokenised;
288             # We may have comment entries, if so add those to the header object
289 5         8 foreach my $child ( @{ $entry->children() } ) {
  5         38  
290 2         5 delete $child->{ 'parent' };
291 2         7 $self->{ 'header' }->add_child( $child );
292             }
293 5         25 return;
294             }
295              
296 95 100       240 if ( $expecting ne 'key' ) {
297 8 100       23 if ( $is_subentry ) {
298 3         10 $entry->add_child( $working_on );
299             }
300             }
301              
302 95         319 $self->{ 'header' }->add_child( $entry );
303 95         158 $self->{ 'tokenised' } = $tokenised;
304              
305 95         168 return;
306             }
307              
308              
309             sub parsed {
310 54     54 1 3659 my ( $self ) = @_;
311 54         309 return $self->{ 'header' };
312             }
313              
314             1;
315              
316             __END__