File Coverage

blib/lib/Mail/Message/Field.pm
Criterion Covered Total %
statement 192 213 90.1
branch 85 110 77.2
condition 26 33 78.7
subroutine 39 45 86.6
pod 28 29 96.5
total 370 430 86.0


line stmt bran cond sub pod time code
1             # Copyrights 2001-2023 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 distribution Mail-Message. Meta-POD processed with
6             # OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Mail::Message::Field;
10 53     53   1110 use vars '$VERSION';
  53         107  
  53         4407  
11             $VERSION = '3.013';
12              
13 53     53   340 use base 'Mail::Reporter';
  53         1643  
  53         10869  
14              
15 53     53   376 use strict;
  53         134  
  53         2787  
16 53     53   286 use warnings;
  53         1872  
  53         1438  
17              
18 53     53   273 use Carp;
  53         120  
  53         3237  
19 53     53   27111 use Mail::Address;
  53         132252  
  53         2047  
20 53     53   28251 use Date::Format 'strftime';
  53         431682  
  53         3904  
21 53     53   9446 use IO::Handle;
  53         102578  
  53         10425  
22              
23             our %_structured; # not to be used directly: call isStructured!
24             my $default_wrap_length = 78;
25              
26              
27             use overload
28 457     457   1269 qq("") => sub { $_[0]->unfoldedBody }
29 2 50   2   28 , '0+' => sub { $_[0]->toInt || 0 }
30 3481     3481   8540 , bool => sub {1}
31 150     150   9152 , cmp => sub { $_[0]->unfoldedBody cmp "$_[1]" }
32 10 100   10   213 , '<=>' => sub { $_[2] ? $_[1] <=> $_[0]->toInt : $_[0]->toInt <=> $_[1] }
33 53     53   19282 , fallback => 1;
  53         15358  
  53         1077  
34              
35             #------------------------------------------
36              
37              
38             sub new(@)
39 725     725 1 3627 { my $class = shift;
40 725 100       1694 if($class eq __PACKAGE__) # bootstrap
41 588         3345 { require Mail::Message::Field::Fast;
42 588         1824 return Mail::Message::Field::Fast->new(@_);
43             }
44 137         621 $class->SUPER::new(@_);
45             }
46              
47              
48              
49             #------------------------------------------
50              
51              
52 174     174 1 391 sub length { length shift->folded }
53              
54              
55             BEGIN {
56 53     53   14595 %_structured = map { (lc($_) => 1) }
  1431         172478  
57             qw/To Cc Bcc From Date Reply-To Sender
58             Resent-Date Resent-From Resent-Sender Resent-To Return-Path
59             List-Help List-Post List-Unsubscribe Mailing-List
60             Received References Message-ID In-Reply-To
61             Content-Type Content-Disposition Content-ID
62             Delivered-To
63             MIME-Version
64             Precedence
65             Status/;
66             }
67              
68             sub isStructured(;$)
69 735 50   735 1 2474 { my $name = ref $_[0] ? shift->name : $_[1];
70 735         3085 exists $_structured{lc $name};
71             }
72              
73              
74             sub print(;$)
75 21     21 1 42 { my $self = shift;
76 21   33     50 my $fh = shift || select;
77 21         128 $fh->print(scalar $self->folded);
78             }
79              
80              
81 27     27 0 1784 sub toString(;$) {shift->string(@_)}
82             sub string(;$)
83 93     93 1 11062 { my $self = shift;
84 93 100       350 return $self->folded unless @_;
85              
86 5   33     14 my $wrap = shift || $default_wrap_length;
87 5         19 my $name = $self->Name;
88 5         14 my @lines = $self->fold($name, $self->unfoldedBody, $wrap);
89 5         18 $lines[0] = $name . ':' . $lines[0];
90 5 50       22 wantarray ? @lines : join('', @lines);
91             }
92              
93              
94             sub toDisclose()
95 0     0 1 0 { shift->name !~ m!^(?: (?:x-)?status
96             | (?:resent-)?bcc
97             | Content-Length
98             | x-spam-
99             ) $!x;
100             }
101              
102              
103 137     137 1 334 sub nrLines() { my @l = shift->foldedBody; scalar @l }
  137         495  
104              
105              
106             *size = \&length;
107              
108             #------------------------------------------
109              
110              
111             # attempt to change the case of a tag to that required by RFC822. That
112             # being all characters are lowercase except the first of each
113             # word. Also if the word is an `acronym' then all characters are
114             # uppercase. We, rather arbitrarily, decide that a word is an acronym
115             # if it does not contain a vowel and isn't the well-known 'Cc' or
116             # 'Bcc' headers.
117              
118             my %wf_lookup
119             = qw/mime MIME ldap LDAP soap SOAP swe SWE
120             bcc Bcc cc Cc id ID/;
121              
122             sub wellformedName(;$)
123 27     27 1 4256 { my $thing = shift;
124 27 100       73 my $name = @_ ? shift : $thing->name;
125              
126             join '-',
127 27 100       86 map { $wf_lookup{lc $_} || ( /[aeiouyAEIOUY]/ ? ucfirst lc : uc ) }
  42 100       337  
128             split /\-/, $name, -1;
129             }
130              
131             #------------------------------------------
132              
133              
134 0     0 1 0 sub folded { shift->notImplemented }
135              
136              
137             sub body()
138 703     703 1 6143 { my $self = shift;
139 703         1678 my $body = $self->unfoldedBody;
140 703 100       1946 return $body unless $self->isStructured;
141              
142 496         3696 my ($first) = $body =~ m/^((?:"[^"]*"|'[^']*'|[^;])*)/;
143 496         1167 $first =~ s/\s+$//;
144 496         2229 $first;
145             }
146              
147              
148 0     0 1 0 sub foldedBody { shift->notImplemented }
149              
150              
151 0     0 1 0 sub unfoldedBody { shift->notImplemented }
152              
153              
154             sub stripCFWS($)
155 55     55 1 13839 { my $thing = shift;
156              
157             # get (folded) data
158 55 100       140 my $string = @_ ? shift : $thing->foldedBody;
159              
160             # remove comments
161 55         104 my $r = '';
162 55         75 my $in_dquotes = 0;
163 55         75 my $open_paren = 0;
164              
165 55         344 my @s = split m/([()"])/, $string;
166 55         130 while(@s)
167 472         686 { my $s = shift @s;
168              
169 472 100 100     2130 if(CORE::length($r)&& substr($r, -1) eq "\\") { $r .= $s }
  4 100 100     9  
    100 100        
    100          
    100          
170 40         58 elsif($s eq '"') { $in_dquotes = not $in_dquotes; $r .= $s }
  40         73  
171 80         144 elsif($s eq '(' && !$in_dquotes) { $open_paren++ }
172 80         145 elsif($s eq ')' && !$in_dquotes) { $open_paren-- }
173             elsif($open_paren) {} # in comment
174 156         336 else { $r .= $s }
175             }
176              
177             # beautify and unfold at the same time
178 55         111 for($r)
179 55         278 { s/\s+/ /gs;
180 55         194 s/\s+$//;
181 55         156 s/^\s+//;
182             }
183              
184 55         184 $r;
185             }
186              
187             #------------------------------------------
188              
189              
190             sub comment(;$)
191 31     31 1 74 { my $self = shift;
192 31 100       74 return undef unless $self->isStructured;
193              
194 27         82 my $body = $self->unfoldedBody;
195              
196 27 100       79 if(@_)
197 2         5 { my $comment = shift;
198 2         8 $body =~ s/\s*\;.*//;
199 2 50 33     16 $body .= "; $comment" if defined $comment && CORE::length($comment);
200 2         10 $self->unfoldedBody($body);
201 2         7 return $comment;
202             }
203            
204 25 100       178 $body =~ s/.*?\;\s*// ? $body : '';
205             }
206              
207 10     10 1 26 sub content() { shift->unfoldedBody } # Compatibility
208              
209              
210             sub attribute($;$)
211 396     396 1 991 { my ($self, $attr) = (shift, shift);
212 396         977 my $body = $self->unfoldedBody;
213              
214 396 100       1040 unless(@_)
215             { # only get a value
216 276 100       6291 if($body =~ m/\b$attr\s*\=\s*
217             ( "( (?> [^\\"]+|\\. )* )"
218             | ([^";\s]*)
219             )/xi)
220 182         675 { (my $val = $+) =~ s/\\(.)/$1/g;
221 182         797 return $val;
222             }
223 94         830 return undef;
224             }
225              
226             # set the value
227 120         237 my $value = shift;
228 120 50       314 unless(defined $value) # remove attribute
229 0         0 { for($body)
230 0 0       0 { s/\b$attr\s*=\s*"(?>[^\\"]|\\.)*"//i
231             or s/\b$attr\s*=\s*[;\s]*//i;
232             }
233 0         0 $self->unfoldedBody($body);
234 0         0 return undef;
235             }
236              
237 120         303 (my $quoted = $value) =~ s/(["\\])/\\$1/g;
238              
239 120         231 for($body)
240             { s/\b$attr\s*=\s*"(?>[^\\"]|\\.){0,1000}"/$attr="$quoted"/i
241             or s/\b$attr\s*=\s*[^;\s]*/$attr="$quoted"/i
242 120 100 100     3459 or do { $_ .= qq(; $attr="$quoted") }
  93         473  
243             }
244              
245 120         437 $self->unfoldedBody($body);
246 120         291 $value;
247             }
248              
249             #------------------------------------------
250              
251              
252             sub attributes()
253 1     1 1 4 { my $self = shift;
254 1         6 my $body = $self->unfoldedBody;
255              
256 1         3 my @attrs;
257 1         14 while($body =~ m/\b(\w+)\s*\=\s*
258             ( "( (?: [^"]|\\" )* )"
259             | '( (?: [^']|\\' )* )'
260             | ([^;\s]*)
261             )
262             /xig)
263 3         19 { push @attrs, $1 => $+;
264             }
265              
266 1         8 @attrs;
267             }
268              
269             #------------------------------------------
270              
271              
272             sub toInt()
273 12     12 1 26 { my $self = shift;
274 12 50       27 return $1 if $self->body =~ m/^\s*(\d+)\s*$/;
275              
276 0         0 $self->log(WARNING => "Field content is not numerical: ". $self->toString);
277              
278 0         0 return undef;
279             }
280              
281             #------------------------------------------
282              
283              
284             my @weekday = qw/Sun Mon Tue Wed Thu Fri Sat Sun/;
285             my @month = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/;
286              
287             sub toDate(@)
288 16     16 1 34 { my $class = shift;
289 16 0       506 my @time = @_== 0 ? localtime() : @_==1 ? localtime(shift) : @_;
    50          
290 16         113 my $format = "$weekday[$time[6]], %d $month[$time[4]] %Y %H:%M:%S %z";
291 16         81 my $time = strftime $format, @time;
292              
293             # for C libs which do not (GNU compliantly) support %z
294 16         3488 $time =~ s/ (\%z|[A-Za-z ]+)$/_tz_offset($1)/e;
  0         0  
295              
296 16         89 $time;
297             }
298              
299             sub _tz_offset($)
300 0     0   0 { my $zone = shift;
301 0         0 require Time::Zone;
302              
303 0 0       0 my $diff = $zone eq '%z' ? Time::Zone::tz_local_offset()
304             : Time::Zone::tz_offset($zone);
305 0         0 my $minutes = int((abs($diff)+0.01) / 60); # float rounding errors
306 0         0 my $hours = int(($minutes+0.01) / 60);
307 0         0 $minutes -= $hours * 60;
308 0 0       0 sprintf( ($diff < 0 ? " -%02d%02d" : " +%02d%02d"), $hours, $minutes);
309             }
310              
311             #------------------------------------------
312              
313              
314 18     18 1 53 sub addresses() { Mail::Address->parse(shift->unfoldedBody) }
315              
316             #------------------------------------------
317              
318              
319             sub study()
320 73     73 1 148 { my $self = shift;
321 73         379 require Mail::Message::Field::Full;
322 73         272 Mail::Message::Field::Full->new(scalar $self->folded);
323             }
324              
325             #------------------------------------------
326              
327              
328             sub dateToTimestamp($)
329 1     1 1 7 { my $string = $_[0]->stripCFWS($_[1]);
330              
331             # in RFC822, FWSes can appear within the time.
332 1         13 $string =~ s/(\d\d)\s*\:\s*(\d\d)\s*\:\s*(\d\d)/$1:$2:$3/;
333              
334 1         498 require Date::Parse;
335 1         2867 Date::Parse::str2time($string, 'GMT');
336             }
337              
338              
339             #------------------------------------------
340              
341              
342             #=notice Empty field: $name
343             #Empty fields are not allowed, however sometimes found in messages constructed
344             #by broken applications. You probably want to ignore this message unless you
345             #wrote this broken application yourself.
346              
347             sub consume($;$)
348 939     939 1 1501 { my $self = shift;
349 939 100       2435 my ($name, $body) = defined $_[1] ? @_ : split(/\s*\:\s*/, (shift), 2);
350              
351 939 50       3061 Mail::Reporter->log(WARNING => "Illegal character in field name $name")
352             if $name =~ m/[^\041-\071\073-\176]/;
353              
354             #
355             # Compose the body.
356             #
357              
358 939 100       2878 if(ref $body) # Objects or array
    100          
359 28 50       99 { my $flat = $self->stringifyData($body) or return ();
360 28         93 $body = $self->fold($name, $flat);
361             }
362             elsif($body !~ s/\n+$/\n/g) # Added by user...
363 832         2083 { $body = $self->fold($name, $body);
364             }
365             else # Created by parser
366             { # correct erroneous wrap-seperators (dos files under UNIX)
367 79         270 $body =~ s/[\012\015]+/\n/g;
368 79         244 $body =~ s/^[ \t]*/ /; # start with one blank, folding kept unchanged
369              
370 79 50       240 $self->log(NOTICE => "Empty field: $name")
371             if $body eq " \n";
372             }
373              
374 939         3062 ($name, $body);
375             }
376              
377             #------------------------------------------
378              
379              
380             sub stringifyData($)
381 28     28 1 63 { my ($self, $arg) = (shift, shift);
382 28         44 my @addr;
383 28 100       95 foreach my $obj (ref $arg eq 'ARRAY' ? @$arg : ($arg))
384 33 50       82 { next unless defined $obj;
385              
386 33 100       90 if(!ref $obj) { push @addr, $obj; next }
  1         2  
  1         5  
387 32 100       165 if($obj->isa('Mail::Address')) { push @addr, $obj->format; next }
  19         90  
  19         700  
388              
389 13 100 100     121 if($obj->isa('Mail::Identity') || $obj->isa('User::Identity'))
    100          
    50          
390 7         591 { require Mail::Message::Field::Address;
391 7         29 push @addr, Mail::Message::Field::Address->coerce($obj)->string;
392             }
393             elsif($obj->isa('User::Identity::Collection::Emails'))
394 1 50       5 { my @roles = $obj->roles or next;
395 1         49 require Mail::Message::Field::AddrGroup;
396 1         10 my $group = Mail::Message::Field::AddrGroup->coerce($obj);
397 1 50       50 push @addr, $group->string if $group;
398             }
399             elsif($obj->isa('Mail::Message::Field'))
400             {
401 5         21 my $folded = join ' ', $obj->foldedBody;
402 5         28 $folded =~ s/^ //;
403 5         21 $folded =~ s/\n\z//;
404 5         14 push @addr, $folded;
405             }
406             else
407 0         0 { push @addr, "$obj"; # any other object is stringified
408             }
409             }
410              
411 28 50       142 @addr ? join(', ',@addr) : undef;
412             }
413              
414             #------------------------------------------
415              
416              
417             sub setWrapLength(;$)
418 367     367 1 553 { my $self = shift;
419              
420 367 100       842 $self->foldedBody(scalar $self->fold($self->Name, $self->unfoldedBody, @_))
421             if @_;
422              
423 367         659 $self;
424             }
425              
426             #------------------------------------------
427              
428              
429             sub defaultWrapLength(;$)
430 0     0 1 0 { my $self = shift;
431 0 0       0 @_ ? ($default_wrap_length = shift) : $default_wrap_length;
432             }
433              
434             #------------------------------------------
435              
436              
437             sub fold($$;$)
438 1072     1072 1 1696 { my $thing = shift;
439 1072         1537 my $name = shift;
440 1072         1595 my $line = shift;
441 1072   66     3175 my $wrap = shift || $default_wrap_length;
442 1072 100       2192 defined $line or $line = '';
443              
444 1072         1885 $line =~ s/\n(\s)/$1/gms; # Remove accidental folding
445 1072 100       2218 return " \n" unless CORE::length($line); # empty field
446              
447 1069         1928 my @folded;
448 1069         1465 while(1)
449 1124 100       2583 { my $max = $wrap - (@folded ? 1 : CORE::length($name) + 2);
450 1124         1977 my $min = $max >> 2;
451 1124 100       2523 last if CORE::length($line) < $max;
452              
453 55 100 100     1486 $line =~ s/^ ( .{$min,$max} # $max to 30 chars
      100        
454             [;,] # followed at a ; or ,
455             )[ \t] # and then a WSP
456             //x
457             || $line =~ s/^ ( .{$min,$max} ) # $max to 30 chars
458             [ \t] # followed by a WSP
459             //x
460             || $line =~ s/^ ( .{$max,}? ) # longer, but minimal chars
461             [ \t] # followed by a WSP
462             //x
463             || $line =~ s/^ (.*) //x; # everything
464              
465 55         250 push @folded, " $1\n";
466             }
467              
468 1069 100       3708 push @folded, " $line\n" if CORE::length($line);
469 1069 100       4258 wantarray ? @folded : join('', @folded);
470             }
471              
472              
473             sub unfold($)
474 1801     1801 1 2973 { my $string = $_[1];
475 1801         3121 for($string)
476 1801         3527 { s/\r?\n(\s)/$1/gs; # remove FWS
477 1801         8371 s/\r?\n/ /gs;
478 1801         5520 s/^\s+//;
479 1801         6420 s/\s+$//;
480             }
481 1801         6179 $string;
482             }
483              
484             #------------------------------------------
485              
486              
487             1;