File Coverage

blib/lib/Mail/Message/Body/Encode.pm
Criterion Covered Total %
statement 140 187 74.8
branch 71 114 62.2
condition 48 85 56.4
subroutine 21 24 87.5
pod 11 11 100.0
total 291 421 69.1


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::Body;
10 11     11   1351 use vars '$VERSION';
  11         30  
  11         787  
11             $VERSION = '3.013';
12              
13 11     11   75 use base 'Mail::Reporter';
  11         20  
  11         508  
14              
15 11     11   80 use strict;
  11         25  
  11         289  
16 11     11   60 use warnings;
  11         23  
  11         308  
17 11     11   5403 use utf8;
  11         144  
  11         86  
18              
19 11     11   413 use Carp;
  11         29  
  11         614  
20 11     11   73 use MIME::Types ();
  11         26  
  11         230  
21 11     11   63 use File::Basename 'basename';
  11         24  
  11         613  
22 11     11   79 use Encode qw/find_encoding from_to encode_utf8/;
  11         31  
  11         976  
23              
24 11     11   76 use Mail::Message::Field ();
  11         114  
  11         248  
25 11     11   5732 use Mail::Message::Field::Full ();
  11         38  
  11         527  
26              
27             # http://www.iana.org/assignments/character-sets
28 11     11   84 use Encode::Alias;
  11         28  
  11         20089  
29             define_alias(qr/^unicode-?1-?1-?utf-?([78])$/i => '"UTF-$1"'); # rfc1642
30              
31             my $mime_types;
32              
33              
34             sub _char_enc($)
35 126     126   258 { my ($self, $charset) = @_;
36 126 100 66     402 return undef if !$charset || $charset eq 'PERL';
37              
38 44 50       205 my $enc = find_encoding $charset
39             or $self->log(WARNING => "Charset `$charset' is not known.");
40              
41 44         5618 $enc;
42             }
43              
44             sub encode(@)
45 66     66 1 2738 { my ($self, %args) = @_;
46              
47 66   33     282 my $bodytype = $args{result_type} || ref $self;
48              
49             ### The content type
50              
51 66         228 my $type_from = $self->type;
52 66   33     316 my $type_to = $args{mime_type} || $type_from->clone->study;
53 66 50       250 $type_to = Mail::Message::Field::Full->new('Content-Type' => $type_to)
54             unless ref $type_to;
55              
56             ### Detect specified transfer-encodings
57              
58 66   66     198 my $transfer = $args{transfer_encoding} || $self->transferEncoding->clone;
59 66 100       226 $transfer = Mail::Message::Field->new('Content-Transfer-Encoding' => $transfer)
60             unless ref $transfer;
61              
62 66         236 my $trans_was = lc $self->transferEncoding;
63 66         177 my $trans_to = lc $transfer;
64              
65             ### Detect specified charsets
66              
67 66         179 my $is_text = $type_from =~ m!^text/!i;
68 66         149 my ($char_was, $char_to, $from, $to);
69 66 100       142 if($is_text)
    50          
70 63         188 { $char_was = $type_from->attribute('charset'); # sometimes missing
71 63         176 $char_to = $type_to->attribute('charset'); # usually missing
72              
73 63 100 33     190 if(my $charset = delete $args{charset})
    50          
74             { # Explicitly stated output charset
75 61 100 66     180 if(!$char_to || $char_to ne $charset)
76 42         83 { $char_to = $charset;
77 42         109 $type_to->attribute(charset => $char_to);
78             }
79             }
80             elsif(!$char_to && $char_was)
81             { # By default, do not change charset
82 0         0 $char_to = $char_was;
83 0         0 $type_to->attribute(charset => $char_to);
84             }
85              
86 63 50 66     189 if($char_to && $trans_to ne 'none' && $char_to eq 'PERL')
      66        
87             { # We cannot leave the body into the 'PERL' charset when transfer-
88             # encoding is applied.
89 0         0 $self->log(WARNING => "Transfer-Encoding `$trans_to' requires "
90             . "explicit charset, defaulted to utf-8");
91 0         0 $char_to = 'utf-8';
92             }
93              
94 63         228 $from = $self->_char_enc($char_was);
95 63         146 $to = $self->_char_enc($char_to);
96              
97 63 100 100     199 if($from && $to)
98 1 50 33     4 { if($char_was ne $char_to && $from->name eq $to->name)
99             { # modify source charset into a different alias
100 0         0 $type_from->attribute(charset => $char_to);
101 0         0 $char_was = $char_to;
102 0         0 $from = $to;
103             }
104              
105 1 50 33     7 return $self
106             if $trans_was eq $trans_to && $char_was eq $char_to;
107             }
108             }
109             elsif($trans_was eq $trans_to)
110             { # No changes needed;
111 0         0 return $self;
112             }
113              
114             ### Apply transfer-decoding
115              
116 66         114 my $decoded;
117 66 100       163 if($trans_was eq 'none')
    50          
118 58         89 { $decoded = $self }
119             elsif(my $decoder = $self->getTransferEncHandler($trans_was))
120 8         38 { $decoded = $decoder->decode($self, result_type => $bodytype) }
121             else
122 0         0 { $self->log(WARNING =>
123             "No decoder defined for transfer encoding $trans_was.");
124 0         0 return $self;
125             }
126              
127             ### Apply character-set recoding
128              
129 66         101 my $recoded;
130 66 100       123 if($is_text)
131 63 50       135 { unless($char_was)
132             { # When we do not know the character-sets, try to auto-detect
133 0   0     0 my $auto = $args{charset_detect} || $self->charsetDetectAlgorithm;
134 0         0 $char_was = $decoded->$auto;
135 0         0 $from = $self->_char_enc($char_was);
136 0         0 $decoded->type->attribute(charset => $char_was);
137              
138 0 0       0 unless($char_to)
139 0         0 { $char_to = $char_was;
140 0         0 $type_to->attribute(charset => $char_to);
141 0         0 $to = $from;
142             }
143             }
144              
145 63 50 100     384 my $new_data
    100 100        
    100 66        
146             = $to && $char_was eq 'PERL' ? $to->encode($decoded->string)
147             : $from && $char_to eq 'PERL' ? $from->decode($decoded->string)
148             : $to && $from && $char_was ne $char_to ? $to->encode($from->decode($decoded->string))
149             : undef;
150              
151 63 100       289 $recoded
152             = $new_data
153             ? $bodytype->new(based_on => $decoded, data => $new_data,
154             mime_type => $type_to, checked => 1)
155             : $decoded;
156             }
157             else
158 3         8 { $recoded = $decoded;
159             }
160              
161             ### Apply transfer-encoding
162              
163 66         129 my $trans;
164 66 100       162 if($trans_to ne 'none')
165 34 50       114 { $trans = $self->getTransferEncHandler($trans_to)
166             or $self->log(WARNING =>
167             "No encoder defined for transfer encoding `$trans_to'.");
168             }
169              
170 66 100       222 my $encoded = defined $trans
171             ? $trans->encode($recoded, result_type => $bodytype)
172             : $recoded;
173              
174 66         225 $encoded;
175             }
176              
177              
178             sub charsetDetectAlgorithm(;$)
179 32     32 1 61 { my $self = shift;
180 32 50       74 $self->{MMBE_det} = shift if @_;
181 32 50       161 $self->{MMBE_det} || 'charsetDetect';
182             }
183              
184              
185             sub charsetDetect(%)
186 32     32 1 114 { my ($self, %args) = @_;
187 32         132 my $text = $self->string;
188              
189             # Flagged as UTF8, so certainly created by the Perl program itself:
190             # the content is not octets.
191 32 100       101 if(utf8::is_utf8($text))
192 6 50       18 { $args{external} or return 'PERL';
193 6         22 $text = encode_utf8 $text;
194             }
195              
196             # Only look for normal characters, first 1920 unicode characters
197             # When there is any octet in 'utf-encoding'-space, but not an
198             # legal utf8, than it's not utf8.
199             #XXX Use the fact that cp1252 does not define (0x81, 0x8d, 0x8f, 0x90, 0x9d) ?
200 32 50 33     262 return 'utf-8'
201             if $text =~ m/[\0xC0-\xDF][\x80-\xBF]/ # 110xxxxx, 10xxxxxx
202             && $text !~ m/[\0xC0-\xFF]([^\0x80-\xBF]|$)/;
203              
204             # Produce 'us-ascii' when it suffices: it is the RFC compliant
205             # default charset.
206 32 50       132 $text =~ m/[\x80-\xFF]/ ? 'cp1252' : 'us-ascii';
207             }
208              
209              
210              
211             sub check()
212 23     23 1 41 { my $self = shift;
213 23 100       78 return $self if $self->checked;
214 1         10 my $eol = $self->eol;
215              
216 1         4 my $encoding = $self->transferEncoding->body;
217 1 50       6 return $self->eol($eol)
218             if $encoding eq 'none';
219              
220 1         4 my $encoder = $self->getTransferEncHandler($encoding);
221              
222 1 50       8 my $checked
223             = $encoder
224             ? $encoder->check($self)->eol($eol)
225             : $self->eol($eol);
226              
227 1         5 $checked->checked(1);
228 1         4 $checked;
229             }
230              
231             #------------------------------------------
232              
233              
234             sub encoded(%)
235 55     55 1 124 { my ($self, %args) = @_;
236              
237 55   66     216 $mime_types ||= MIME::Types->new;
238 55         345 my $mime = $mime_types->type($self->type->body);
239              
240 55   100     1866 my $charset = my $old_charset = $self->charset || '';
241 55 100 100     628 if(!$charset || $charset eq 'PERL')
242 32   33     163 { my $auto = $args{charset_detect} || $self->charsetDetectAlgorithm;
243 32         134 $charset = $self->$auto(external => 1);
244             }
245              
246 55         186 my $enc_was = $self->transferEncoding;
247 55         107 my $enc = $enc_was;
248 55 50       489 $enc = defined $mime ? $mime->encoding : 'base64'
    100          
249             if $enc eq 'none';
250              
251             # we could (expensively) try to autodetect character-set used,
252             # but everything is a subset of utf-8.
253 55 100 66     251 my $new_charset = (!$mime || $mime !~ m!^text/!i) ? '' : $charset;
254              
255 55 100 100     677 ($enc_was ne 'none' && $old_charset eq $new_charset)
256             ? $self->check
257             : $self->encode(transfer_encoding => $enc, charset => $new_charset);
258             }
259              
260             #------------------------------------------
261              
262              
263             sub unify($)
264 0     0 1 0 { my ($self, $body) = @_;
265 0 0       0 return $self if $self==$body;
266              
267 0         0 my $mime = $self->type;
268 0         0 my $transfer = $self->transferEncoding;
269              
270 0         0 my $encoded = $body->encode
271             ( mime_type => $mime
272             , transfer_encoding => $transfer
273             );
274              
275             # Encode makes the best of it, but is it good enough?
276              
277 0         0 my $newmime = $encoded->type;
278 0 0       0 return unless $newmime eq $mime;
279 0 0       0 return unless $transfer eq $encoded->transferEncoding;
280 0         0 $encoded;
281             }
282              
283             #------------------------------------------
284              
285              
286             sub isBinary()
287 11     11 1 26 { my $self = shift;
288 11   66     41 $mime_types ||= MIME::Types->new(only_complete => 1);
289 11 50       58 my $type = $self->type or return 1;
290 11 50       39 my $mime = $mime_types->type($type->body) or return 1;
291 11         394 $mime->isBinary;
292             }
293            
294              
295 0     0 1 0 sub isText() { not shift->isBinary }
296              
297              
298             sub dispositionFilename(;$)
299 1     1 1 3 { my $self = shift;
300 1         4 my $raw;
301              
302             my $field;
303 1 50       7 if($field = $self->disposition)
304 1 50       10 { $field = $field->study if $field->can('study');
305 1   33     9 $raw = $field->attribute('filename')
306             || $field->attribute('file')
307             || $field->attribute('name');
308             }
309              
310 1 50 33     11 if(!defined $raw && ($field = $self->type))
311 1 50       18 { $field = $field->study if $field->can('study');
312 1   33     4 $raw = $field->attribute('filename')
313             || $field->attribute('file')
314             || $field->attribute('name');
315             }
316              
317 1         5 my $base;
318 1 50 33     6 if(!defined $raw || !length $raw) {}
    0          
319             elsif(index($raw, '?') >= 0)
320 0         0 { eval 'require Mail::Message::Field::Full';
321 0         0 $base = Mail::Message::Field::Full->decode($raw);
322             }
323             else
324 0         0 { $base = $raw;
325             }
326              
327 1 50       6 return $base
328             unless @_;
329              
330 1         2 my $dir = shift;
331 1         2 my $filename = '';
332 1 50       4 if(defined $base) # RFC6266 section 4.3, very safe
333 0         0 { $filename = basename $base;
334 0         0 for($filename)
335 0         0 { s/\s+/ /g; s/ $//; s/^ //;
  0         0  
  0         0  
336 0         0 s/[^\w .-]//g;
337             }
338             }
339              
340 1 50 33     7 my ($filebase, $ext) = length $filename && $filename =~ m/(.*)\.([^.]+)/
      50        
341             ? ($1, $2) : (part => ($self->mimeType->extensions)[0] || 'raw');
342              
343 1         65 my $fn = File::Spec->catfile($dir, "$filebase.$ext");
344              
345 1         44 for(my $unique = 1; -e $fn; $unique++)
346 0         0 { $fn = File::Spec->catfile($dir, "$filebase-$unique.$ext");
347             }
348              
349 1         9 $fn;
350             }
351              
352             #------------------------------------------
353              
354              
355             my %transfer_encoder_classes =
356             ( base64 => 'Mail::Message::TransferEnc::Base64'
357             , binary => 'Mail::Message::TransferEnc::Binary'
358             , '8bit' => 'Mail::Message::TransferEnc::EightBit'
359             , 'quoted-printable' => 'Mail::Message::TransferEnc::QuotedPrint'
360             , '7bit' => 'Mail::Message::TransferEnc::SevenBit'
361             );
362              
363             my %transfer_encoders; # they are reused.
364              
365             sub getTransferEncHandler($)
366 43     43 1 113 { my ($self, $type) = @_;
367              
368             return $transfer_encoders{$type}
369 43 100       188 if exists $transfer_encoders{$type}; # they are reused.
370              
371 11         30 my $class = $transfer_encoder_classes{$type};
372 11 50       30 return unless $class;
373              
374 11         773 eval "require $class";
375 11 50       63 confess "Cannot load $class: $@\n" if $@;
376              
377 11         124 $transfer_encoders{$type} = $class->new;
378             }
379              
380              
381             sub addTransferEncHandler($$)
382 0     0 1   { my ($this, $name, $what) = @_;
383              
384 0           my $class;
385 0 0         if(ref $what)
386 0           { $transfer_encoders{$name} = $what;
387 0           $class = ref $what;
388             }
389             else
390 0           { delete $transfer_encoders{$name};
391 0           $class = $what;
392             }
393              
394 0           $transfer_encoder_classes{$name} = $class;
395 0           $this;
396             }
397              
398             1;