File Coverage

blib/lib/Mail/Message/Field/Full.pm
Criterion Covered Total %
statement 224 233 96.1
branch 85 96 88.5
condition 20 22 90.9
subroutine 39 43 90.7
pod 19 20 95.0
total 387 414 93.4


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::Full;
10 22     22   1354 use vars '$VERSION';
  22         85  
  22         1210  
11             $VERSION = '3.013';
12              
13 22     22   133 use base 'Mail::Message::Field';
  22         44  
  22         6233  
14              
15 22     22   154 use strict;
  22         58  
  22         489  
16 22     22   107 use warnings;
  22         52  
  22         709  
17 22     22   4271 use utf8;
  22         140  
  22         119  
18              
19 22     22   5630 use Encode ();
  22         94006  
  22         508  
20 22     22   10244 use MIME::QuotedPrint ();
  22         27832  
  22         582  
21 22     22   14061 use Storable 'dclone';
  22         72673  
  22         1547  
22              
23 22     22   11035 use Mail::Message::Field::Addresses;
  22         68  
  22         730  
24 22     22   10042 use Mail::Message::Field::AuthResults;
  22         66  
  22         968  
25             #use Mail::Message::Field::AuthRecChain;
26 22     22   9696 use Mail::Message::Field::Date;
  22         65  
  22         975  
27 22     22   9250 use Mail::Message::Field::DKIM;
  22         63  
  22         789  
28 22     22   162 use Mail::Message::Field::Structured;
  22         58  
  22         502  
29 22     22   9397 use Mail::Message::Field::Unstructured;
  22         62  
  22         908  
30 22     22   9714 use Mail::Message::Field::URIs;
  22         60  
  22         1682  
31              
32             my $atext = q[a-zA-Z0-9!#\$%&'*+\-\/=?^_`{|}~]; # from RFC
33             my $atext_ill = q/\[\]/; # illegal, but still used (esp spam)
34              
35              
36 22     22   253 use overload '""' => sub { shift->decodedBody };
  22     87   58  
  22         213  
  87         4220  
37              
38             #------------------------------------------
39              
40              
41             my %implementation;
42              
43             BEGIN {
44             $implementation{$_} = 'Addresses'
45 22     22   4772 for qw/from to sender cc bcc reply-to envelope-to
46             resent-from resent-to resent-cc resent-bcc resent-reply-to
47             resent-sender
48             x-beenthere errors-to mail-follow-up x-loop delivered-to
49             original-sender x-original-sender/;
50             $implementation{$_} = 'URIs'
51 22         186 for qw/list-help list-post list-subscribe list-unsubscribe
52             list-archive list-owner/;
53             $implementation{$_} = 'Structured'
54 22         95 for qw/content-disposition content-type content-id/;
55             $implementation{$_} = 'Date'
56 22         64 for qw/date resent-date/;
57             $implementation{$_} = 'AuthResults'
58 22         82 for qw/authentication-results/;
59             $implementation{$_} = 'DKIM'
60 22         40210 for qw/dkim-signature/;
61             # $implementation{$_} = 'AuthRecChain'
62             # for qw/arc-authentication-results arc-message-signature arc-seal/;
63             }
64              
65             sub new($;$$@)
66 119     119 1 31443 { my $class = shift;
67 119         212 my $name = shift;
68 119 100       404 my $body = @_ % 2 ? shift : undef;
69 119         245 my %args = @_;
70              
71 119 50       329 $body = delete $args{body} if defined $args{body};
72 119 100       299 unless(defined $body)
73 96         582 { (my $n, $body) = split /\s*\:\s*/s, $name, 2;
74 96 100       310 $name = $n if defined $body;
75             }
76            
77 119 100       543 return $class->SUPER::new(%args, name => $name, body => $body)
78             if $class ne __PACKAGE__;
79              
80             # Look for best class to suit this field
81             my $myclass = 'Mail::Message::Field::'
82 85   100     364 . ($implementation{lc $name} || 'Unstructured');
83              
84 85         418 $myclass->SUPER::new(%args, name => $name, body => $body);
85             }
86              
87             sub init($)
88 119     119 0 224 { my ($self, $args) = @_;
89              
90 119         450 $self->SUPER::init($args);
91 119         271 $self->{MMFF_name} = $args->{name};
92 119         207 my $body = $args->{body};
93              
94 119 100 100     920 if(!defined $body || !length $body || ref $body) { ; } # no body yet
    100 66        
95             elsif(index($body, "\n") >= 0)
96 86         284 { $self->foldedBody($body) } # body is already folded
97 20         94 else { $self->unfoldedBody($body) } # body must be folded
98              
99 119         274 $self;
100             }
101              
102 0     0 1 0 sub clone() { dclone(shift) }
103 167     167 1 10478 sub name() { lc shift->{MMFF_name}}
104 0     0 1 0 sub Name() { shift->{MMFF_name}}
105              
106             sub folded()
107 89     89 1 146 { my $self = shift;
108 89 100       423 return $self->{MMFF_name}.':'.$self->foldedBody
109             unless wantarray;
110              
111 1         4 my @lines = $self->foldedBody;
112 1         5 my $first = $self->{MMFF_name}. ':'. shift @lines;
113 1         5 ($first, @lines);
114             }
115              
116             sub unfoldedBody($;$)
117 228     228 1 2019 { my ($self, $body) = (shift, shift);
118              
119 228 100       538 if(defined $body)
120 20         82 { $self->foldedBody(scalar $self->fold($self->{MMFF_name}, $body));
121 20         41 return $body;
122             }
123              
124 208         455 $body = $self->foldedBody;
125              
126 208         450 for($body)
127 208         478 { s/\r?\n(\s)/$1/g;
128 208         1135 s/\r?\n/ /g;
129 208         688 s/^\s+//;
130 208         862 s/\s+$//;
131             }
132 208         656 $body;
133             }
134              
135             sub foldedBody($)
136 450     450 1 1012 { my ($self, $body) = @_;
137              
138 450 100       1494 if(@_==2)
    100          
139 106         430 { $self->parse($body);
140 106         441 $body =~ s/^\s*/ /m;
141 106         286 $self->{MMFF_body} = $body;
142             }
143             elsif(defined($body = $self->{MMFF_body})) { ; }
144             else
145             { # Create a new folded body from the parts.
146             $self->{MMFF_body} = $body
147 58         222 = $self->fold($self->{MMFF_name}, $self->produceBody);
148             }
149              
150 450 100       1448 wantarray ? (split /^/, $body) : $body;
151             }
152              
153             #------------------------------------------
154              
155              
156             sub from($@)
157 0     0 1 0 { my ($class, $field) = (shift, shift);
158 0 0       0 defined $field ? $class->new($field->Name, $field->foldedBody, @_) : ();
159             }
160              
161             #------------------------------------------
162              
163              
164             sub decodedBody()
165 88     88 1 1428 { my $self = shift;
166 88         229 $self->decode($self->unfoldedBody, @_);
167             }
168              
169             #------------------------------------------
170              
171              
172             sub createComment($@)
173 38     38 1 13799 { my ($thing, $comment) = (shift, shift);
174              
175 38 100       112 $comment = $thing->encode($comment, @_)
176             if @_; # encoding required...
177              
178             # Correct dangling parenthesis
179 38         74 local $_ = $comment; # work with a copy
180 38         101 s#\\[()]#xx#g; # remove escaped parens
181 38         224 s#[^()]#x#g; # remove other chars
182 38         220 while( s#\(([^()]*)\)#x$1x# ) {;} # remove pairs of parens
183              
184 38         189 substr($comment, CORE::length($_), 0, '\\')
185             while s#[()][^()]*$##; # add escape before remaining parens
186              
187 38         90 $comment =~ s#\\+$##; # backslash at end confuses
188 38         173 "($comment)";
189             }
190              
191              
192             sub createPhrase($)
193 65     65 1 2737 { my $self = shift;
194 65         131 local $_ = shift;
195              
196             # I do not case whether it gets a but sloppy in the header string,
197             # as long as it is functionally correct: no folding inside phrase
198             # quotes.
199 65 100       157 return $_ = $self->encode($_, @_, force => 1)
200             if length $_ > 50;
201              
202 63 100       208 $_ = $self->encode($_, @_)
203             if @_; # encoding required...
204              
205 63 100       436 if( m/[^$atext]/ )
206 48         103 { s#\\#\\\\#g;
207 48         94 s#"#\\"#g;
208 48         106 $_ = qq["$_"];
209             }
210              
211 63         188 $_;
212             }
213              
214              
215 0     0 1 0 sub beautify() { shift }
216              
217             #------------------------------------------
218              
219              
220 59     59   199 sub _mime_word($$) { "$_[0]$_[1]?=" }
221 15     15   143 sub _encode_b($) { MIME::Base64::encode_base64(shift, '') }
222              
223             sub _encode_q($) # RFC2047 sections 4.2 and 5
224 668     668   142156 { my $chunk = shift;
225 668         1533 $chunk =~ s#([^a-zA-Z0-9!*+/=_ -])#sprintf "=%02X", ord $1#ge;
  452         1459  
226 668         1035 $chunk =~ s#([_\?,"])#sprintf "=%02X", ord $1#ge;
  0         0  
227 668         1124 $chunk =~ s/ /_/g; # special case for =? ?= use
228 668         1368 $chunk;
229             }
230              
231             sub encode($@)
232 91     91 1 1480 { my ($self, $utf8, %args) = @_;
233              
234 91         147 my ($charset, $lang, $encoding);
235              
236 91 100       224 if($charset = $args{charset})
237 28 50       99 { $self->log(WARNING => "Illegal character in charset '$charset'")
238             if $charset =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
239             }
240 63         133 else { $charset = 'us-ascii' }
241              
242 91 100       211 if($lang = $args{language})
243 4 50       14 { $self->log(WARNING => "Illegal character in language '$lang'")
244             if $lang =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
245             }
246              
247 91 100       187 if($encoding = $args{encoding})
248 11 50       48 { unless($encoding =~ m/^[bBqQ]$/ )
249 0         0 { $self->log(WARNING => "Illegal encoding '$encoding', used 'q'");
250 0         0 $encoding = 'q';
251             }
252             }
253 80         117 else { $encoding = 'q' }
254              
255 91         147 my $name = $args{name};
256 91 100       183 my $lname = defined $name ? length($name)+1 : 0;
257              
258             return $utf8
259             if lc($encoding) eq 'q'
260             && length $utf8 < 70
261 22 100 100 22   207 && ($utf8 =~ m/\A[\p{IsASCII}]+\z/ms && !$args{force});
  22   100     62  
  22   100     419  
  91         827  
262              
263 28 100       122 my $pre = '=?'. $charset. ($lang ? '*'.$lang : '') .'?'.$encoding.'?';
264              
265 28         48 my @result;
266 28 100       64 if(lc($encoding) eq 'q')
267 20         103 { my $chunk = '';
268 20         93 my $llen = 73 - length($pre) - $lname;
269              
270 20         118 while(length(my $chr = substr($utf8, 0, 1, '')))
271 668         1421 { $chr = _encode_q Encode::encode($charset, $chr, 0);
272 668 100       1383 if(bytes::length($chunk) + bytes::length($chr) > $llen)
273 24         126 { push @result, _mime_word($pre, $chunk);
274 24         46 $chunk = '';
275 24         34 $llen = 73 - length $pre;
276             }
277 668         8546 $chunk .= $chr;
278             }
279 20 50       87 push @result, _mime_word($pre, $chunk)
280             if length($chunk);
281             }
282             else
283 8         14 { my $chunk = '';
284 8         33 my $llen = int((73 - length($pre) - $lname) / 4) * 3;
285 8         36 while(length(my $chr = substr($utf8, 0, 1, '')))
286 310         615 { my $chr = Encode::encode($charset, $chr, 0);
287 310 100       10325 if(bytes::length($chunk) + bytes::length($chr) > $llen)
288 7         41 { push @result, _mime_word($pre, _encode_b($chunk));
289 7         14 $chunk = '';
290 7         21 $llen = int((73 - length $pre) / 4) * 3;
291             }
292 310         1809 $chunk .= $chr;
293             }
294 8 50       39 push @result, _mime_word($pre, _encode_b($chunk))
295             if length $chunk;
296             }
297              
298 28         229 join ' ', @result;
299             }
300              
301              
302             sub _decoder($$$)
303 41     41   165 { my ($charset, $encoding, $encoded) = @_;
304 41         91 $charset =~ s/\*[^*]+$//; # language component not used
305 41   100     142 my $to_utf8 = Encode::find_encoding($charset || 'us-ascii');
306 41 50       21293 $to_utf8 or return $encoded;
307              
308 41         57 my $decoded;
309 41 100       197 if($encoding !~ /\S/)
    100          
    50          
310 1         4 { $decoded = $encoded;
311             }
312             elsif(lc($encoding) eq 'q')
313             { # Quoted-printable encoded
314 32         102 $encoded =~ s/_/ /g; # specific to mime-fields
315 32         136 $decoded = MIME::QuotedPrint::decode_qp($encoded);
316             }
317             elsif(lc($encoding) eq 'b')
318             { # Base64 encoded
319 8         49 require MIME::Base64;
320 8         38 $decoded = MIME::Base64::decode_base64($encoded);
321             }
322             else
323             { # unknown encodings ignored
324 0         0 return $encoded;
325             }
326              
327 41         216 $to_utf8->decode($decoded, Encode::FB_DEFAULT); # error-chars -> '?'
328             }
329              
330             sub decode($@)
331 285     285 1 11253 { my $thing = shift;
332 285         918 my @encoded = split /(\=\?[^?\s]*\?[bqBQ]?\?[^?]*\?\=)/, shift;
333 285 100       718 @encoded or return '';
334              
335 284         540 my %args = @_;
336              
337 284 50       654 my $is_text = defined $args{is_text} ? $args{is_text} : 1;
338 284         572 my @decoded = shift @encoded;
339              
340 284         701 while(@encoded)
341 41         186 { shift(@encoded) =~ /\=\?([^?\s]*)\?([^?\s]*)\?([^?]*)\?\=/;
342 41         116 push @decoded, _decoder $1, $2, $3;
343              
344 41 100       165 @encoded or last;
345              
346             # in text, blanks between encoding must be removed, but otherwise kept
347 27 100 66     126 if($is_text && $encoded[0] !~ m/\S/) { shift @encoded }
  17         41  
348 10         31 else { push @decoded, shift @encoded }
349             }
350              
351 284         1735 join '', @decoded;
352             }
353              
354             #------------------------------------------
355              
356              
357 5     5 1 9 sub parse($) { shift }
358              
359              
360             sub consumePhrase($)
361 204     204 1 12334 { my ($thing, $string) = @_;
362              
363 204         329 my $phrase;
364 204 100       2039 if($string =~ s/^\s*\" ((?:[^"\r\n\\]*|\\.)*) (?:\"|\s*$)//x )
    100          
365 82         249 { ($phrase = $1) =~ s/\\\"/"/g;
366             }
367             elsif($string =~ s/^\s*((?:\=\?.*?\?\=|[${atext}${atext_ill}\ \t.])+)//o )
368 103         377 { ($phrase = $1) =~ s/\s+$//;
369 103 100       275 CORE::length($phrase) or undef $phrase;
370             }
371            
372 204 100       729 defined $phrase
373             ? ($thing->decode($phrase), $string)
374             : (undef, $string);
375             }
376              
377              
378             sub consumeComment($)
379 617     617 1 12524 { my ($thing, $string) = @_;
380             # Backslashes are officially not permitted in comments, but not everyone
381             # knows that. Nested parens are supported.
382              
383 617 100       2315 return (undef, $string)
384             unless $string =~ s/^\s* \( ((?:\\.|[^)])*) (?:\)|$) //x;
385             # allow unterminated comments
386              
387 47         126 my $comment = $1;
388              
389             # Continue consuming characters until we have balanced parens, for
390             # nested comments which are permitted.
391 47         67 while(1)
392 51         109 { (my $count = $comment) =~ s/\\./xx/g;
393 51 100       140 last if +( $count =~ tr/(// ) == ( $count =~ tr/)// );
394              
395 5 100       24 last if $string !~ s/^((?:\\.|[^)])*) \)//x; # cannot satisfy
396              
397 4         14 $comment .= ')'.$1;
398             }
399              
400 47         95 for($comment)
401 47         91 { s/^\s+//;
402 47         97 s/\s+$//;
403 47         97 s/\\ ( [()] )/$1/gx; # Remove backslashes before nested comment.
404             }
405              
406 47         138 ($comment, $string);
407             }
408              
409              
410             sub consumeDotAtom($)
411 75     75 1 132 { my ($self, $string) = @_;
412 75         115 my ($atom, $comment);
413              
414 75         101 while(1)
415 153         279 { (my $c, $string) = $self->consumeComment($string);
416 153 100       304 if(defined $c) { $comment .= $c; next }
  5         11  
  5         7  
417              
418 148 100       636 last unless $string =~ s/^\s*([$atext]+(?:\.[$atext]+)*)//o;
419              
420 73         225 $atom .= $1;
421             }
422              
423 75         247 ($atom, $string, $comment);
424             }
425              
426              
427 1     1 1 6 sub produceBody() { $_[0]->{MMFF_body} }
428              
429             #------------------------------------------
430              
431              
432              
433             1;