File Coverage

blib/lib/Courriel/Headers.pm
Criterion Covered Total %
statement 113 166 68.0
branch 17 34 50.0
condition 4 18 22.2
subroutine 24 29 82.7
pod 9 9 100.0
total 167 256 65.2


line stmt bran cond sub pod time code
1             package Courriel::Headers;
2              
3 8     8   1038 use strict;
  8         9  
  8         192  
4 8     8   24 use warnings;
  8         28  
  8         159  
5 8     8   26 use namespace::autoclean;
  8         8  
  8         36  
6              
7             our $VERSION = '0.42';
8              
9 8     8   2362 use Courriel::Header;
  8         15  
  8         246  
10 8     8   2941 use Courriel::Header::ContentType;
  8         23  
  8         258  
11 8     8   3559 use Courriel::Header::Disposition;
  8         18  
  8         341  
12             use Courriel::Types
13 8     8   52 qw( ArrayRef Defined HashRef HeaderArray NonEmptyStr Str Streamable StringRef );
  8         10  
  8         62  
14 8     8   56079 use Encode qw( decode );
  8         11  
  8         444  
15 8     8   35 use MIME::Base64 qw( decode_base64 );
  8         10  
  8         294  
16 8     8   3932 use MIME::QuotedPrint qw( decode_qp );
  8         1523  
  8         375  
17 8     8   37 use MooseX::Params::Validate qw( pos_validated_list validated_list );
  8         9  
  8         62  
18 8     8   1631 use Scalar::Util qw( blessed reftype );
  8         10  
  8         317  
19              
20 8     8   31 use Moose;
  8         11  
  8         43  
21 8     8   22711 use MooseX::StrictConstructor;
  8         12  
  8         57  
22              
23             with 'Courriel::Role::Streams' => { -exclude => ['stream_to'] };
24              
25             has _headers => (
26             traits => ['Array'],
27             is => 'ro',
28             isa => HeaderArray,
29             default => sub { [] },
30             init_arg => 'headers',
31             handles => {
32             headers => 'elements',
33             },
34             );
35              
36             # The _key_indices field, along with all the complicated code to
37             # get/add/remove headers below, is necessary because RFC 5322 says:
38             #
39             # However, for the purposes of this specification, header fields SHOULD NOT
40             # be reordered when a message is transported or transformed. More
41             # importantly, the trace header fields and resent header fields MUST NOT be
42             # reordered, and SHOULD be kept in blocks prepended to the message.
43             #
44             # So we store headers as an array ref. When we add additional values for a
45             # header, we will put them after the last header of the same name in the array
46             # ref. If no such header exists yet, then we just put them at the end of the
47             # arrayref.
48              
49             has _key_indices => (
50             traits => ['Hash'],
51             isa => HashRef [ ArrayRef [NonEmptyStr] ],
52             init_arg => undef,
53             lazy => 1,
54             builder => '_build_key_indices',
55             clearer => '_clear_key_indices',
56             handles => {
57             __key_indices_for => 'get',
58             },
59             );
60              
61             override BUILDARGS => sub {
62             my $class = shift;
63              
64             my $p = super();
65              
66             return $p unless $p->{headers};
67              
68             # Could this be done as a coercion for the HeaderArray type? Maybe, but
69             # it'd probably need structured types, which seems like as much of a
70             # hassle as just doing this.
71             if ( reftype( $p->{headers} ) eq 'ARRAY' ) {
72             my $headers = $p->{headers};
73              
74             ## no critic (ControlStructures::ProhibitCStyleForLoops)
75             for ( my $i = 1; $i < @{$headers}; $i += 2 ) {
76             next if blessed( $headers->[ $i - 1 ] );
77              
78             my $name = $headers->[ $i - 1 ];
79              
80             next unless defined $name;
81              
82             $headers->[$i] = $class->_inflate_header( $name, $headers->[$i] );
83             }
84             }
85             elsif ( reftype( $p->{headers} ) eq 'HASH' ) {
86             for my $name ( keys %{ $p->{headers} } ) {
87             next if blessed( $p->{headers}{$name} );
88              
89             $p->{headers}{$name}
90             = $class->_inflate_header( $name, $p->{headers}{$name} );
91             }
92             }
93              
94             return $p;
95             };
96              
97             sub _inflate_header {
98 2210     2210   1741 my $class = shift;
99 2210         1480 my $name = shift;
100 2210         1537 my $value = shift;
101              
102 2210 100       4436 my ( $header_class, $method )
    100          
103             = lc $name eq 'content-type'
104             ? ( 'Courriel::Header::ContentType', 'new_from_value' )
105             : lc $name eq 'content-disposition'
106             ? ( 'Courriel::Header::Disposition', 'new_from_value' )
107             : ( 'Courriel::Header', 'new' );
108              
109 2210         47082 return $header_class->$method(
110             name => $name,
111             value => $value,
112             );
113             }
114              
115             sub _build_key_indices {
116 356     356   349 my $self = shift;
117              
118 356         7481 my $headers = $self->_headers;
119              
120 356         342 my %indices;
121             ## no critic (ControlStructures::ProhibitCStyleForLoops)
122 356         361 for ( my $i = 0; $i < @{$headers}; $i += 2 ) {
  4553         5633  
123 4197         2427 push @{ $indices{ lc $headers->[$i] } }, $i + 1;
  4197         6734  
124             }
125              
126 356         9990 return \%indices;
127             }
128              
129             {
130             my @spec = ( { isa => NonEmptyStr } );
131              
132             sub get {
133 309     309 1 289 my $self = shift;
134 309         949 my ($name) = pos_validated_list(
135             \@_,
136             @spec,
137             );
138              
139 309         147024 return @{ $self->_headers }[ $self->_key_indices_for($name) ];
  309         6801  
140             }
141             }
142              
143             {
144             my @spec = ( { isa => NonEmptyStr } );
145              
146             sub get_values {
147 0     0 1 0 my $self = shift;
148 0         0 my ($name) = pos_validated_list(
149             \@_,
150             @spec,
151             );
152              
153             return
154 0         0 map { $_->value }
155 0         0 @{ $self->_headers }[ $self->_key_indices_for($name) ];
  0         0  
156             }
157             }
158              
159             sub _key_indices_for {
160 655     655   671 my $self = shift;
161 655         672 my $name = shift;
162              
163 655 100       559 return @{ $self->__key_indices_for( lc $name ) || [] };
  655         18534  
164             }
165              
166             {
167             my @spec = (
168             { isa => NonEmptyStr },
169             { isa => Defined },
170             );
171              
172             sub add {
173 173     173 1 174 my $self = shift;
174 173         414 my ( $name, $value ) = pos_validated_list(
175             \@_,
176             @spec,
177             );
178              
179 173         127715 my $headers = $self->_headers;
180              
181 173         361 my $last_index = ( $self->_key_indices_for($name) )[-1];
182              
183 173 100 66     861 my $header
184             = blessed($value)
185             && $value->isa('Courriel::Header')
186             ? $value
187             : $self->_inflate_header( $name, $value );
188              
189 173 50       229 if ($last_index) {
190 0         0 splice @{$headers}, $last_index + 1, 0, ( $name => $header );
  0         0  
191             }
192             else {
193 173         138 push @{$headers}, ( $name => $header );
  173         264  
194             }
195              
196 173         4798 $self->_clear_key_indices;
197              
198 173         256 return;
199             }
200             }
201              
202             {
203             my @spec = (
204             { isa => NonEmptyStr },
205             { isa => Defined },
206             );
207              
208             # Used to add things like Resent or Received headers
209              
210             ## no critic (Subroutines::ProhibitBuiltinHomonyms)
211             sub unshift {
212 0     0 1 0 my $self = shift;
213 0         0 my ( $name, $value ) = pos_validated_list(
214             \@_,
215             { isa => NonEmptyStr },
216             ( { isa => Defined } ) x ( @_ - 1 ),
217             MX_PARAMS_VALIDATE_NO_CACHE => 1,
218             );
219              
220 0         0 my $headers = $self->_headers;
221              
222 0 0 0     0 my $header
223             = blessed($value)
224             && $value->isa('Courriel::Header')
225             ? $value
226             : $self->_inflate_header( $name, $value );
227              
228 0         0 unshift @{$headers}, ( $name => $header );
  0         0  
229              
230 0         0 return;
231             }
232             }
233              
234             {
235             my @spec = (
236             { isa => NonEmptyStr },
237             );
238              
239             sub remove {
240 173     173 1 204 my $self = shift;
241 173         400 my ($name) = pos_validated_list(
242             \@_,
243             @spec,
244             );
245              
246 173         83685 my $headers = $self->_headers;
247              
248 173         377 for my $idx ( reverse $self->_key_indices_for($name) ) {
249 107         84 splice @{$headers}, $idx - 1, 2;
  107         1919  
250             }
251              
252 173         4470 $self->_clear_key_indices;
253              
254 173         288 return;
255             }
256             }
257              
258             {
259             my @spec = (
260             { isa => NonEmptyStr },
261             { isa => Defined },
262             );
263              
264             sub replace {
265 173     173 1 156 my $self = shift;
266 173         436 my ( $name, $value ) = pos_validated_list(
267             \@_,
268             @spec,
269             );
270              
271 173         125725 $self->remove($name);
272 173         357 $self->add( $name => $value );
273              
274 173         321 return;
275             }
276             }
277              
278             {
279             my $horiz_text = qr/[^\x0a\x0d]/;
280             my $horiz_ws = qr/[ \t]/;
281             my $line_re = qr/
282             (?:
283             ([^\s:][^:\n\r]*) # a header name
284             : # followed by a colon
285             $horiz_ws*
286             ($horiz_text*) # header value - can be empty
287             )
288             |
289             $horiz_ws+(\S$horiz_text*)? # continuation line
290             /x;
291              
292             my @spec = (
293             text => { isa => StringRef, coerce => 1 },
294             );
295              
296             sub parse {
297 142     142 1 180 my $class = shift;
298 142         435 my ($text) = validated_list(
299             \@_,
300             @spec,
301             );
302              
303 142         145074 my @headers;
304              
305 142         394 $class->_maybe_fix_broken_headers($text);
306              
307 142         152 while ( ${$text} =~ /\G${line_re}$Courriel::Helpers::LINE_SEP_RE/gc )
  2844         11384  
308             {
309 2702 100       3131 if ( defined $1 ) {
310 2091         3669 push @headers, $1, $2;
311             }
312             else {
313 611 50       787 die
314             'Header text contains a continuation line before a header name has been seen.'
315             unless @headers;
316              
317 611   50     728 $headers[-1] //= q{};
318              
319             # RFC 5322 says:
320             #
321             # Runs of FWS, comment, or CFWS that occur between lexical tokens in a
322             # structured header field are semantically interpreted as a single
323             # space character.
324 611 50       846 $headers[-1] .= q{ } if length $headers[-1];
325 611 100       1330 $headers[-1] .= $3 if defined $3;
326             }
327             }
328              
329 142   50     149 my $pos = pos ${$text} // 0;
  142         389  
330 142 50       167 if ( $pos != length ${$text} ) {
  142         270  
331             my @lines = split $Courriel::Helpers::LINE_SEP_RE,
332 0         0 substr( ${$text}, 0, $pos );
  0         0  
333 0         0 my $count = ( scalar @lines ) + 1;
334              
335 0         0 my $line = ( split $Courriel::Helpers::LINE_SEP_RE, ${$text} )
  0         0  
336             [ $count - 1 ];
337              
338 0 0       0 die defined $line
339             ? "Found an unparseable chunk in the header text starting at line $count:\n $line"
340             : 'Could not parse headers at all';
341             }
342              
343             ## no critic (ControlStructures::ProhibitCStyleForLoops)
344 142         348 for ( my $i = 1; $i < @headers; $i += 2 ) {
345 2091         2176 $headers[$i] = $class->_mime_decode( $headers[$i] );
346             }
347              
348 142         3598 return $class->new( headers => \@headers );
349             }
350             }
351              
352             sub _maybe_fix_broken_headers {
353 142     142   213 my $class = shift;
354 142         169 my $text = shift;
355              
356             # Some broken email messages have a newline in the headers that isn't
357             # acting as a continuation, it's just an arbitrary line break. See
358             # t/data/stress-test/mbox_mime_applemail_1xb.txt
359 142         134 ${$text}
  142         2256  
360             =~ s/$Courriel::Helpers::LINE_SEP_RE([^\s:][^:]+$Courriel::Helpers::LINE_SEP_RE)/$1/g;
361              
362 142         230 return;
363             }
364              
365             {
366             my @spec = (
367             output => { isa => Streamable, coerce => 1 },
368             skip => { isa => ArrayRef [NonEmptyStr], default => [] },
369             charset => { isa => NonEmptyStr, default => 'utf8' },
370             );
371              
372             sub stream_to {
373 0     0 1 0 my $self = shift;
374 0         0 my ( $output, $skip, $charset ) = validated_list(
375             \@_,
376             @spec
377             );
378              
379 0         0 my %skip = map { lc $_ => 1 } @{$skip};
  0         0  
  0         0  
380              
381 0         0 for my $header ( grep { blessed($_) } @{ $self->_headers } ) {
  0         0  
  0         0  
382 0 0       0 next if $skip{ lc $header->name };
383              
384 0         0 $header->stream_to( charset => $charset, output => $output );
385             }
386              
387 0         0 return;
388             }
389             }
390              
391             sub as_string {
392 0     0 1 0 my $self = shift;
393              
394 0         0 my $string = q{};
395              
396 0         0 $self->stream_to( output => $self->_string_output( \$string ), @_ );
397              
398 0         0 return $string;
399             }
400              
401             {
402             my $mime_word = qr/
403             (?:
404             =\? # begin encoded word
405             (?<charset>[-0-9A-Za-z_]+) # charset (encoding)
406             (?:\*[A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*)? # language (RFC 2231)
407             \?
408             (?<encoding>[QqBb]) # encoding type
409             \?
410             (?<content>.*?) # Base64-encoded contents
411             \?= # end encoded word
412             |
413             (?<unencoded>\S+)
414             )
415             (?<ws>[ \t]+)?
416             /x;
417              
418             sub _mime_decode {
419 2091     2091   1298 my $self = shift;
420 2091         1351 my $text = shift;
421              
422 2091 50       4893 return $text unless $text =~ /=\?[\w-]+\?[BQ]\?/i;
423              
424 0           my @chunks;
425              
426             # If a MIME encoded word is followed by _another_ such word, we ignore any
427             # intervening whitespace, otherwise we preserve the whitespace between a
428             # MIME encoded word and an unencoded word. See RFC 2047 for details on
429             # this.
430 0           while ( $text =~ /\G$mime_word/g ) {
431 0 0         if ( defined $+{charset} ) {
432             push @chunks, {
433             content => $self->_decode_one_word(
434             @+{ 'charset', 'encoding', 'content' }
435             ),
436             ws => $+{ws},
437 0           is_mime => 1,
438             };
439             }
440             else {
441             push @chunks, {
442             content => $+{unencoded},
443             ws => $+{ws},
444 0           is_mime => 0,
445             };
446             }
447             }
448              
449 0           my $result = q{};
450              
451 0           for my $i ( 0 .. $#chunks ) {
452 0           $result .= $chunks[$i]{content};
453             $result .= ( $chunks[$i]{ws} // q{} )
454             unless $chunks[$i]{is_mime}
455             && $chunks[ $i + 1 ]
456 0 0 0       && $chunks[ $i + 1 ]{is_mime};
      0        
      0        
457             }
458              
459 0           return $result;
460             }
461             }
462              
463             sub _decode_one_word {
464 0     0     my $self = shift;
465 0           my $charset = shift;
466 0           my $encoding = shift;
467 0           my $content = shift;
468              
469 0 0         if ( uc $encoding eq 'B' ) {
470 0           return decode( $charset, decode_base64($content) );
471             }
472             else {
473 0           $content =~ tr/_/ /;
474 0           return decode( $charset, decode_qp($content) );
475             }
476             }
477              
478             __PACKAGE__->meta->make_immutable;
479              
480             1;
481              
482             # ABSTRACT: The headers for an email part
483              
484             __END__
485              
486             =pod
487              
488             =encoding utf-8
489              
490             =head1 NAME
491              
492             Courriel::Headers - The headers for an email part
493              
494             =head1 VERSION
495              
496             version 0.42
497              
498             =head1 SYNOPSIS
499              
500             my $email = Courriel->parse( text => ... );
501             my $headers = $email->headers;
502              
503             print "$_\n" for $headers->get('Received');
504              
505             =head1 DESCRIPTION
506              
507             This class represents the headers of an email.
508              
509             Any sub part of an email can have its own headers, so every part has an
510             associated object representing its headers. This class makes no distinction
511             between top-level headers and headers for a sub part.
512              
513             Each individual header name/value pair is represented internally by a
514             L<Courriel::Header> object. Some headers have their own special
515             subclass. These are:
516              
517             =over 4
518              
519             =item * Content-Type
520              
521             This is stored as a L<Courriel::Header::ContentType> object.
522              
523             =item * Content-Disposition
524              
525             This is stored as a L<Courriel::Header::Disposition> object.
526              
527             =back
528              
529             =head1 API
530              
531             This class supports the following methods:
532              
533             =head2 Courriel::Headers->parse( ... )
534              
535             This method creates a new object by parsing a string. It accepts the following
536             parameters:
537              
538             =over 4
539              
540             =item * text
541              
542             The text to parse. This can either be a plain scalar or a reference to a
543             scalar. If you pass a reference, the underlying scalar may be modified.
544              
545             =back
546              
547             Header parsing unfolds folded headers, and decodes any MIME-encoded values as
548             described in RFC 2047. Parsing also decodes header attributes encoded as
549             described in RFC 2231.
550              
551             =head2 Courriel::Headers->new( headers => [ ... ] )
552              
553             This method creates a new object. It accepts one parameter, C<headers>, which
554             should be an array reference of header names and values.
555              
556             A given header key can appear multiple times.
557              
558             This object does not (yet, perhaps) enforce RFC restrictions on repetition of
559             certain headers.
560              
561             Header order is preserved, per RFC 5322.
562              
563             =head2 $headers->get($name)
564              
565             Given a header name, this returns a list of the L<Courriel::Header> objects
566             found for the header. Each occurrence of the header is returned as a separate
567             object.
568              
569             =head2 $headers->get_values($name)
570              
571             Given a header name, this returns a list of the string values found for the
572             header. Each occurrence of the header is returned as a separate string.
573              
574             =head2 $headers->add( $name => $value )
575              
576             Given a header name and value, this adds the headers to the object. If any of
577             the headers already have values in the object, then new values are added after
578             the existing values, rather than at the end of headers.
579              
580             The value can be provided as a string or a L<Courriel::Header> object.
581              
582             =head2 $headers->unshift( $name => $value )
583              
584             This is like C<add()>, but this pushes the headers onto the front of the
585             internal headers array. This is useful if you are adding "Received" headers,
586             which per RFC 5322, should always be added at the I<top> of the headers.
587              
588             The value can be provided as a string or a L<Courriel::Header> object.
589              
590             =head2 $headers->remove($name)
591              
592             Given a header name, this removes all instances of that header from the object.
593              
594             =head2 $headers->replace( $name => $value )
595              
596             A shortcut for calling C<remove()> and C<add()>.
597              
598             The value can be provided as a string or a L<Courriel::Header> object.
599              
600             =head2 $headers->as_string( skip => ...., charset => ... )
601              
602             This returns a string representing the headers in the object. The values will
603             be folded and/or MIME-encoded as needed.
604              
605             The C<skip> parameter should be an array reference containing the name of
606             headers that should be skipped. This parameter is optional, and the default is
607             to include all headers.
608              
609             The C<charset> parameter specifies what character set to use for MIME-encoding
610             non-ASCII values. This defaults to "utf8". The charset name must be one
611             recognized by the L<Encode> module.
612              
613             MIME encoding is always done using the "B" (Base64) encoding, never the "Q"
614             encoding.
615              
616             =head2 $headers->stream_to( output => $output, skip => ...., charset => ... )
617              
618             This method will send the stringified headers to the specified output.
619              
620             See the C<as_string()> method for documentation on the C<skip> and C<charset>
621             parameters.
622              
623             =head1 ROLES
624              
625             This class does the C<Courriel::Role::Streams> role.
626              
627             =head1 SUPPORT
628              
629             Bugs may be submitted through L<the RT bug tracker|http://rt.cpan.org/Public/Dist/Display.html?Name=Courriel>
630             (or L<bug-courriel@rt.cpan.org|mailto:bug-courriel@rt.cpan.org>).
631              
632             I am also usually active on IRC as 'drolsky' on C<irc://irc.perl.org>.
633              
634             =head1 AUTHOR
635              
636             Dave Rolsky <autarch@urth.org>
637              
638             =head1 COPYRIGHT AND LICENCE
639              
640             This software is Copyright (c) 2016 by Dave Rolsky.
641              
642             This is free software, licensed under:
643              
644             The Artistic License 2.0 (GPL Compatible)
645              
646             =cut