File Coverage

blib/lib/Mail/Message/Field/Full.pm
Criterion Covered Total %
statement 214 223 95.9
branch 87 98 88.7
condition 20 22 90.9
subroutine 38 42 90.4
pod 19 20 95.0
total 378 405 93.3


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::Full;{
13             our $VERSION = '4.04';
14             }
15              
16 28     28   138585 use parent 'Mail::Message::Field';
  28         353  
  28         288  
17              
18 28     28   2255 use strict;
  28         73  
  28         820  
19 28     28   139 use warnings;
  28         53  
  28         1680  
20 28     28   170 use utf8;
  28         84  
  28         290  
21              
22 28     28   1584 use Log::Report 'mail-message', import => [ qw/__x error warning/ ];
  28         72  
  28         266  
23              
24 28     28   6102 use Encode ();
  28         74  
  28         647  
25 28     28   17082 use MIME::QuotedPrint ();
  28         45930  
  28         1254  
26 28     28   225 use Storable qw/dclone/;
  28         63  
  28         2937  
27              
28 28     28   15463 use Mail::Message::Field::Addresses ();
  28         119  
  28         1223  
29 28     28   18493 use Mail::Message::Field::AuthResults ();
  28         313  
  28         1409  
30             #use Mail::Message::Field::AuthRecChain ();
31 28     28   17222 use Mail::Message::Field::Date ();
  28         140  
  28         1154  
32 28     28   15192 use Mail::Message::Field::DKIM ();
  28         125  
  28         1068  
33 28     28   210 use Mail::Message::Field::Structured ();
  28         97  
  28         559  
34 28     28   15894 use Mail::Message::Field::Unstructured ();
  28         100  
  28         1191  
35 28     28   15675 use Mail::Message::Field::URIs ();
  28         139  
  28         2504  
36              
37             my $atext = q[a-zA-Z0-9!#\$%&'*+\-\/=?^_`{|}~]; # from RFC5322
38             my $utf8_atext = q[\p{Alnum}!#\$%&'*+\-\/=?^_`{|}~]; # from RFC5335
39             my $atext_ill = q/\[\]/; # illegal, but still used (esp spam)
40              
41             #--------------------
42              
43 28     28   235 use overload '""' => sub { shift->decodedBody };
  28     89   66  
  28         349  
  89         10246  
44              
45             #--------------------
46              
47             my %implementation;
48              
49             BEGIN {
50 28     28   7614 $implementation{$_} = 'Addresses' for qw/from to sender cc bcc reply-to envelope-to resent-from resent-to
51             resent-cc resent-bcc resent-reply-to resent-sender x-beenthere errors-to mail-follow-up x-loop
52             delivered-to original-sender x-original-sender/;
53 28         281 $implementation{$_} = 'URIs' for qw/list-help list-post list-subscribe list-unsubscribe list-archive list-owner/;
54 28         246 $implementation{$_} = 'Structured' for qw/content-disposition content-type content-id/;
55 28         122 $implementation{$_} = 'Date' for qw/date resent-date/;
56 28         112 $implementation{$_} = 'AuthResults' for qw/authentication-results/;
57 28         139176 $implementation{$_} = 'DKIM' for qw/dkim-signature/;
58             # $implementation{$_} = 'AuthRecChain' for qw/arc-authentication-results arc-message-signature arc-seal/;
59             }
60              
61             sub new($;$$@)
62 165     165 1 2549382 { my $class = shift;
63 165         407 my $name = shift;
64 165 100       702 my $body = @_ % 2 ? shift : undef;
65 165         444 my %args = @_;
66              
67 165 50       663 $body = delete $args{body} if defined $args{body};
68 165 100       505 unless(defined $body)
69 136         2072 { (my $n, $body) = split /\s*\:\s*/s, $name, 2;
70 136 100       582 $name = $n if defined $body;
71             }
72              
73 165 100       884 $class eq __PACKAGE__
74             or return $class->SUPER::new(%args, name => $name, body => $body);
75              
76             # Look for best class to suit this field
77 125   100     794 my $myclass = 'Mail::Message::Field::' . ($implementation{lc $name} || 'Unstructured');
78              
79 125         822 $myclass->SUPER::new(%args, name => $name, body => $body);
80             }
81              
82             sub init($)
83 165     165 0 447 { my ($self, $args) = @_;
84              
85 165         748 $self->SUPER::init($args);
86 165         617 $self->{MMFF_name} = $args->{name};
87 165         360 my $body = $args->{body};
88              
89 165 100 100     1547 if(!defined $body || !length $body || ref $body) { ; } # no body yet
    100 66        
90             elsif(index($body, "\n") >= 0)
91 106         431 { $self->foldedBody($body) } # body is already folded
92 46         206 else { $self->unfoldedBody($body) } # body must be folded
93              
94 165         445 $self;
95             }
96              
97 0     0 1 0 sub clone() { dclone(shift) }
98 173     173 1 9170 sub name() { lc shift->{MMFF_name}}
99 0     0 1 0 sub Name() { $_[0]->{MMFF_name} }
100              
101             sub folded()
102 90     90 1 168 { my $self = shift;
103 90 100       419 wantarray or return $self->{MMFF_name}.':'.$self->foldedBody;
104              
105 1         5 my @lines = $self->foldedBody;
106 1         3 my $first = $self->{MMFF_name}. ':'. shift @lines;
107 1         7 ($first, @lines);
108             }
109              
110             sub unfoldedBody($;$)
111 264     264 1 3730 { my ($self, $body) = (shift, shift);
112              
113 264 100       754 if(defined $body)
114 46         278 { $self->foldedBody(scalar $self->fold($self->{MMFF_name}, $body));
115 46         205 return $body;
116             }
117              
118 218         667 $self->foldedBody =~ s/\r?\n(\s)/$1/gr =~ s/\r?\n/ /gr =~ s/^\s+//r =~ s/\s+$//r;
119             }
120              
121             sub foldedBody($)
122 506     506 1 1165 { my ($self, $body) = @_;
123              
124 506 100       1842 if(@_==2)
    100          
125 152         722 { $self->parse($body);
126 152         830 $body =~ s/^\s*/ /m;
127 152         478 $self->{MMFF_body} = $body;
128             }
129             elsif(defined($body = $self->{MMFF_body})) { ; }
130             else
131             { # Create a new folded body from the parts.
132 58         304 $self->{MMFF_body} = $body = $self->fold($self->{MMFF_name}, $self->produceBody);
133             }
134              
135 506 100       4762 wantarray ? (split /^/, $body) : $body;
136             }
137              
138             #--------------------
139              
140             sub from($@)
141 0     0 1 0 { my ($class, $field) = (shift, shift);
142 0 0       0 defined $field ? $class->new($field->Name, $field->foldedBody, @_) : ();
143             }
144              
145             #--------------------
146              
147             sub decodedBody()
148 90     90 1 1711 { my $self = shift;
149 90         279 $self->decode($self->unfoldedBody, @_);
150             }
151              
152             #--------------------
153              
154             sub createComment($@)
155 38     38 1 29102 { my ($thing, $comment) = (shift, shift);
156              
157 38 100       137 $comment = $thing->encode($comment, @_) if @_; # encoding required...
158              
159             # Correct dangling parenthesis
160 38         93 local $_ = $comment; # work with a copy
161 38         124 s#\\[()]#xx#g; # remove escaped parens
162 38         316 s#[^()]#x#g; # remove other chars
163 38         277 while( s#\(([^()]*)\)#x$1x# ) {;} # remove pairs of parens
164              
165 38         248 substr($comment, CORE::length($_), 0, '\\')
166             while s#[()][^()]*$##; # add escape before remaining parens
167              
168 38         128 $comment =~ s#\\+$##; # backslash at end confuses
169 38         251 "($comment)";
170             }
171              
172              
173             sub createPhrase($)
174 43     43 1 8204 { my $self = shift;
175 43         93 local $_ = shift;
176              
177             # I do not case whether it gets a but sloppy in the header string,
178             # as long as it is functionally correct: no folding inside phrase quotes.
179 43 100       143 return $_ = $self->encode($_, @_, force => 1)
180             if length $_ > 50;
181              
182 41 100       178 $_ = $self->encode($_, @_) if @_; # encoding required...
183              
184 41 100       635 if( m/[^$atext]/ )
185 28         84 { s#\\#\\\\#g;
186 28         92 s#"#\\"#g;
187 28         77 $_ = qq["$_"];
188             }
189              
190 41         166 $_;
191             }
192              
193              
194 0     0 1 0 sub beautify() { $_[0] }
195              
196             #--------------------
197              
198 59     59   279 sub _mime_word($$) { "$_[0]$_[1]?=" }
199 15     15   73 sub _encode_b($) { MIME::Base64::encode_base64(shift, '') }
200              
201             sub _encode_q($) # RFC2047 sections 4.2 and 5
202 668     668   23377 { my $chunk = shift;
203 668         1735 $chunk =~ s#([^a-zA-Z0-9!*+/_ -])#sprintf "=%02X", ord $1#ge;
  454         1464  
204 668         1161 $chunk =~ s#([_\?,"])#sprintf "=%02X", ord $1#ge;
  0         0  
205 668         1348 $chunk =~ s/ /_/g; # special case for =? ?= use
206 668         1441 $chunk;
207             }
208              
209             sub encode($@)
210 69     69 1 1723 { my ($self, $utf8, %args) = @_;
211              
212 69         141 my ($charset, $lang, $encoding);
213              
214 69 100       233 if($charset = $args{charset})
215 28 50       129 { warning __x"illegal character in charset '{name}'.", name => $charset
216             if $charset =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
217             }
218             else
219 41 100       221 { $charset = $utf8 =~ /\P{ASCII}/ ? 'utf8' : 'us-ascii';
220             }
221              
222 69 100       210 if($lang = $args{language})
223 4 50       17 { warning __x"illegal character in language '{name}'.", name => $lang
224             if $lang =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
225             }
226              
227 69 100       170 if($encoding = $args{encoding})
228 11 50       56 { unless($encoding =~ m/^[bBqQ]$/ )
229 0         0 { warning __x"illegal encoding '{name}', using 'q'.", name => $encoding;
230 0         0 $encoding = 'q';
231             }
232             }
233 58         119 else { $encoding = 'q' }
234              
235 69         134 my $name = $args{name};
236 69 100       175 my $lname = defined $name ? length($name)+1 : 0;
237              
238             return $utf8
239             if lc($encoding) eq 'q'
240             && length $utf8 < 70
241 69 100 100     945 && ($utf8 =~ m/\A[\p{IsASCII}]+\z/ms && !$args{force});
      100        
      100        
242              
243 28 100       106 my $pre = '=?'. $charset. ($lang ? '*'.$lang : '') .'?'.$encoding.'?';
244              
245 28         55 my @result;
246 28 100       70 if(lc($encoding) eq 'q')
247 20         43 { my $chunk = '';
248 20         45 my $llen = 73 - length($pre) - $lname;
249              
250 20         128 while(length(my $chr = substr($utf8, 0, 1, '')))
251 668         2307 { $chr = _encode_q Encode::encode($charset, $chr, 0);
252 668 100       1742 if(bytes::length($chunk) + bytes::length($chr) > $llen)
253 24         158 { push @result, _mime_word($pre, $chunk);
254 24         44 $chunk = '';
255 24         48 $llen = 73 - length $pre;
256             }
257 668         10193 $chunk .= $chr;
258             }
259 20 50       88 push @result, _mime_word($pre, $chunk)
260             if length($chunk);
261             }
262             else
263 8         17 { my $chunk = '';
264 8         32 my $llen = int((73 - length($pre) - $lname) / 4) * 3;
265 8         39 while(length(my $chr = substr($utf8, 0, 1, '')))
266 310         881 { my $chr = Encode::encode($charset, $chr, 0);
267 310 100       8757 if(bytes::length($chunk) + bytes::length($chr) > $llen)
268 7         46 { push @result, _mime_word($pre, _encode_b($chunk));
269 7         18 $chunk = '';
270 7         24 $llen = int((73 - length $pre) / 4) * 3;
271             }
272 310         1842 $chunk .= $chr;
273             }
274 8 50       36 push @result, _mime_word($pre, _encode_b($chunk))
275             if length $chunk;
276             }
277              
278 28         285 join ' ', @result;
279             }
280              
281              
282             sub _decoder($$$)
283 41     41   199 { my ($charset, $encoding, $encoded) = @_;
284 41         104 $charset =~ s/\*[^*]+$//; # language component not used
285 41   100     161 my $to_utf8 = Encode::find_encoding($charset || 'us-ascii');
286 41 50       14778 $to_utf8 or return $encoded;
287              
288 41         69 my $decoded;
289 41 100       210 if($encoding !~ /\S/)
    100          
    50          
290 1         4 { $decoded = $encoded;
291             }
292             elsif(lc($encoding) eq 'q')
293             { # Quoted-printable encoded specific to mime-fields
294 32         185 $decoded = MIME::QuotedPrint::decode_qp($encoded =~ s/_/ /gr);
295             }
296             elsif(lc($encoding) eq 'b')
297             { # Base64 encoded
298 8         55 require MIME::Base64;
299 8         34 $decoded = MIME::Base64::decode_base64($encoded);
300             }
301             else
302             { # unknown encodings ignored
303 0         0 return $encoded;
304             }
305              
306 41         275 $to_utf8->decode($decoded, Encode::FB_DEFAULT); # error-chars -> '?'
307             }
308              
309             sub decode($@)
310 324     324 1 18290 { my $thing = shift;
311 324         1064 my @encoded = split /(\=\?[^?\s]*\?[bqBQ]?\?[^?]*\?\=)/, shift;
312 324 100       825 @encoded or return '';
313              
314 323         710 my %args = @_;
315              
316 323 50       793 my $is_text = exists $args{is_text} ? $args{is_text} : 1;
317 323         704 my @decoded = shift @encoded;
318              
319 323         856 while(@encoded)
320 41         249 { shift(@encoded) =~ /\=\?([^?\s]*)\?([^?\s]*)\?([^?]*)\?\=/;
321 41         143 push @decoded, _decoder $1, $2, $3;
322              
323 41 100       205 @encoded or last;
324              
325             # in text, blanks between encoding must be removed, but otherwise kept
326 27 100 66     157 if($is_text && $encoded[0] !~ m/\S/) { shift @encoded }
  17         50  
327 10         32 else { push @decoded, shift @encoded }
328             }
329              
330 323         2207 join '', @decoded;
331             }
332              
333             #--------------------
334              
335 5     5 1 7 sub parse($) { $_[0] }
336              
337              
338             sub consumePhrase($)
339 257     257 1 211279 { my ($thing, $string) = @_;
340              
341 257         413 my $phrase;
342 257 100       5711 if($string =~ s/^\s*\" ((?:[^"\r\n\\]*|\\.)*) (?:\"|\s*$)//x )
    100          
343 88         320 { ($phrase = $1) =~ s/\\\"/"/g;
344             }
345             elsif($string =~ s/^\s*((?:\=\?.*?\?\=|[${utf8_atext}${atext_ill}\ \t.])+)//o )
346 139         641 { ($phrase = $1) =~ s/\s+$//;
347 139 100       396 CORE::length($phrase) or undef $phrase;
348             }
349              
350 257 100       1154 defined $phrase ? ($thing->decode($phrase), $string) : (undef, $string);
351             }
352              
353              
354             sub consumeComment($)
355 882     882 1 12643 { my ($thing, $string) = @_;
356             # Backslashes are officially not permitted in comments, but not everyone
357             # knows that. Nested parens are supported.
358              
359 882 100       4578 $string =~ s/^\s* \( ((?:\\.|[^)])*) (?:\)|$) //x
360             or return (undef, $string); # allow unterminated comments
361              
362 75         250 my $comment = $1;
363              
364             # Continue consuming characters until we have balanced parens, for
365             # nested comments which are permitted.
366 75         125 while(1)
367 91         225 { (my $count = $comment) =~ s/\\./xx/g;
368 91 100       311 last if +( $count =~ tr/(// ) == ( $count =~ tr/)// );
369              
370 27 100       164 last if $string !~ s/^((?:\\.|[^)])*) \)//x; # cannot satisfy
371              
372 16         60 $comment .= ')'.$1;
373             }
374              
375 75         172 for($comment)
376 75         168 { s/^\s+//;
377 75         199 s/\s+$//;
378 75         230 s/\\ ( [()] )/$1/gx; # Remove backslashes before nested comment.
379             }
380              
381 75         1498 ($comment, $string);
382             }
383              
384              
385             sub consumeDotAtom($)
386 142     142 1 312 { my ($self, $string) = @_;
387 142         247 my ($atom, $comment);
388              
389 142         244 while(1)
390 298         693 { (my $c, $string) = $self->consumeComment($string);
391 298 100       775 if(defined $c) { $comment .= $c; next }
  14         37  
  14         29  
392              
393 284 100       1772 $string =~ s/^\s*([$atext]+(?:\.[$atext]+)*)//o
394             or last;
395              
396 142         485 $atom .= $1;
397             }
398              
399 142         587 ($atom, $string, $comment);
400             }
401              
402              
403 1     1 1 10 sub produceBody() { $_[0]->{MMFF_body} }
404              
405             #--------------------
406              
407              
408             1;