File Coverage

blib/lib/Mail/Message/Field/Full.pm
Criterion Covered Total %
statement 220 228 96.4
branch 84 96 87.5
condition 20 22 90.9
subroutine 39 43 90.7
pod 19 20 95.0
total 382 409 93.4


line stmt bran cond sub pod time code
1             # Copyrights 2001-2021 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.02.
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 21     21   1593 use vars '$VERSION';
  21         45  
  21         1147  
11             $VERSION = '3.011';
12              
13 21     21   196 use base 'Mail::Message::Field';
  21         51  
  21         6861  
14              
15 21     21   177 use strict;
  21         50  
  21         567  
16 21     21   121 use warnings;
  21         41  
  21         655  
17 21     21   11233 use utf8;
  21         269  
  21         139  
18              
19 21     21   6578 use Encode ();
  21         102521  
  21         447  
20 21     21   10649 use MIME::QuotedPrint ();
  21         28250  
  21         597  
21 21     21   14231 use Storable 'dclone';
  21         74493  
  21         1835  
22              
23 21     21   11602 use Mail::Message::Field::Addresses;
  21         78  
  21         830  
24 21     21   11861 use Mail::Message::Field::AuthResults;
  21         80  
  21         1111  
25             #use Mail::Message::Field::AuthRecChain;
26 21     21   11694 use Mail::Message::Field::Date;
  21         300  
  21         1052  
27 21     21   11312 use Mail::Message::Field::DKIM;
  21         62  
  21         805  
28 21     21   153 use Mail::Message::Field::Structured;
  21         52  
  21         472  
29 21     21   10230 use Mail::Message::Field::Unstructured;
  21         63  
  21         790  
30 21     21   9640 use Mail::Message::Field::URIs;
  21         200  
  21         1497  
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 21     21   153 use overload '""' => sub { shift->decodedBody };
  21     78   50  
  21         308  
  78         4302  
37              
38             #------------------------------------------
39              
40              
41             my %implementation;
42              
43             BEGIN {
44             $implementation{$_} = 'Addresses'
45 21     21   4600 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 21         159 for qw/list-help list-post list-subscribe list-unsubscribe
52             list-archive list-owner/;
53             $implementation{$_} = 'Structured'
54 21         90 for qw/content-disposition content-type content-id/;
55             $implementation{$_} = 'Date'
56 21         76 for qw/date resent-date/;
57             $implementation{$_} = 'AuthResults'
58 21         139 for qw/authentication-results/;
59             $implementation{$_} = 'DKIM'
60 21         39501 for qw/dkim-signature/;
61             # $implementation{$_} = 'AuthRecChain'
62             # for qw/arc-authentication-results arc-message-signature arc-seal/;
63             }
64              
65             sub new($;$$@)
66 112     112 1 31863 { my $class = shift;
67 112         244 my $name = shift;
68 112 100       473 my $body = @_ % 2 ? shift : undef;
69 112         278 my %args = @_;
70              
71 112 50       499 $body = delete $args{body} if defined $args{body};
72 112 100       386 unless(defined $body)
73 91         739 { (my $n, $body) = split /\s*\:\s*/s, $name, 2;
74 91 100       369 $name = $n if defined $body;
75             }
76            
77 112 100       575 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 80   100     463 . ($implementation{lc $name} || 'Unstructured');
83              
84 80         519 $myclass->SUPER::new(%args, name => $name, body => $body);
85             }
86              
87             sub init($)
88 112     112 0 301 { my ($self, $args) = @_;
89              
90 112         512 $self->SUPER::init($args);
91 112         320 $self->{MMFF_name} = $args->{name};
92 112         255 my $body = $args->{body};
93              
94 112 100 100     1058 if(!defined $body || !length $body || ref $body) { ; } # no body yet
    100 66        
95             elsif(index($body, "\n") >= 0)
96 81         320 { $self->foldedBody($body) } # body is already folded
97 18         105 else { $self->unfoldedBody($body) } # body must be folded
98              
99 112         329 $self;
100             }
101              
102 0     0 1 0 sub clone() { dclone(shift) }
103 158     158 1 11446 sub name() { lc shift->{MMFF_name}}
104 0     0 1 0 sub Name() { shift->{MMFF_name}}
105              
106             sub folded()
107 85     85 1 172 { my $self = shift;
108 85 100       410 return $self->{MMFF_name}.':'.$self->foldedBody
109             unless wantarray;
110              
111 1         7 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 214     214 1 1898 { my ($self, $body) = (shift, shift);
118              
119 214 100       667 if(defined $body)
120 18         111 { $self->foldedBody(scalar $self->fold($self->{MMFF_name}, $body));
121 18         144 return $body;
122             }
123              
124 196         485 $body = $self->foldedBody;
125 196         911 $body =~ s/^ //;
126              
127             # remove FWS, also required within quoted strings.
128 196         985 $body =~ s/\r?\n\s?/ /g;
129 196         683 $body =~ s/ +$//;
130 196         656 $body;
131             }
132              
133             sub foldedBody($)
134 423     423 1 1084 { my ($self, $body) = @_;
135              
136 423 100       1679 if(@_==2)
    100          
137 99         495 { $self->parse($body);
138 99         469 $body =~ s/^\s*/ /m;
139 99         423 $self->{MMFF_body} = $body;
140             }
141             elsif(defined($body = $self->{MMFF_body})) { ; }
142             else
143             { # Create a new folded body from the parts.
144             $self->{MMFF_body} = $body
145 55         251 = $self->fold($self->{MMFF_name}, $self->produceBody);
146             }
147              
148 423 100       1518 wantarray ? (split /^/, $body) : $body;
149             }
150              
151             #------------------------------------------
152              
153              
154             sub from($@)
155 0     0 1 0 { my ($class, $field) = (shift, shift);
156 0 0       0 defined $field ? $class->new($field->Name, $field->foldedBody, @_) : ();
157             }
158              
159             #------------------------------------------
160              
161              
162             sub decodedBody()
163 79     79 1 1496 { my $self = shift;
164 79         259 $self->decode($self->unfoldedBody, @_);
165             }
166              
167             #------------------------------------------
168              
169              
170             sub createComment($@)
171 38     38 1 14088 { my ($thing, $comment) = (shift, shift);
172              
173 38 100       177 $comment = $thing->encode($comment, @_)
174             if @_; # encoding required...
175              
176             # Correct dangling parenthesis
177 38         123 local $_ = $comment; # work with a copy
178 38         138 s#\\[()]#xx#g; # remove escaped parens
179 38         243 s#[^()]#x#g; # remove other chars
180 38         226 while( s#\(([^()]*)\)#x$1x# ) {;} # remove pairs of parens
181              
182 38         266 substr($comment, CORE::length($_), 0, '\\')
183             while s#[()][^()]*$##; # add escape before remaining parens
184              
185 38         107 $comment =~ s#\\+$##; # backslash at end confuses
186 38         171 "($comment)";
187             }
188              
189              
190             sub createPhrase($)
191 63     63 1 2758 { my $self = shift;
192 63         136 local $_ = shift;
193              
194             # I do not case whether it gets a but sloppy in the header string,
195             # as long as it is functionally correct: no folding inside phrase
196             # quotes.
197 63 50       174 return $_ = $self->encode($_, @_, force => 1)
198             if length $_ > 50;
199              
200 63 100       234 $_ = $self->encode($_, @_)
201             if @_; # encoding required...
202              
203 63 100       495 if( m/[^$atext]/ )
204 48         110 { s#\\#\\\\#g;
205 48         95 s#"#\\"#g;
206 48         129 $_ = qq["$_"];
207             }
208              
209 63         206 $_;
210             }
211              
212              
213 0     0 1 0 sub beautify() { shift }
214              
215             #------------------------------------------
216              
217              
218 55     55   179 sub _mime_word($$) { "$_[0]$_[1]?=" }
219 15     15   60 sub _encode_b($) { MIME::Base64::encode_base64(shift, '') }
220             sub _encode_q($)
221 548     548   22710 { my $chunk = shift;
222 548         1218 $chunk =~ s#([\x00-\x1F=\x7F-\xFF])#sprintf "=%02X", ord $1#ge;
  432         1380  
223 548         895 $chunk =~ s#([_\?,"])#sprintf "=%02X", ord $1#ge;
  7         42  
224 548         993 $chunk =~ s/ /_/g;
225 548         1074 $chunk;
226             }
227              
228             sub encode($@)
229 89     89 1 1515 { my ($self, $utf8, %args) = @_;
230              
231 89         199 my ($charset, $lang, $encoding);
232              
233 89 100       227 if($charset = $args{charset})
234 28 50       111 { $self->log(WARNING => "Illegal character in charset '$charset'")
235             if $charset =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
236             }
237 61         115 else { $charset = 'us-ascii' }
238              
239 89 100       223 if($lang = $args{language})
240 4 50       13 { $self->log(WARNING => "Illegal character in language '$lang'")
241             if $lang =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
242             }
243              
244 89 100       210 if($encoding = $args{encoding})
245 11 50       48 { unless($encoding =~ m/^[bBqQ]$/ )
246 0         0 { $self->log(WARNING => "Illegal encoding '$encoding', used 'q'");
247 0         0 $encoding = 'q';
248             }
249             }
250 78         167 else { $encoding = 'q' }
251              
252 89         158 my $name = $args{name};
253 89 100       196 my $lname = defined $name ? length($name)+1 : 0;
254              
255             return $utf8
256             if lc($encoding) eq 'q'
257             && length $utf8 < 70
258 21 100 100 21   226 && ($utf8 =~ m/\A[\p{IsASCII}]+\z/ms && !$args{force});
  21   100     65  
  21   100     506  
  89         919  
259              
260 26 100       101 my $pre = '=?'. $charset. ($lang ? '*'.$lang : '') .'?'.$encoding.'?';
261              
262 26         61 my @result;
263 26 100       63 if(lc($encoding) eq 'q')
264 18         92 { my $chunk = '';
265 18         80 my $llen = 73 - length($pre) - $lname;
266              
267 18         113 while(length(my $chr = substr($utf8, 0, 1, '')))
268 548         1454 { $chr = _encode_q Encode::encode($charset, $chr, 0);
269 548 100       1307 if(bytes::length($chunk) + bytes::length($chr) > $llen)
270 22         116 { push @result, _mime_word($pre, $chunk);
271 22         43 $chunk = '';
272 22         33 $llen = 73 - length $pre;
273             }
274 548         7585 $chunk .= $chr;
275             }
276 18 50       91 push @result, _mime_word($pre, $chunk)
277             if length($chunk);
278             }
279             else
280 8         15 { my $chunk = '';
281 8         30 my $llen = int((73 - length($pre) - $lname) / 4) * 3;
282 8         33 while(length(my $chr = substr($utf8, 0, 1, '')))
283 310         558 { my $chr = Encode::encode($charset, $chr, 0);
284 310 100       9737 if(bytes::length($chunk) + bytes::length($chr) > $llen)
285 7         40 { push @result, _mime_word($pre, _encode_b($chunk));
286 7         17 $chunk = '';
287 7         24 $llen = int((73 - length $pre) / 4) * 3;
288             }
289 310         1739 $chunk .= $chr;
290             }
291 8 50       32 push @result, _mime_word($pre, _encode_b($chunk))
292             if length $chunk;
293             }
294              
295 26         221 join ' ', @result;
296             }
297              
298              
299             sub _decoder($$$)
300 39     39   168 { my ($charset, $encoding, $encoded) = @_;
301 39         90 $charset =~ s/\*[^*]+$//; # language component not used
302 39   100     128 my $to_utf8 = Encode::find_encoding($charset || 'us-ascii');
303 39 50       18575 $to_utf8 or return $encoded;
304              
305 39         55 my $decoded;
306 39 100       193 if($encoding !~ /\S/)
    100          
    50          
307 1         3 { $decoded = $encoded;
308             }
309             elsif(lc($encoding) eq 'q')
310             { # Quoted-printable encoded
311 30         80 $encoded =~ s/_/ /g; # specific to mime-fields
312 30         109 $decoded = MIME::QuotedPrint::decode_qp($encoded);
313             }
314             elsif(lc($encoding) eq 'b')
315             { # Base64 encoded
316 8         63 require MIME::Base64;
317 8         41 $decoded = MIME::Base64::decode_base64($encoded);
318             }
319             else
320             { # unknown encodings ignored
321 0         0 return $encoded;
322             }
323              
324 39         194 $to_utf8->decode($decoded, Encode::FB_DEFAULT); # error-chars -> '?'
325             }
326              
327             sub decode($@)
328 264     264 1 10526 { my $thing = shift;
329 264         954 my @encoded = split /(\=\?[^?\s]*\?[bqBQ]?\?[^?]*\?\=)/, shift;
330 264 100       713 @encoded or return '';
331              
332 263         530 my %args = @_;
333              
334 263 50       683 my $is_text = defined $args{is_text} ? $args{is_text} : 1;
335 263         510 my @decoded = shift @encoded;
336              
337 263         668 while(@encoded)
338 39         177 { shift(@encoded) =~ /\=\?([^?\s]*)\?([^?\s]*)\?([^?]*)\?\=/;
339 39         115 push @decoded, _decoder $1, $2, $3;
340              
341 39 100       166 @encoded or last;
342              
343             # in text, blanks between encoding must be removed, but otherwise kept
344 26 100 66     122 if($is_text && $encoded[0] !~ m/\S/) { shift @encoded }
  16         41  
345 10         44 else { push @decoded, shift @encoded }
346             }
347              
348 263         1641 join '', @decoded;
349             }
350              
351             #------------------------------------------
352              
353              
354 5     5 1 15 sub parse($) { shift }
355              
356              
357             sub consumePhrase($)
358 172     172 1 10802 { my ($thing, $string) = @_;
359              
360 172         266 my $phrase;
361 172 100       1857 if($string =~ s/^\s*\" ((?:[^"\r\n\\]*|\\.)*) (?:\"|\s*$)//x )
    100          
362 77         262 { ($phrase = $1) =~ s/\\\"/"/g;
363             }
364             elsif($string =~ s/^\s*([${atext}${atext_ill}\ \t.]+)//o )
365 90         405 { ($phrase = $1) =~ s/\s+$//;
366 90 100       275 CORE::length($phrase) or undef $phrase;
367             }
368            
369 172 100       705 defined $phrase
370             ? ($thing->decode($phrase), $string)
371             : (undef, $string);
372             }
373              
374              
375             sub consumeComment($)
376 583     583 1 10954 { my ($thing, $string) = @_;
377              
378 583 100       2261 return (undef, $string)
379             unless $string =~ s/^\s*\(((?:[^)\\]+|\\.)*)\)//;
380              
381 45         116 my $comment = $1;
382 45         72 while(1)
383 49         108 { (my $count = $comment) =~ s/\\./xx/g;
384              
385 49 100       140 last if $count =~ tr/(// == $count =~ tr/)//;
386              
387 5 100       29 return (undef, $_[1])
388             unless $string =~ s/^((?:[^)\\]+|\\.)*)\)//;
389              
390 4         12 $comment .= ')'.$1;
391             }
392              
393 44         90 $comment =~ s/\\([()])/$1/g;
394 44         131 ($comment, $string);
395             }
396              
397              
398             sub consumeDotAtom($)
399 69     69 1 126 { my ($self, $string) = @_;
400 69         112 my ($atom, $comment);
401              
402 69         106 while(1)
403 140         308 { (my $c, $string) = $self->consumeComment($string);
404 140 100       370 if(defined $c) { $comment .= $c; next }
  5         11  
  5         8  
405              
406 135 100       667 last unless $string =~ s/^\s*([$atext]+(?:\.[$atext]+)*)//o;
407              
408 66         182 $atom .= $1;
409             }
410              
411 69         251 ($atom, $string, $comment);
412             }
413              
414              
415 1     1 1 6 sub produceBody() { $_[0]->{MMFF_body} }
416              
417             #------------------------------------------
418              
419              
420              
421             1;