File Coverage

blib/lib/Mail/Header.pm
Criterion Covered Total %
statement 196 310 63.2
branch 69 168 41.0
condition 36 82 43.9
subroutine 24 31 77.4
pod 22 22 100.0
total 347 613 56.6


line stmt bran cond sub pod time code
1             # Copyrights 1995-2017 by [Mark Overmeer ].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             package Mail::Header;
6 3     3   640 use vars '$VERSION';
  3         6  
  3         131  
7             $VERSION = '2.19';
8              
9              
10 3     3   15 use strict;
  3         4  
  3         54  
11 3     3   23 use Carp;
  3         5  
  3         9559  
12              
13             my $MAIL_FROM = 'KEEP';
14             my %HDR_LENGTHS = ();
15              
16             our $FIELD_NAME = '[^\x00-\x1f\x7f-\xff :]+:';
17              
18              
19             ##
20             ## Private functions
21             ##
22              
23 1     1   8 sub _error { warn @_; () }
  1         7  
24              
25             # tidy up internal hash table and list
26              
27             sub _tidy_header
28 0     0   0 { my $self = shift;
29 0         0 my $deleted = 0;
30              
31 0         0 for(my $i = 0 ; $i < @{$self->{mail_hdr_list}}; $i++)
  0         0  
32 0 0       0 { next if defined $self->{mail_hdr_list}[$i];
33              
34 0         0 splice @{$self->{mail_hdr_list}}, $i, 1;
  0         0  
35 0         0 $deleted++;
36 0         0 $i--;
37             }
38              
39 0 0       0 if($deleted)
40 0         0 { local $_;
41 0         0 my @del;
42              
43 0         0 while(my ($key,$ref) = each %{$self->{mail_hdr_hash}} )
  0         0  
44             { push @del, $key
45 0 0       0 unless @$ref = grep { ref $_ && defined $$_ } @$ref;
  0 0       0  
46             }
47              
48 0         0 delete $self->{'mail_hdr_hash'}{$_} for @del;
49             }
50             }
51              
52             # fold the line to the given length
53              
54             my %STRUCTURE = map { (lc $_ => undef) }
55             qw{ To Cc Bcc From Date Reply-To Sender
56             Resent-Date Resent-From Resent-Sender Resent-To Return-Path
57             list-help list-post list-unsubscribe Mailing-List
58             Received References Message-ID In-Reply-To
59             Content-Length Content-Type Content-Disposition
60             Delivered-To
61             Lines
62             MIME-Version
63             Precedence
64             Status
65             };
66              
67             sub _fold_line
68 27     27   38 { my($ln,$maxlen) = @_;
69              
70 27 50       42 $maxlen = 20
71             if $maxlen < 20;
72              
73 27         31 my $max = int($maxlen - 5); # 4 for leading spcs + 1 for [\,\;]
74 27         49 my $min = int($maxlen * 4 / 5) - 4;
75              
76 27         86 $_[0] =~ s/[\r\n]+//og; # Remove new-lines
77 27         162 $_[0] =~ s/\s*\Z/\n/so; # End line with a EOLN
78              
79 27 50       53 return if $_[0] =~ /^From\s/io;
80              
81 27 100       55 if(length($_[0]) > $maxlen)
82 14 100 66     58 { if($_[0] =~ /^([-\w]+)/ && exists $STRUCTURE{ lc $1 } )
83             { #Split the line up
84             # first bias towards splitting at a , or a ; >4/5 along the line
85             # next split a whitespace
86             # else we are looking at a single word and probably don't want to split
87 9         14 my $x = "";
88 9         365 $x .= "$1\n " while $_[0] =~
89             s/^\s*
90             ( [^"]{$min,$max} [,;]
91             | [^"]{1,$max} [,;\s]
92             | [^\s"]*(?:"[^"]*"[ \t]?[^\s"]*)+\s
93             ) //x;
94              
95 9         16 $x .= $_[0];
96 9         11 $_[0] = $x;
97 9         103 $_[0] =~ s/(\A\s+|[\t ]+\Z)//sog;
98 9         50 $_[0] =~ s/\s+\n/\n/sog;
99             }
100             else
101 5         75 { $_[0] =~ s/(.{$min,$max})(\s)/$1\n$2/g;
102 5         43 $_[0] =~ s/\s*$/\n/s;
103             }
104             }
105              
106 27         69 $_[0] =~ s/\A(\S+)\n\s*(?=\S)/$1 /so;
107             }
108              
109             # Tags are case-insensitive, but there is a (slightly) preferred construction
110             # being all characters are lowercase except the first of each word. Also
111             # if the word is an `acronym' then all characters are uppercase. We decide
112             # a word is an acronym if it does not contain a vowel.
113             # In general, this change of capitalization is a bad idea, but it is in
114             # the code for ages, and therefore probably crucial for existing
115             # applications.
116              
117             sub _tag_case
118 61     61   75 { my $tag = shift;
119 61         116 $tag =~ s/\:$//;
120             join '-'
121 61 100       121 , map { /^[b-df-hj-np-tv-z]+$|^(?:MIME|SWE|SOAP|LDAP|ID)$/i
  69         343  
122             ? uc($_) : ucfirst(lc($_))
123             } split m/\-/, $tag, -1;
124             }
125              
126             # format a complete line
127             # ensure line starts with the given tag
128             # ensure tag is correct case
129             # change the 'From ' tag as required
130             # fold the line
131              
132             sub _fmt_line
133 25     25   43 { my ($self, $tag, $line, $modify) = @_;
134 25   66     77 $modify ||= $self->{mail_hdr_modify};
135 25         28 my $ctag = undef;
136              
137 25 50       41 ($tag) = $line =~ /^($FIELD_NAME|From )/oi
138             unless defined $tag;
139              
140 25 50 66     93 if(defined $tag && $tag =~ /^From /io && $self->{mail_hdr_mail_from} ne 'KEEP')
      66        
141 0 0       0 { if($self->{mail_hdr_mail_from} eq 'COERCE')
    0          
    0          
142 0         0 { $line =~ s/^From /Mail-From: /o;
143 0         0 $tag = "Mail-From:";
144             }
145             elsif($self->{mail_hdr_mail_from} eq 'IGNORE')
146 0         0 { return ();
147             }
148             elsif($self->{mail_hdr_mail_from} eq 'ERROR')
149 0         0 { return _error "unadorned 'From ' ignored: <$line>";
150             }
151             }
152              
153 25 50       48 if(defined $tag)
154 25         40 { $tag = _tag_case($ctag = $tag);
155 25 100       51 $ctag = $tag if $modify;
156 25 50       91 $ctag =~ s/([^ :])$/$1:/o if defined $ctag;
157             }
158              
159 25 50 33     170 defined $ctag && $ctag =~ /^($FIELD_NAME|From )/oi
160             or croak "Bad RFC822 field name '$tag'\n";
161              
162             # Ensure the line starts with tag
163 25 100 100     177 if(defined $ctag && ($modify || $line !~ /^\Q$ctag\E/i))
      66        
164 15         50 { (my $xtag = $ctag) =~ s/\s*\Z//o;
165 15         153 $line =~ s/^(\Q$ctag\E)?\s*/$xtag /i;
166             }
167              
168             my $maxlen = $self->{mail_hdr_lengths}{$tag}
169 25   33     109 || $HDR_LENGTHS{$tag}
170             || $self->fold_length;
171              
172 25 100 66     74 if ($modify && defined $maxlen)
    100          
173             { # folding will fix bad header continuations for us
174 9         13 _fold_line $line, $maxlen;
175             }
176             elsif($line =~ /\r?\n\S/)
177 1         5 { return _error "Bad header continuation, skipping '$tag': ",
178             "no space after newline in '$line'\n";
179             }
180              
181              
182 24         168 $line =~ s/\n*$/\n/so;
183 24         86 ($tag, $line);
184             }
185              
186             sub _insert
187 24     24   41 { my ($self, $tag, $line, $where) = @_;
188              
189 24 50       38 if($where < 0)
    0          
190 24         26 { $where = @{$self->{mail_hdr_list}} + $where + 1;
  24         45  
191 24 50       40 $where = 0 if $where < 0;
192             }
193 0         0 elsif($where >= @{$self->{mail_hdr_list}})
194 0         0 { $where = @{$self->{mail_hdr_list}};
  0         0  
195             }
196              
197 24         27 my $atend = $where == @{$self->{mail_hdr_list}};
  24         37  
198 24         26 splice @{$self->{mail_hdr_list}}, $where, 0, $line;
  24         51  
199              
200 24   100     91 $self->{mail_hdr_hash}{$tag} ||= [];
201 24         27 my $ref = \${$self->{mail_hdr_list}}[$where];
  24         79  
202              
203 24         32 my $def = $self->{mail_hdr_hash}{$tag};
204 24 100 66     74 if($def && $where)
205 18 50       26 { if($atend) { push @$def, $ref }
  18         42  
206             else
207 0         0 { my $i = 0;
208 0         0 foreach my $ln (@{$self->{mail_hdr_list}})
  0         0  
209 0         0 { my $r = \$ln;
210 0 0       0 last if $r == $ref;
211 0 0       0 $i++ if $r == $def->[$i];
212             }
213 0         0 splice @$def, $i, 0, $ref;
214             }
215             }
216             else
217 6         20 { unshift @$def, $ref;
218             }
219             }
220              
221             #------------
222              
223             sub new
224 11     11 1 118 { my $call = shift;
225 11   33     32 my $class = ref($call) || $call;
226 11 100       23 my $arg = @_ % 2 ? shift : undef;
227 11         20 my %opt = @_;
228              
229             $opt{Modify} = delete $opt{Reformat}
230 11 100       24 unless exists $opt{Modify};
231              
232             my $self = bless
233             { mail_hdr_list => []
234             , mail_hdr_hash => {}
235 11   100     63 , mail_hdr_modify => (delete $opt{Modify} || 0)
236             , mail_hdr_foldlen => 79
237             , mail_hdr_lengths => {}
238             }, $class;
239              
240 11   33     50 $self->mail_from( uc($opt{MailFrom} || $MAIL_FROM) );
241              
242             $self->fold_length($opt{FoldLength})
243 11 50       24 if exists $opt{FoldLength};
244              
245 11 100       22 if(!ref $arg) {}
    50          
    0          
246 5         16 elsif(ref($arg) eq 'ARRAY') { $self->extract( [ @$arg ] ) }
247 0         0 elsif(defined fileno($arg)) { $self->read($arg) }
248              
249 11         45 $self;
250             }
251              
252              
253             sub dup
254 2     2 1 3 { my $self = shift;
255 2         25 my $dup = ref($self)->new;
256              
257 2         11 %$dup = %$self;
258 2         6 $dup->empty; # rebuild tables
259              
260 2         2 $dup->{mail_hdr_list} = [ @{$self->{mail_hdr_list}} ];
  2         6  
261              
262 2         3 foreach my $ln ( @{$dup->{mail_hdr_list}} )
  2         5  
263 8         57 { my $tag = _tag_case +($ln =~ /^($FIELD_NAME|From )/oi)[0];
264 8         13 push @{$dup->{mail_hdr_hash}{$tag}}, \$ln;
  8         19  
265             }
266              
267 2         13 $dup;
268             }
269              
270             #------------
271              
272             sub extract
273 6     6 1 11 { my ($self, $lines) = @_;
274 6         13 $self->empty;
275              
276 6         10 while(@$lines)
277 21         31 { my $line = shift @$lines;
278 21 100       58 last if $line =~ /^\r?$/;
279              
280 19 50       115 $line =~ /^($FIELD_NAME|From )/o or next;
281 19         37 my $tag = $1;
282              
283 19   100     79 $line .= shift @$lines
284             while @$lines && $lines->[0] =~ /^[ \t]+/;
285              
286 19         35 ($tag, $line) = _fmt_line $self, $tag, $line;
287              
288 19 100       54 _insert $self, $tag, $line, -1
289             if defined $line;
290             }
291              
292 6         10 $self;
293             }
294              
295              
296             sub read
297 0     0 1 0 { my ($self, $fd) = @_;
298 0         0 $self->empty;
299              
300 0         0 my ($ln, $tag, $line);
301 0         0 while(1)
302 0         0 { $ln = <$fd>;
303              
304 0 0 0     0 if(defined $ln && defined $line && $ln =~ /^[ \t]+/)
      0        
305 0         0 { $line .= $ln; # folded line
306 0         0 next;
307             }
308              
309 0 0       0 if(defined $line)
310 0         0 { ($tag, $line) = _fmt_line $self, $tag, $line;
311 0 0       0 _insert $self, $tag, $line, -1
312             if defined $line;
313 0         0 ($tag, $line) = ();
314             }
315              
316 0 0 0     0 last if !defined $ln || $ln =~ m/^\r?$/;
317              
318 0 0       0 $ln =~ /^($FIELD_NAME|From )/o or next;
319 0         0 ($tag, $line) = ($1, $ln);
320             }
321              
322 0         0 $self;
323             }
324              
325              
326             sub empty
327 8     8 1 10 { my $self = shift;
328 8         12 $self->{mail_hdr_list} = [];
329 8         15 $self->{mail_hdr_hash} = {};
330 8         11 $self;
331             }
332              
333              
334             sub header
335 1     1 1 3 { my $self = shift;
336              
337 1 50       5 $self->extract(@_)
338             if @_;
339              
340             $self->fold
341 1 50       3 if $self->{mail_hdr_modify};
342              
343 1         1 [ @{$self->{mail_hdr_list}} ];
  1         4  
344             }
345              
346              
347             sub header_hashref
348 2     2 1 16 { my ($self, $hashref) = @_;
349              
350 2         7 while(my ($key, $value) = each %$hashref)
351 2 100       6 { $self->add($key, $_) for ref $value ? @$value : $value;
352             }
353              
354             $self->fold
355 2 50       4 if $self->{mail_hdr_modify};
356              
357             defined wantarray # MO, added minimal optimization
358 2 100       5 or return;
359              
360 3         6 +{ map { ($_ => [$self->get($_)] ) } # MO: Eh?
361 1         2 keys %{$self->{mail_hdr_hash}}
  1         2  
362             };
363             }
364              
365             #------------
366              
367             sub modify
368 3     3 1 4 { my $self = shift;
369 3         4 my $old = $self->{mail_hdr_modify};
370              
371 3 50       8 $self->{mail_hdr_modify} = 0 + shift
372             if @_;
373              
374 3         4 $old;
375             }
376              
377              
378             sub mail_from
379 11     11 1 15 { my $thing = shift;
380 11         16 my $choice = uc shift;
381              
382 11 50       46 $choice =~ /^(IGNORE|ERROR|COERCE|KEEP)$/
383             or die "bad Mail-From choice: '$choice'";
384              
385 11 50       22 if(ref $thing) { $thing->{mail_hdr_mail_from} = $choice }
  11         23  
386 0         0 else { $MAIL_FROM = $choice }
387              
388 11         15 $thing;
389             }
390              
391              
392             sub fold_length
393 28     28 1 43 { my $thing = shift;
394 28         30 my $old;
395              
396 28 50       43 if(@_ == 2)
397 0         0 { my $tag = _tag_case shift;
398 0         0 my $len = shift;
399              
400 0 0       0 my $hash = ref $thing ? $thing->{mail_hdr_lengths} : \%HDR_LENGTHS;
401 0         0 $old = $hash->{$tag};
402 0 0       0 $hash->{$tag} = $len > 20 ? $len : 20;
403             }
404             else
405 28         31 { my $self = $thing;
406 28         31 my $len = shift;
407 28         37 $old = $self->{mail_hdr_foldlen};
408              
409 28 100       52 if(defined $len)
410 3 50       5 { $self->{mail_hdr_foldlen} = $len > 20 ? $len : 20;
411 3 50       7 $self->fold if $self->{mail_hdr_modify};
412             }
413             }
414              
415 28         70 $old;
416             }
417              
418             #------------
419              
420             sub fold
421 3     3 1 27 { my ($self, $maxlen) = @_;
422              
423 3         3 while(my ($tag, $list) = each %{$self->{mail_hdr_hash}})
  12         34  
424             { my $len = $maxlen
425             || $self->{mail_hdr_lengths}{$tag}
426 9   0     14 || $HDR_LENGTHS{$tag}
427             || $self->fold_length;
428              
429 9         11 foreach my $ln (@$list)
430 18 50       36 { _fold_line $$ln, $len
431             if defined $ln;
432             }
433             }
434              
435 3         5 $self;
436             }
437              
438              
439             sub unfold
440 1     1 1 7 { my $self = shift;
441              
442 1 50       3 if(@_)
443 0         0 { my $tag = _tag_case shift;
444 0 0       0 my $list = $self->{mail_hdr_hash}{$tag}
445             or return $self;
446              
447 0         0 foreach my $ln (@$list)
448 0 0 0     0 { $$ln =~ s/\r?\n\s+/ /sog
449             if defined $ln && defined $$ln;
450             }
451              
452 0         0 return $self;
453             }
454              
455 1         1 while( my ($tag, $list) = each %{$self->{mail_hdr_hash}})
  4         15  
456 3         4 { foreach my $ln (@$list)
457 6 50 33     25 { $$ln =~ s/\r?\n\s+/ /sog
458             if defined $ln && defined $$ln;
459             }
460             }
461              
462 1         2 $self;
463             }
464              
465              
466             sub add
467 6     6 1 20 { my ($self, $tag, $text, $where) = @_;
468 6         7 ($tag, my $line) = _fmt_line $self, $tag, $text;
469              
470 6 50 33     16 defined $tag && defined $line
471             or return undef;
472              
473 6 50       11 defined $where
474             or $where = -1;
475              
476 6         11 _insert $self, $tag, $line, $where;
477              
478 6         13 $line =~ /^\S+\s(.*)/os;
479 6         16 $1;
480             }
481              
482              
483             sub replace
484 0     0 1 0 { my $self = shift;
485 0 0       0 my $idx = @_ % 2 ? pop @_ : 0;
486              
487 0         0 my ($tag, $line);
488             TAG:
489 0         0 while(@_)
490 0         0 { ($tag,$line) = _fmt_line $self, splice(@_,0,2);
491              
492 0 0 0     0 defined $tag && defined $line
493             or return undef;
494              
495 0         0 my $field = $self->{mail_hdr_hash}{$tag};
496 0 0 0     0 if($field && defined $field->[$idx])
497 0         0 { ${$field->[$idx]} = $line }
  0         0  
498 0         0 else { _insert $self, $tag, $line, -1 }
499             }
500              
501 0         0 $line =~ /^\S+\s*(.*)/os;
502 0         0 $1;
503             }
504              
505              
506             sub combine
507 0     0 1 0 { my $self = shift;
508 0         0 my $tag = _tag_case shift;
509 0   0     0 my $with = shift || ' ';
510              
511 0 0 0     0 $tag =~ /^From /io && $self->{mail_hdr_mail_from} ne 'KEEP'
512             and return _error "unadorned 'From ' ignored";
513              
514 0 0       0 my $def = $self->{mail_hdr_hash}{$tag}
515             or return undef;
516              
517 0 0       0 return $def->[0]
518             if @$def <= 1;
519              
520 0         0 my @lines = $self->get($tag);
521 0         0 chomp @lines;
522              
523 0         0 my $line = (_fmt_line $self, $tag, join($with,@lines), 1)[1];
524              
525 0         0 $self->{mail_hdr_hash}{$tag} = [ \$line ];
526 0         0 $line;
527             }
528              
529              
530             sub get
531 26     26 1 144 { my $self = shift;
532 26         35 my $tag = _tag_case shift;
533 26         33 my $idx = shift;
534              
535 26 100       50 my $def = $self->{mail_hdr_hash}{$tag}
536             or return ();
537              
538 23         26 my $l = length $tag;
539 23 50       39 $l += 1 if $tag !~ / $/o;
540              
541 23 100 100     49 if(defined $idx || !wantarray)
542 20   100     41 { $idx ||= 0;
543 20 50       30 defined $def->[$idx] or return undef;
544 20         20 my $val = ${$def->[$idx]};
  20         26  
545 20 50       31 defined $val or return undef;
546              
547 20         29 $val = substr $val, $l;
548 20         44 $val =~ s/^\s+//;
549 20         49 return $val;
550             }
551              
552 3         5 map { my $tmp = substr $$_,$l; $tmp =~ s/^\s+//; $tmp } @$def;
  6         9  
  6         15  
  6         18  
553             }
554              
555              
556              
557             sub count
558 0     0 1 0 { my $self = shift;
559 0         0 my $tag = _tag_case shift;
560 0         0 my $def = $self->{mail_hdr_hash}{$tag};
561 0 0       0 defined $def ? scalar(@$def) : 0;
562             }
563              
564              
565              
566             sub delete
567 2     2 1 2 { my $self = shift;
568 2         10 my $tag = _tag_case shift;
569 2         5 my $idx = shift;
570 2         4 my @val;
571              
572 2 50       8 if(my $def = $self->{mail_hdr_hash}{$tag})
573 0         0 { my $l = length $tag;
574 0 0       0 $l += 2 if $tag !~ / $/;
575              
576 0 0       0 if(defined $idx)
577 0 0       0 { if(defined $def->[$idx])
578 0         0 { push @val, substr ${$def->[$idx]}, $l;
  0         0  
579 0         0 undef ${$def->[$idx]};
  0         0  
580             }
581             }
582             else
583 0         0 { @val = map {my $x = substr $$_,$l; undef $$_; $x } @$def;
  0         0  
  0         0  
  0         0  
584             }
585              
586 0         0 _tidy_header($self);
587             }
588              
589 2         5 @val;
590             }
591              
592              
593              
594             sub print
595 1     1 1 4 { my $self = shift;
596 1   50     4 my $fd = shift || \*STDOUT;
597              
598 1         1 foreach my $ln (@{$self->{mail_hdr_list}})
  1         2  
599 6 50       15 { defined $ln or next;
600 6 50       10 print $fd $ln or return 0;
601             }
602              
603 1         2 1;
604             }
605              
606              
607 6     6 1 20 sub as_string { join '', grep {defined} @{shift->{mail_hdr_list}} }
  28         64  
  6         17  
608              
609              
610 0     0 1   sub tags { keys %{shift->{mail_hdr_hash}} }
  0            
611              
612              
613             sub cleanup
614 0     0 1   { my $self = shift;
615 0           my $deleted = 0;
616              
617 0 0         foreach my $key (@_ ? @_ : keys %{$self->{mail_hdr_hash}})
  0            
618 0           { my $fields = $self->{mail_hdr_hash}{$key};
619 0           foreach my $field (@$fields)
620 0 0         { next if $$field =~ /^\S+\s+\S/s;
621 0           undef $$field;
622 0           $deleted++;
623             }
624             }
625              
626 0 0         _tidy_header $self
627             if $deleted;
628              
629 0           $self;
630             }
631              
632             1;