File Coverage

blib/lib/Mail/Message/Body/Encode.pm
Criterion Covered Total %
statement 155 195 79.4
branch 82 130 63.0
condition 47 91 51.6
subroutine 25 29 86.2
pod 12 12 100.0
total 321 457 70.2


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