File Coverage

blib/lib/Mail/Message/Field/Full.pm
Criterion Covered Total %
statement 219 228 96.0
branch 85 96 88.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-2022 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 21     21   1162 use vars '$VERSION';
  21         41  
  21         948  
11             $VERSION = '3.012';
12              
13 21     21   163 use base 'Mail::Message::Field';
  21         38  
  21         5131  
14              
15 21     21   134 use strict;
  21         39  
  21         405  
16 21     21   90 use warnings;
  21         43  
  21         483  
17 21     21   8751 use utf8;
  21         243  
  21         116  
18              
19 21     21   4738 use Encode ();
  21         79652  
  21         330  
20 21     21   8957 use MIME::QuotedPrint ();
  21         22667  
  21         494  
21 21     21   12169 use Storable 'dclone';
  21         60903  
  21         1410  
22              
23 21     21   9360 use Mail::Message::Field::Addresses;
  21         59  
  21         671  
24 21     21   8880 use Mail::Message::Field::AuthResults;
  21         70  
  21         885  
25             #use Mail::Message::Field::AuthRecChain;
26 21     21   8978 use Mail::Message::Field::Date;
  21         262  
  21         811  
27 21     21   9037 use Mail::Message::Field::DKIM;
  21         55  
  21         698  
28 21     21   129 use Mail::Message::Field::Structured;
  21         37  
  21         408  
29 21     21   7780 use Mail::Message::Field::Unstructured;
  21         54  
  21         626  
30 21     21   7694 use Mail::Message::Field::URIs;
  21         145  
  21         1214  
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   118 use overload '""' => sub { shift->decodedBody };
  21     78   39  
  21         262  
  78         3466  
37              
38             #------------------------------------------
39              
40              
41             my %implementation;
42              
43             BEGIN {
44             $implementation{$_} = 'Addresses'
45 21     21   3839 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         148 for qw/list-help list-post list-subscribe list-unsubscribe
52             list-archive list-owner/;
53             $implementation{$_} = 'Structured'
54 21         81 for qw/content-disposition content-type content-id/;
55             $implementation{$_} = 'Date'
56 21         59 for qw/date resent-date/;
57             $implementation{$_} = 'AuthResults'
58 21         93 for qw/authentication-results/;
59             $implementation{$_} = 'DKIM'
60 21         31950 for qw/dkim-signature/;
61             # $implementation{$_} = 'AuthRecChain'
62             # for qw/arc-authentication-results arc-message-signature arc-seal/;
63             }
64              
65             sub new($;$$@)
66 113     113 1 24472 { my $class = shift;
67 113         215 my $name = shift;
68 113 100       404 my $body = @_ % 2 ? shift : undef;
69 113         233 my %args = @_;
70              
71 113 50       336 $body = delete $args{body} if defined $args{body};
72 113 100       280 unless(defined $body)
73 91         566 { (my $n, $body) = split /\s*\:\s*/s, $name, 2;
74 91 100       302 $name = $n if defined $body;
75             }
76            
77 113 100       502 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     368 . ($implementation{lc $name} || 'Unstructured');
83              
84 80         436 $myclass->SUPER::new(%args, name => $name, body => $body);
85             }
86              
87             sub init($)
88 113     113 0 259 { my ($self, $args) = @_;
89              
90 113         437 $self->SUPER::init($args);
91 113         281 $self->{MMFF_name} = $args->{name};
92 113         189 my $body = $args->{body};
93              
94 113 100 100     884 if(!defined $body || !length $body || ref $body) { ; } # no body yet
    100 66        
95             elsif(index($body, "\n") >= 0)
96 81         276 { $self->foldedBody($body) } # body is already folded
97 19         74 else { $self->unfoldedBody($body) } # body must be folded
98              
99 113         231 $self;
100             }
101              
102 0     0 1 0 sub clone() { dclone(shift) }
103 158     158 1 6795 sub name() { lc shift->{MMFF_name}}
104 0     0 1 0 sub Name() { shift->{MMFF_name}}
105              
106             sub folded()
107 85     85 1 160 { my $self = shift;
108 85 100       376 return $self->{MMFF_name}.':'.$self->foldedBody
109             unless wantarray;
110              
111 1         6 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 215     215 1 1497 { my ($self, $body) = (shift, shift);
118              
119 215 100       549 if(defined $body)
120 19         90 { $self->foldedBody(scalar $self->fold($self->{MMFF_name}, $body));
121 19         97 return $body;
122             }
123              
124 196         444 $body = $self->foldedBody;
125 196         852 $body =~ s/^ //;
126              
127             # remove FWS, also required within quoted strings.
128 196         866 $body =~ s/\r?\n\s?/ /g;
129 196         650 $body =~ s/ +$//;
130 196         618 $body;
131             }
132              
133             sub foldedBody($)
134 424     424 1 909 { my ($self, $body) = @_;
135              
136 424 100       1439 if(@_==2)
    100          
137 100         432 { $self->parse($body);
138 100         362 $body =~ s/^\s*/ /m;
139 100         301 $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         233 = $self->fold($self->{MMFF_name}, $self->produceBody);
146             }
147              
148 424 100       1330 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 935 { my $self = shift;
164 79         227 $self->decode($self->unfoldedBody, @_);
165             }
166              
167             #------------------------------------------
168              
169              
170             sub createComment($@)
171 38     38 1 11218 { my ($thing, $comment) = (shift, shift);
172              
173 38 100       88 $comment = $thing->encode($comment, @_)
174             if @_; # encoding required...
175              
176             # Correct dangling parenthesis
177 38         52 local $_ = $comment; # work with a copy
178 38         92 s#\\[()]#xx#g; # remove escaped parens
179 38         187 s#[^()]#x#g; # remove other chars
180 38         179 while( s#\(([^()]*)\)#x$1x# ) {;} # remove pairs of parens
181              
182 38         159 substr($comment, CORE::length($_), 0, '\\')
183             while s#[()][^()]*$##; # add escape before remaining parens
184              
185 38         72 $comment =~ s#\\+$##; # backslash at end confuses
186 38         140 "($comment)";
187             }
188              
189              
190             sub createPhrase($)
191 65     65 1 2274 { my $self = shift;
192 65         119 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 65 100       128 return $_ = $self->encode($_, @_, force => 1)
198             if length $_ > 50;
199              
200 63 100       170 $_ = $self->encode($_, @_)
201             if @_; # encoding required...
202              
203 63 100       357 if( m/[^$atext]/ )
204 48         80 { s#\\#\\\\#g;
205 48         76 s#"#\\"#g;
206 48         88 $_ = qq["$_"];
207             }
208              
209 63         157 $_;
210             }
211              
212              
213 0     0 1 0 sub beautify() { shift }
214              
215             #------------------------------------------
216              
217              
218 59     59   147 sub _mime_word($$) { "$_[0]$_[1]?=" }
219 15     15   87 sub _encode_b($) { MIME::Base64::encode_base64(shift, '') }
220              
221             sub _encode_q($) # RFC2047 sections 4.2 and 5
222 668     668   27075 { my $chunk = shift;
223 668         1196 $chunk =~ s#([^a-zA-Z0-9!*+/=_ -])#sprintf "=%02X", ord $1#ge;
  452         1204  
224 668         819 $chunk =~ s#([_\?,"])#sprintf "=%02X", ord $1#ge;
  0         0  
225 668         910 $chunk =~ s/ /_/g; # special case for =? ?= use
226 668         1058 $chunk;
227             }
228              
229             sub encode($@)
230 91     91 1 1116 { my ($self, $utf8, %args) = @_;
231              
232 91         150 my ($charset, $lang, $encoding);
233              
234 91 100       176 if($charset = $args{charset})
235 28 50       82 { $self->log(WARNING => "Illegal character in charset '$charset'")
236             if $charset =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
237             }
238 63         78 else { $charset = 'us-ascii' }
239              
240 91 100       171 if($lang = $args{language})
241 4 50       9 { $self->log(WARNING => "Illegal character in language '$lang'")
242             if $lang =~ m/[\x00-\ ()<>@,;:"\/[\]?.=\\]/;
243             }
244              
245 91 100       155 if($encoding = $args{encoding})
246 11 50       56 { unless($encoding =~ m/^[bBqQ]$/ )
247 0         0 { $self->log(WARNING => "Illegal encoding '$encoding', used 'q'");
248 0         0 $encoding = 'q';
249             }
250             }
251 80         106 else { $encoding = 'q' }
252              
253 91         117 my $name = $args{name};
254 91 100       147 my $lname = defined $name ? length($name)+1 : 0;
255              
256             return $utf8
257             if lc($encoding) eq 'q'
258             && length $utf8 < 70
259 21 100 100 21   181 && ($utf8 =~ m/\A[\p{IsASCII}]+\z/ms && !$args{force});
  21   100     40  
  21   100     334  
  91         690  
260              
261 28 100       85 my $pre = '=?'. $charset. ($lang ? '*'.$lang : '') .'?'.$encoding.'?';
262              
263 28         42 my @result;
264 28 100       51 if(lc($encoding) eq 'q')
265 20         84 { my $chunk = '';
266 20         64 my $llen = 73 - length($pre) - $lname;
267              
268 20         88 while(length(my $chr = substr($utf8, 0, 1, '')))
269 668         1127 { $chr = _encode_q Encode::encode($charset, $chr, 0);
270 668 100       1107 if(bytes::length($chunk) + bytes::length($chr) > $llen)
271 24         104 { push @result, _mime_word($pre, $chunk);
272 24         36 $chunk = '';
273 24         30 $llen = 73 - length $pre;
274             }
275 668         10676 $chunk .= $chr;
276             }
277 20 50       63 push @result, _mime_word($pre, $chunk)
278             if length($chunk);
279             }
280             else
281 8         12 { my $chunk = '';
282 8         26 my $llen = int((73 - length($pre) - $lname) / 4) * 3;
283 8         30 while(length(my $chr = substr($utf8, 0, 1, '')))
284 310         463 { my $chr = Encode::encode($charset, $chr, 0);
285 310 100       8160 if(bytes::length($chunk) + bytes::length($chr) > $llen)
286 7         33 { push @result, _mime_word($pre, _encode_b($chunk));
287 7         13 $chunk = '';
288 7         17 $llen = int((73 - length $pre) / 4) * 3;
289             }
290 310         1412 $chunk .= $chr;
291             }
292 8 50       23 push @result, _mime_word($pre, _encode_b($chunk))
293             if length $chunk;
294             }
295              
296 28         163 join ' ', @result;
297             }
298              
299              
300             sub _decoder($$$)
301 41     41   133 { my ($charset, $encoding, $encoded) = @_;
302 41         72 $charset =~ s/\*[^*]+$//; # language component not used
303 41   100     109 my $to_utf8 = Encode::find_encoding($charset || 'us-ascii');
304 41 50       19910 $to_utf8 or return $encoded;
305              
306 41         52 my $decoded;
307 41 100       150 if($encoding !~ /\S/)
    100          
    50          
308 1         2 { $decoded = $encoded;
309             }
310             elsif(lc($encoding) eq 'q')
311             { # Quoted-printable encoded
312 32         70 $encoded =~ s/_/ /g; # specific to mime-fields
313 32         97 $decoded = MIME::QuotedPrint::decode_qp($encoded);
314             }
315             elsif(lc($encoding) eq 'b')
316             { # Base64 encoded
317 8         38 require MIME::Base64;
318 8         30 $decoded = MIME::Base64::decode_base64($encoded);
319             }
320             else
321             { # unknown encodings ignored
322 0         0 return $encoded;
323             }
324              
325 41         152 $to_utf8->decode($decoded, Encode::FB_DEFAULT); # error-chars -> '?'
326             }
327              
328             sub decode($@)
329 265     265 1 8826 { my $thing = shift;
330 265         783 my @encoded = split /(\=\?[^?\s]*\?[bqBQ]?\?[^?]*\?\=)/, shift;
331 265 100       591 @encoded or return '';
332              
333 264         467 my %args = @_;
334              
335 264 50       601 my $is_text = defined $args{is_text} ? $args{is_text} : 1;
336 264         496 my @decoded = shift @encoded;
337              
338 264         556 while(@encoded)
339 41         147 { shift(@encoded) =~ /\=\?([^?\s]*)\?([^?\s]*)\?([^?]*)\?\=/;
340 41         86 push @decoded, _decoder $1, $2, $3;
341              
342 41 100       133 @encoded or last;
343              
344             # in text, blanks between encoding must be removed, but otherwise kept
345 27 100 66     121 if($is_text && $encoded[0] !~ m/\S/) { shift @encoded }
  17         35  
346 10         25 else { push @decoded, shift @encoded }
347             }
348              
349 264         1436 join '', @decoded;
350             }
351              
352             #------------------------------------------
353              
354              
355 5     5 1 8 sub parse($) { shift }
356              
357              
358             sub consumePhrase($)
359 173     173 1 10517 { my ($thing, $string) = @_;
360              
361 173         236 my $phrase;
362 173 100       2134 if($string =~ s/^\s*\" ((?:[^"\r\n\\]*|\\.)*) (?:\"|\s*$)//x )
    100          
363 77         219 { ($phrase = $1) =~ s/\\\"/"/g;
364             }
365             elsif($string =~ s/^\s*((?:\=\?.*\?\=|[${atext}${atext_ill}\ \t.])+)//o )
366 91         391 { ($phrase = $1) =~ s/\s+$//;
367 91 100       245 CORE::length($phrase) or undef $phrase;
368             }
369            
370 173 100       600 defined $phrase
371             ? ($thing->decode($phrase), $string)
372             : (undef, $string);
373             }
374              
375              
376             sub consumeComment($)
377 593     593 1 10064 { my ($thing, $string) = @_;
378              
379 593 100       1808 return (undef, $string)
380             unless $string =~ s/^\s*\(((?:[^)\\]+|\\.)*)\)//;
381              
382 45         90 my $comment = $1;
383 45         49 while(1)
384 49         77 { (my $count = $comment) =~ s/\\./xx/g;
385              
386 49 100       111 last if $count =~ tr/(// == $count =~ tr/)//;
387              
388 5 100       22 return (undef, $_[1])
389             unless $string =~ s/^((?:[^)\\]+|\\.)*)\)//;
390              
391 4         10 $comment .= ')'.$1;
392             }
393              
394 44         68 $comment =~ s/\\([()])/$1/g;
395 44         100 ($comment, $string);
396             }
397              
398              
399             sub consumeDotAtom($)
400 72     72 1 116 { my ($self, $string) = @_;
401 72         94 my ($atom, $comment);
402              
403 72         79 while(1)
404 147         213 { (my $c, $string) = $self->consumeComment($string);
405 147 100       253 if(defined $c) { $comment .= $c; next }
  5         10  
  5         6  
406              
407 142 100       531 last unless $string =~ s/^\s*([$atext]+(?:\.[$atext]+)*)//o;
408              
409 70         155 $atom .= $1;
410             }
411              
412 72         207 ($atom, $string, $comment);
413             }
414              
415              
416 1     1 1 5 sub produceBody() { $_[0]->{MMFF_body} }
417              
418             #------------------------------------------
419              
420              
421              
422             1;