File Coverage

blib/lib/Mail/Message/Field.pm
Criterion Covered Total %
statement 185 206 89.8
branch 81 106 76.4
condition 30 40 75.0
subroutine 40 46 86.9
pod 28 29 96.5
total 364 427 85.2


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Mail-Message version 4.04.
2             # The POD got stripped from this file by OODoc version 3.06.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2001-2026 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package Mail::Message::Field;{
13             our $VERSION = '4.04';
14             }
15              
16 57     57   24701 use parent 'Mail::Reporter';
  57         131  
  57         552  
17              
18 57     57   4150 use strict;
  57         133  
  57         1793  
19 57     57   325 use warnings;
  57         144  
  57         5241  
20              
21 57     57   366 use Log::Report 'mail-message', import => [ qw/__x error info panic warning/ ];
  57         211  
  57         636  
22              
23 57     57   43298 use Mail::Address ();
  57         236432  
  57         2037  
24 57     57   12058 use IO::Handle ();
  57         146275  
  57         1921  
25 57     57   31675 use Date::Format qw/strftime/;
  57         272840  
  57         5205  
26 57     57   557 use Scalar::Util qw/blessed/;
  57         192  
  57         3196  
27 57     57   39990 use Hash::Case::Preserve ();
  57         150937  
  57         11136  
28              
29             our %_structured; # not to be used directly: call isStructured!
30             my $default_wrap_length = 78;
31              
32              
33             use overload
34 883     883   2759 qq("") => sub { $_[0]->unfoldedBody },
35 15 50   15   232 '0+' => sub { $_[0]->toInt || 0 },
36 3203     3203   8728 bool => sub {1},
37 238     238   20496 cmp => sub { $_[0]->unfoldedBody cmp "$_[1]" },
38 10 100   10   461 '<=>' => sub { $_[2] ? $_[1] <=> $_[0]->toInt : $_[0]->toInt <=> $_[1] },
39 57     57   588 fallback => 1;
  57         201  
  57         1225  
40              
41             #--------------------
42              
43             sub new(@)
44 1078     1078 1 8973 { my $class = shift;
45 1078 100       2903 if($class eq __PACKAGE__) # bootstrap
46 895         6288 { require Mail::Message::Field::Fast;
47 895         3244 return Mail::Message::Field::Fast->new(@_);
48             }
49 183         985 $class->SUPER::new(@_);
50             }
51              
52              
53             #--------------------
54              
55             # attempt to change the case of a tag to that required by RFC822. That
56             # being all characters are lowercase except the first of each
57             # word. Also if the word is an `acronym' then all characters are
58             # uppercase. We, rather arbitrarily, decide that a word is an acronym
59             # if it does not contain a vowel and isn't the well-known 'Cc' or
60             # 'Bcc' headers.
61              
62             my %wf_lookup = qw/mime MIME ldap LDAP soap SOAP swe SWE bcc Bcc cc Cc id ID/;
63              
64             sub wellformedName(;$)
65 27     27 1 8305 { my $thing = shift;
66 27 100       83 my $name = @_ ? shift : $thing->name;
67              
68             join '-',
69 27 100       94 map { $wf_lookup{lc $_} || ( /[aeiouyAEIOUY]/ ? ucfirst lc : uc ) }
  42 100       410  
70             split /\-/, $name, -1;
71             }
72              
73              
74 0     0 1 0 sub folded { $_[0]->notImplemented }
75              
76              
77             sub body()
78 786     786 1 24751 { my $self = shift;
79 786         2285 my $body = $self->unfoldedBody;
80 786 100       2368 $self->isStructured or return $body;
81              
82 565         18255 my ($first) = $body =~ m/^((?:"[^"]*"|'[^']*'|[^;])*)/;
83 565         4129 $first =~ s/\s+$//r;
84             }
85              
86              
87 0     0 1 0 sub foldedBody { $_[0]->notImplemented }
88              
89              
90 0     0 1 0 sub unfoldedBody { $_[0]->notImplemented }
91              
92             #--------------------
93              
94 172     172 1 429 sub length { length $_[0]->folded }
95              
96              
97             BEGIN {
98 57     57   303601 %_structured = map +(lc($_) => 1), qw/
99             To Cc Bcc From Date Reply-To Sender
100             Resent-Date Resent-From Resent-Sender Resent-To Return-Path
101             List-Help List-Post List-Unsubscribe Mailing-List
102             Received References Message-ID In-Reply-To Delivered-To
103             Content-Type Content-Disposition Content-ID
104             MIME-Version Precedence Status
105             /;
106             }
107              
108             sub isStructured(;$)
109 828 50 33 828 1 4023 { my $name = $_[1] // (blessed $_[0] ? $_[0]->name : panic);
110 828         5326 exists $_structured{lc $name};
111             }
112              
113              
114             sub print(;$)
115 21     21 1 45 { my $self = shift;
116 21   33     46 my $fh = shift || select;
117 21         174 $fh->print(scalar $self->folded);
118             }
119              
120              
121 27     27 0 2878 sub toString(;$) { shift->string(@_) }
122              
123             sub string(;$)
124 93     93 1 16716 { my $self = shift;
125 93 100       435 return $self->folded unless @_;
126              
127 5   33     16 my $wrap = shift || $default_wrap_length;
128 5         32 my $name = $self->Name;
129 5         18 my @lines = $self->fold($name, $self->unfoldedBody, $wrap);
130 5         20 $lines[0] = $name . ':' . $lines[0];
131 5 50       25 wantarray ? @lines : join('', @lines);
132             }
133              
134              
135             sub toDisclose()
136 0     0 1 0 { $_[0]->name !~ m! ^
137             (?: (?:x-)?status
138             | (?:resent-)?bcc
139             | content-length
140             | x-spam-
141             ) $ !x;
142             }
143              
144              
145 135     135 1 399 sub nrLines() { my @l = $_[0]->foldedBody; scalar @l }
  135         609  
146              
147              
148             *size = \&length;
149              
150             #--------------------
151              
152             sub stripCFWS($)
153 55     55 1 197410 { my $thing = shift;
154              
155             # get (folded) data
156 55 100       138 my $string = @_ ? shift : $thing->foldedBody;
157              
158             # remove comments
159 55         97 my $r = '';
160 55         61 my $in_dquotes = 0;
161 55         63 my $open_paren = 0;
162              
163 55         322 my @s = split m/([()"])/, $string;
164 55         117 while(@s)
165 472         512 { my $s = shift @s;
166              
167 472 100 100     1675 if(CORE::length($r)&& substr($r, -1) eq "\\") { $r .= $s }
  4 100 100     10  
    100 100        
    100          
    100          
168 40         47 elsif($s eq '"') { $in_dquotes = not $in_dquotes; $r .= $s }
  40         57  
169 80         112 elsif($s eq '(' && !$in_dquotes) { $open_paren++ }
170 80         109 elsif($s eq ')' && !$in_dquotes) { $open_paren-- }
171             elsif($open_paren) {} # in comment
172 156         257 else { $r .= $s }
173             }
174              
175             # beautify and unfold at the same time
176 55         545 $r =~ s/\s+/ /grs =~ s/\s+$//r =~ s/^\s+//r;
177             }
178              
179              
180             sub comment(;$)
181 41     41 1 3569 { my $self = shift;
182 41 100       116 $self->isStructured or return undef;
183              
184 37         109 my $body = $self->unfoldedBody;
185              
186 37 100       148 if(@_)
187 2         5 { my $comment = shift;
188 2         6 $body =~ s/\s*\;.*//;
189 2 50 33     16 $body .= "; $comment" if defined $comment && CORE::length($comment);
190 2         10 $self->unfoldedBody($body);
191 2         6 return $comment;
192             }
193              
194 35 100       266 $body =~ s/.*?\;\s*// ? $body : '';
195             }
196              
197 10     10 1 28 sub content() { shift->unfoldedBody } # Compatibility
198              
199              
200             sub attribute($;$)
201 477     477 1 7847 { my ($self, $attr) = (shift, shift);
202              
203             # Although each attribute can appear only once, some (intentionally)
204             # broken messages do repeat them. See github issue 20. Apple Mail and
205             # Outlook will take the last of the repeated in such case, so we do that
206             # as well.
207 477         1490 tie my %attrs, 'Hash::Case::Preserve', [ $self->attributes ];
208 477 100       30300 @_ or return $attrs{$attr};
209              
210             # set the value
211 153         334 my $value = shift;
212 153         498 my $body = $self->unfoldedBody;
213              
214 153 50       528 unless(defined $value) # remove attribute
215 0         0 { for($body)
216 0 0       0 { s/\b$attr\s*=\s*"(?>[^\\"]|\\.)*"//i or s/\b$attr\s*=\s*[;\s]*//i;
217             }
218 0         0 $self->unfoldedBody($body);
219 0         0 return undef;
220             }
221              
222 153         502 my $quoted = $value =~ s/(["\\])/\\$1/gr;
223              
224 153         373 for($body)
225             { s/\b$attr\s*=\s*"(?>[^\\"]|\\.){0,1000}"/$attr="$quoted"/i
226             or s/\b$attr\s*=\s*[^;\s]*/$attr="$quoted"/i
227 153 100 100     17559 or do { $_ .= qq(; $attr="$quoted") }
  127         617  
228             }
229              
230 153         679 $self->unfoldedBody($body);
231 153         1096 $value;
232             }
233              
234              
235             sub attributes()
236 479     479 1 1748 { my $self = shift;
237 479         1434 my $body = $self->unfoldedBody;
238              
239 479         850 my @attrs;
240 479         4857 while($body =~ m/ \b(\w+)\s*\=\s* ( "( (?: [^"]|\\" )* )" | '( (?: [^']|\\' )* )' | ([^;\s]*) ) /xig)
241 246         1555 { push @attrs, $1 => $+;
242             }
243              
244 479         4693 @attrs;
245             }
246              
247              
248             sub toInt()
249 25     25 1 50 { my $self = shift;
250 25 50       69 $self->body =~ m/^\s*(\d+)\s*$/
251             and return $1;
252              
253 0         0 warning __x"field content is not numerical: {content}", content => $self->toString;
254 0         0 undef;
255             }
256              
257              
258             my @weekday = qw/Sun Mon Tue Wed Thu Fri Sat Sun/;
259             my @month = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/;
260              
261             sub toDate(@)
262 16     16 1 35 { my $class = shift;
263 16 0       931 my @time = @_== 0 ? localtime() : @_==1 ? localtime(shift) : @_;
    50          
264 16         81 my $format = "$weekday[$time[6]], %d $month[$time[4]] %Y %H:%M:%S %z";
265 16         147 my $time = strftime $format, @time;
266              
267             # for C libs which do not (GNU compliantly) support %z
268 16         4690 $time =~ s/ (\%z|[A-Za-z ]+)$/_tz_offset($1)/re;
  0         0  
269             }
270              
271             sub _tz_offset($)
272 0     0   0 { my $zone = shift;
273 0         0 require Time::Zone;
274              
275 0 0       0 my $diff = $zone eq '%z' ? Time::Zone::tz_local_offset() : Time::Zone::tz_offset($zone);
276 0         0 my $minutes = int((abs($diff)+0.01) / 60); # float rounding errors
277 0         0 my $hours = int(($minutes+0.01) / 60);
278 0         0 $minutes -= $hours * 60;
279 0 0       0 sprintf +($diff < 0 ? " -%02d%02d" : " +%02d%02d"), $hours, $minutes;
280             }
281              
282              
283 18     18 1 62 sub addresses() { Mail::Address->parse(shift->unfoldedBody) }
284              
285              
286             sub study()
287 93     93 1 456 { my $self = shift;
288 93         733 require Mail::Message::Field::Full;
289 93         448 Mail::Message::Field::Full->new(scalar $self->folded);
290             }
291              
292              
293             sub dateToTimestamp($)
294 1     1 1 6 { my $string = $_[0]->stripCFWS($_[1]);
295              
296             # in RFC822, FWSes can appear within the time.
297 1         16 $string =~ s/(\d\d)\s*\:\s*(\d\d)\s*\:\s*(\d\d)/$1:$2:$3/;
298              
299 1         9 require Date::Parse;
300 1         7 Date::Parse::str2time($string, 'GMT');
301             }
302              
303             #--------------------
304              
305             sub consume($;$)
306 1977     1977 1 3407 { my $self = shift;
307 1977 100       5729 my ($name, $body) = defined $_[1] ? @_ : split(/\s*\:\s*/, (shift), 2);
308              
309 1977 50       8197 $name !~ m/[^\041-\071\073-\176]/
310             or warning __x"illegal character in field name '{name}'.", name => $name;
311 1977 50       5141 panic $name if $name =~ m/[^\041-\071\073-\176]/;
312              
313             #
314             # Compose the body.
315             #
316              
317 1977 100       8785 if(ref $body) # Objects or array
    100          
318 28   50     106 { my $flat = $self->stringifyData($body) // return ();
319 28         103 $body = $self->fold($name, $flat);
320             }
321             elsif($body !~ s/\n+$/\n/g) # Added by user...
322 1144         3258 { $body = $self->fold($name, $body);
323             }
324             else # Created by parser
325             { # correct erroneous wrap-seperators (dos files under UNIX)
326 805         4927 $body =~ s/[\012\015]+/\n/g;
327 805         2789 $body =~ s/^[ \t]*/ /; # start with one blank, folding kept unchanged
328             }
329              
330 1977         7269 ($name, $body);
331             }
332              
333              
334             sub stringifyData($)
335 28     28 1 67 { my ($self, $arg) = (shift, shift);
336 28         53 my @addr;
337 28 100       110 foreach my $obj (ref $arg eq 'ARRAY' ? @$arg : ($arg))
338 33 50       92 { defined $obj or next;
339              
340 33 100       83 if(!ref $obj) { push @addr, $obj; next }
  1         3  
  1         4  
341 32 100       606 if($obj->isa('Mail::Address')) { push @addr, $obj->format; next }
  19         77  
  19         966  
342              
343 13 100 100     170 if($obj->isa('Mail::Identity') || $obj->isa('User::Identity'))
    100          
    50          
344 7         770 { require Mail::Message::Field::Address;
345 7         31 push @addr, Mail::Message::Field::Address->coerce($obj)->string;
346             }
347             elsif($obj->isa('User::Identity::Collection::Emails'))
348 1 50       6 { my @roles = $obj->roles or next;
349 1         61 require Mail::Message::Field::AddrGroup;
350 1         8 my $group = Mail::Message::Field::AddrGroup->coerce($obj);
351 1 50       105 push @addr, $group->string if $group;
352             }
353             elsif($obj->isa('Mail::Message::Field'))
354 5         21 { my $folded = join ' ', $obj->foldedBody;
355 5         48 push @addr, $folded =~ s/^ //r =~ s/\n\z//r;
356             }
357             else
358 0         0 { push @addr, "$obj"; # any other object is stringified
359             }
360             }
361              
362 28 50       141 @addr ? join(', ',@addr) : undef;
363             }
364              
365              
366             sub setWrapLength(;$)
367 368     368 1 582 { my $self = shift;
368              
369 368 100       1587 $self->foldedBody(scalar $self->fold($self->Name, $self->unfoldedBody, $_[0]))
370             if @_;
371              
372 368         634 $self;
373             }
374              
375              
376             sub defaultWrapLength(;$)
377 0     0 1 0 { my $self = shift;
378 0 0       0 @_ ? ($default_wrap_length = shift) : $default_wrap_length;
379             }
380              
381              
382             sub fold($$;$)
383 1443     1443 1 2557 { my $thing = shift;
384 1443         2319 my $name = shift;
385 1443         2503 my $line = shift;
386 1443   66     6593 my $wrap = shift || $default_wrap_length;
387 1443   100     3349 $line //= '';
388              
389 1443         2910 $line =~ s/\n(\s)/$1/gms; # Remove accidental folding
390 1443 100       3520 CORE::length($line) or return " \n"; # empty field
391              
392 1440         2440 my $lname = CORE::length($name);
393 1440 50       3430 $lname <= $wrap -5 # Cannot find a real limit in the spec
394             or error __x"field name too long (max {count}), in '{name}'.", count => $wrap - 5, name => $name;
395              
396 1440         5804 my @folded;
397 1440         3980 while(1)
398 1495 100       3765 { my $max = $wrap - (@folded ? 1 : $lname + 2);
399 1495         2791 my $min = $max >> 2;
400 1495 100       4094 last if CORE::length($line) < $max;
401              
402 55 100 100     3264 $line =~ s/^ ( .{$min,$max} # $max to 30 chars
      100        
403             [;,] # followed at a ; or ,
404             )[ \t] # and then a WSP
405             //x
406             || $line =~ s/^ ( .{$min,$max} ) # $max to 30 chars
407             [ \t] # followed by a WSP
408             //x
409             || $line =~ s/^ ( .{$max,}? ) # longer, but minimal chars
410             [ \t] # followed by a WSP
411             //x
412             || $line =~ s/^ (.*) //x; # everything
413              
414 55         285 push @folded, " $1\n";
415             }
416              
417 1440 100       4987 push @folded, " $line\n" if CORE::length($line);
418 1440 100       7013 wantarray ? @folded : join('', @folded);
419             }
420              
421              
422             sub unfold($)
423 2897     2897 1 5154 { my $string = $_[1];
424 2897         5703 for($string)
425 2897         7343 { s/\r?\n(\s)/$1/gs; # remove FWS
426 2897         16838 s/\r?\n/ /gs;
427 2897         13384 s/^\s+//;
428 2897         12790 s/\s+$//;
429             }
430 2897         14094 $string;
431             }
432              
433             #--------------------
434              
435             1;