File Coverage

blib/lib/Mail/Header.pm
Criterion Covered Total %
statement 193 307 62.8
branch 69 168 41.0
condition 36 82 43.9
subroutine 23 30 76.6
pod 22 22 100.0
total 343 609 56.3


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