File Coverage

blib/lib/MIME/Mini.pm
Criterion Covered Total %
statement 467 526 88.7
branch 261 336 77.6
condition 153 243 62.9
subroutine 59 66 89.3
pod 25 55 45.4
total 965 1226 78.7


line stmt bran cond sub pod time code
1             # MIME::Mini - Minimal code to parse/create mbox files and mail messages
2             #
3             # Copyright (C) 2005-2007, 2023 raf
4             #
5             # This is free software; you can redistribute it and/or modify it under
6             # the same terms as the Perl 5 programming language system itself.
7             #
8             # 20230510 raf
9              
10             package MIME::Mini;
11 2     2   1810 use 5.014;
  2         17  
12 2     2   10 use strict;
  2         5  
  2         53  
13 2     2   9 use warnings;
  2         4  
  2         71  
14              
15             our $VERSION = '1.001';
16              
17 2     2   11 use Exporter;
  2         5  
  2         5307  
18             our @ISA = ('Exporter');
19              
20             our @EXPORT = ();
21             our @EXPORT_OK = qw(
22             formail mail2str mail2multipart mail2singlepart mail2mbox
23             insert_header append_header replace_header delete_header
24             insert_part append_part replace_part delete_part
25             header headers header_names
26             param mimetype encoding filename
27             body message parts
28             newparam newmail
29             );
30             our %EXPORT_TAGS = (all => [@EXPORT_OK]);
31              
32             sub formail # rfc2822 + mboxrd format (see http://www.qmail.org/man/man5/mbox.html)
33             {
34             sub mime # rfc2045, rfc2046
35             {
36 178     178 0 394 my ($mail, $parent) = @_;
37 178 100 100     1151 return $mail unless exists $mail->{header} && exists $mail->{header}->{'content-type'} || defined $parent && exists $parent->{mime_type} && $parent->{mime_type} =~ /^multipart\/digest$/i;
      66        
      66        
      100        
38 136 100 66     664 my ($content_type) = (exists $mail->{header} && exists $mail->{header}->{'content-type'}) ? @{$mail->{header}->{'content-type'}} : "Content-Type: message/rfc822\n";
  130         313  
39 136         723 my ($type) = $content_type =~ /^content-type:\s*([\w\/.-]+)/i;
40 136 100       609 my $boundary = param($mail, 'content-type', 'boundary') if $type =~ /^multipart\//i;
41 136 100 100     1143 return $mail unless defined $type && ($type =~ /^multipart\//i && $boundary || $type =~ /^message\/rfc822$/i);
      66        
42 82 100       345 ($mail->{mime_boundary}) = $boundary =~ /^(.*\S)/ if $boundary;
43 82         178 $mail->{mime_type} = $type;
44 82 100 100     403 $mail->{mime_message} = mimepart(delete $mail->{body} || '', $mail), return $mail if $type =~ /^message\/(?:rfc822|external-body)$/i;
45 62         170 return tnef2mime(mimeparts($mail, $parent));
46             }
47              
48             sub mimeparts
49             {
50 62     62 0 119 my ($mail, $parent) = @_;
51 62         108 my $state = 'preamble';
52 62         104 my $text = '';
53              
54 62   50     2544 for (split /(?<=\n)/, delete $mail->{body} || '')
55             {
56 872 100       3455 if (/^--\Q$mail->{mime_boundary}\E(--)?/)
57             {
58 146 100       366 if ($state eq 'preamble')
    50          
59             {
60 62         91 $state = 'part';
61 62 100       179 $mail->{mime_preamble} = $text if length $text;
62             }
63             elsif ($state eq 'part')
64             {
65 84 100 66     435 $state = 'epilogue' if defined $1 && $1 eq '--';
66 84         123 push @{$mail->{mime_parts}}, mimepart($text, $mail);
  84         246  
67             }
68              
69 146         357 $text = '', next;
70             }
71              
72 726         1221 $text .= $_;
73             }
74              
75 62 100 100     283 push @{$mail->{mime_parts}}, mimepart($text, $mail) if $state eq 'part' && length $text;
  6         52  
76 62 100 100     371 $mail->{mime_epilogue} = $text if $state eq 'epilogue' && length $text;
77 62         338 return $mail;
78             }
79              
80             sub mimepart
81             {
82 110     110 0 256 my ($mail, $parent) = @_;
83 110         2133 my @lines = split /(?<=\n)/, $mail;
84             # Needed to cope (badly) when message/rfc822 attachments incorrectly start with /^From / (thanks libpst)
85 110 100       302 @lines = ('') unless @lines;
86 110     844   926 formail(sub { shift @lines }, sub { $mail = shift }, $parent);
  844         1722  
  110         240  
87 110         518 return $mail;
88             }
89              
90 140     140 1 237163 my ($rd, $act, $parent) = @_;
91 140         404 my $state = 'header';
92 140         284 my $mail; my $last;
93              
94 140         484 while (defined($_ = $rd->()))
95             {
96 2366         9527 s/\r(?=\n)//g; #, tr/\r/\n/;
97              
98 2366 100 100     6994 if (!defined $parent && /^From (?:\S+\s+)?\s*[a-zA-Z]+\s+[a-zA-Z]+\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+(?:[A-Z]+\s+)?\d{4}/) # mbox header
99             {
100 68 50 66     671 $mail->{body} =~ s/\n\n\z/\n/ if $mail && exists $mail->{mbox} && exists $mail->{body};
      33        
101 68 100 50     227 my $mbox = $_; $act->(mime($mail, $parent)) or return if $mail;
  68         267  
102 68         1220 $mail = { mbox => $mbox }, $state = 'header', undef $last, next;
103             }
104              
105 2298 100       4324 if ($state eq 'header')
    50          
106             {
107 950 100       2779 if (/^([\w-]+):/) # mail header
    100          
108             {
109 682         889 push @{$mail->{headers}}, $_;
  682         1662  
110 682         931 push @{$mail->{header}->{$last = lc $1}}, $_;
  682         3562  
111             }
112             elsif (/^$/) # blank line after mail headers
113             {
114 174         492 $mail->{body} = '', $state = 'body';
115             }
116             else # mail header continuation or error
117             {
118 94 50       171 ${$mail->{headers}}[$#{$mail->{headers}}] .= $_ if defined $last;
  94         241  
  94         156  
119 94 50       202 ${$mail->{header}->{$last}}[$#{$mail->{header}->{$last}}] .= $_ if defined $last;
  94         265  
  94         164  
120             }
121             }
122             elsif ($state eq 'body')
123             {
124 1348 100       2565 s/^>(>*From )/$1/ if exists $mail->{mbox};
125 1348         2793 $mail->{body} .= $_;
126             }
127             }
128              
129 140 100 100     1624 $mail->{body} =~ s/\n\n\z/\n/ if $mail && exists $mail->{mbox} && exists $mail->{body};
      100        
130 140 100       494 $act->(mime($mail, $parent)) if $mail;
131             }
132              
133             sub mail2str
134             {
135 272     272 1 525 my $mail = shift;
136 272         429 my $head = '';
137 272 100       635 $head .= $mail->{mbox} if exists $mail->{mbox};
138 272 100       548 $head .= join '', @{$mail->{headers}} if exists $mail->{headers};
  230         673  
139 272         456 my $body = '';
140 272 100       545 $body .= $mail->{body} if exists $mail->{body};
141 272 100       514 $body .= "$mail->{mime_preamble}" if exists $mail->{mime_preamble};
142 272 100 100     778 $body .= "--$mail->{mime_boundary}\n" if exists $mail->{mime_boundary} && !exists $mail->{mime_parts};
143 272 100       597 $body .= join('', map { "--$mail->{mime_boundary}\n" . mail2str($_) } @{$mail->{mime_parts}}) if exists $mail->{mime_parts};
  132         349  
  70         133  
144 272 100       567 $body .= "--$mail->{mime_boundary}--\n" if exists $mail->{mime_boundary};
145 272 100       507 $body .= "$mail->{mime_epilogue}" if exists $mail->{mime_epilogue};
146 272 100       558 $body .= mail2str($mail->{mime_message}) if exists $mail->{mime_message};
147 272 100       1346 $body =~ s/^(>*From )/>$1/mg, $body =~ s/([^\n])\n?\z/$1\n\n/ if exists $mail->{mbox};
148 272         1939 return $head . "\n" . $body;
149             }
150              
151             my $bchar = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-.\/:=?";
152             sub mail2multipart
153             {
154 24     24 1 182 my $m = shift;
155 24 100 100     215 return $m if exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i;
156 12         42 my $p = {};
157 12         28 append_header($p, $_) for grep { /^content-/i } @{$m->{headers}};
  60         203  
  12         36  
158 12 100       78 $p->{body} = delete $m->{body} if exists $m->{body};
159 12 100       60 $p->{mime_message} = delete $m->{mime_message} if exists $m->{mime_message};
160 12 100       43 $p->{mime_type} = $m->{mime_type} if exists $m->{mime_type};
161 12         38 $m->{mime_type} = 'multipart/mixed';
162 12 50       54 $m->{mime_boundary} = exists $m->{mime_prev_boundary} ? delete $m->{mime_prev_boundary} : join '', map { substr $bchar, int(rand(length $bchar)), 1 } 0..30;
  372         868  
163 12 50       78 $m->{mime_preamble} = delete $m->{mime_prev_preamble} if exists $m->{mime_prev_preamble};
164 12 50       38 $m->{mime_epilogue} = delete $m->{mime_prev_epilogue} if exists $m->{mime_prev_epilogue};
165 12         161 delete_header($m, qr/content-[^:]*/i);
166 12 100 66     160 append_header($m, 'MIME-Version: 1.0') unless exists $m->{header} && exists $m->{header}->{'mime-version'};
167 12         93 append_header($m, "Content-Type: $m->{mime_type}; boundary=\"$m->{mime_boundary}\"");
168 12         59 $m->{mime_parts} = [$p];
169 12         72 return $m;
170             }
171              
172             sub mail2singlepart
173             {
174 40     40 1 152 my $m = shift;
175 40 100 100     281 $m->{mime_message} = mail2singlepart($m->{mime_message}), return $m if exists $m->{mime_type} && $m->{mime_type} =~ /^message\//i;
176 36 100 66     263 return $m unless exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i && @{$m->{mime_parts}} <= 1;
  20   100     93  
177 16         34 my $p = shift @{$m->{mime_parts}};
  16         32  
178 16 50       80 $m->{mime_prev_boundary} = delete $m->{mime_boundary} if exists $m->{mime_boundary};
179 16 100       75 $m->{mime_prev_preamble} = delete $m->{mime_preamble} if exists $m->{mime_preamble};
180 16 50       52 $m->{mime_prev_epilogue} = delete $m->{mime_epilogue} if exists $m->{mime_epilogue};
181 16 100       60 $m->{body} = $p->{body} if exists $p->{body};
182 16 100       42 $m->{mime_message} = $p->{mime_message} if exists $p->{mime_message};
183 16 100       31 delete $m->{mime_type}; $m->{mime_type} = $p->{mime_type} if exists $p->{mime_type};
  16         42  
184 16 100       35 delete $m->{mime_parts}; $m->{mime_parts} = $p->{mime_parts} if exists $p->{mime_parts};
  16         38  
185 16 100       43 $m->{mime_boundary} = $p->{mime_boundary} if exists $p->{mime_boundary};
186 16 50       38 $m->{mime_preamble} = $p->{mime_preamble} if exists $p->{mime_preamble};
187 16 50       33 $m->{mime_epilogue} = $p->{mime_epilogue} if exists $p->{mime_epilogue};
188 16         28 my $explicit = 0;
189 16         86 delete_header($m, qr/content-[^:]*/i);
190 16         36 append_header($m, $_), ++$explicit for grep { /^content-/i } @{$p->{headers}};
  18         100  
  16         38  
191 16 100       52 delete_header($m, 'mime-version') unless $explicit;
192 16         51 return mail2singlepart($m);
193             }
194              
195             sub mail2mbox
196             {
197 58     58 1 446 my $m = shift;
198 58 100       227 return $m if exists $m->{mbox};
199 20         45 my ($f) = header($m, 'sender');
200 20 50       65 ($f) = header($m, 'from') unless defined $f;
201 20 50       86 $f =~ s/"(?:\\[^\r\n]|[^\\"])*"//g, $f =~ s/\s*;.*//, $f =~s/^[^:]+:\s*//, $f =~ s/\s*,.*$//, $f =~ s/^[^<]*<\s*//, $f =~ s/\s*>.*$// if defined $f;
202 20 50       39 $f = 'unknown' unless defined $f;
203 2     2   1054 use POSIX; $m->{mbox} = "From $f " . ctime(time());
  2         16043  
  2         10  
  20         564  
204 20         104 return $m;
205             }
206              
207             sub insert_header
208             {
209 4     4 1 21 my ($m, $h, $l, $c) = @_;
210 4         11 $h = header_format($h, $l, $c);
211 4         17 my ($n) = $h =~ /^([^:]+):/;
212 4         8 unshift @{$m->{headers}}, $h;
  4         12  
213 4         6 unshift @{$m->{header}->{lc $n}}, $h;
  4         17  
214             }
215              
216             sub append_header
217             {
218 305     305 1 28561 my ($m, $h, $l, $c) = @_;
219 305         622 $h = header_format($h, $l, $c);
220 305         1188 my ($n) = $h =~ /^([^:]+):/;
221 305         462 push @{$m->{headers}}, $h;
  305         699  
222 305         440 push @{$m->{header}->{lc $n}}, $h;
  305         1253  
223             }
224              
225             sub replace_header
226             {
227 20     20 1 70 my ($m, $h, $l, $c) = @_;
228 20         58 $h = header_format($h, $l, $c);
229 20         94 my ($n) = $h =~ /^([^:]+):/;
230 20 50       33 my $seen = 0; @{$m->{headers}} = grep { defined $_ } map { /^\Q$n\E:/i ? $seen ? undef : do { ++$seen; $h } : $_ } @{$m->{headers}};
  20 100       28  
  20         42  
  20         38  
  20         170  
  6         11  
  6         12  
  20         44  
231 20         32 splice @{$m->{header}->{lc $n}};
  20         58  
232 20         30 push @{$m->{header}->{lc $n}}, $h;
  20         60  
233             }
234              
235             sub delete_header
236             {
237 46     46 1 178 my ($m, $h, $r) = @_;
238 46 100       154 return undef unless exists $m->{header};
239 44         101 @{$m->{headers}} = grep { !/^$h:/i } @{$m->{headers}};
  44         144  
  222         1137  
  44         103  
240 44         74 delete $m->{header}->{$_} for grep { /^$h$/i } keys %{$m->{header}};
  222         917  
  44         169  
241 44 100 100     141 if ($r && exists $m->{mime_parts}) { delete_header($_, $h, $r) for @{$m->{mime_parts}} }
  2         6  
  2         19  
242 44 100 100     141 if ($r && exists $m->{mime_message}) { delete_header($m->{mime_message}, $h, $r) }
  2         9  
243             }
244              
245             sub insert_part
246             {
247 2     2 1 21 my ($m, $p, $i) = @_;
248 2   50     4 splice @{$m->{mime_parts}}, $i || 0, 0, $p;
  2         13  
249             }
250              
251             sub append_part
252             {
253 10     10 1 26 my ($m, $p) = @_;
254 10         16 push @{$m->{mime_parts}}, $p;
  10         28  
255             }
256              
257             sub replace_part
258             {
259 2     2 1 16 my ($m, $p, $i) = @_;
260 2         4 splice @{$m->{mime_parts}}, $i, 1, $p;
  2         11  
261             }
262              
263             sub delete_part
264             {
265 2     2 1 8 my ($m, $i) = @_;
266 2         5 splice @{$m->{mime_parts}}, $i, 1;
  2         8  
267             }
268              
269             sub header
270             {
271 359     359 1 14569 my ($m, $h) = @_;
272 359 100 100     1702 return () unless exists $m->{header} && exists $m->{header}->{lc $h};
273 257         385 return map { s/\n\s+/ /g; header_display($_) =~ /^$h:\s*(.*)\s*$/i; $1 } @{$m->{header}->{lc $h}};
  257         681  
  257         540  
  257         1378  
  257         606  
274             }
275              
276             sub headers
277             {
278 2     2 1 7 my $m = shift;
279 2 50       10 return () unless exists $m->{headers};
280 2         7 return map { s/\n\s+/ /g; header_display($_) =~ /^([\w-]+:.*)\s*$/; $1 } @{$m->{headers}};
  8         15  
  8         14  
  8         39  
  2         6  
281             }
282              
283             sub header_names
284             {
285 2     2 1 7025 my $m = shift;
286 2 50       14 return () unless exists $m->{header};
287 2         4 return keys %{$m->{header}};
  2         20  
288             }
289              
290             my $encword = qr/=\?([^*?]+)(?:\*\w+)?\?(q|b)\?([^? ]+)\?=/i; # encoded words to display
291             sub header_display # rfc2047, rfc2231
292             {
293 2     2   9016 use Encode ();
  2         19959  
  2         939  
294             return join '',
295 525         871 map { tr/ \t/ /s; $_ } # finally, squeeze multiple whitespace
  525         3352  
296 525         1035 map { tr/\x00-\x08\x0b-\x1f\x7f//d; $_ } # strip control characters
  525         909  
297 525 100       1585 map { s/$encword/(defined Encode::find_encoding($1)) ? Encode::decode($1, (lc $2 eq 'q') ? decode_quoted_printable($3, 1) : decode_base64($3)) : $&/ieg; $_ } # decode encoded words if possible
  29 50       516  
  525         1569  
298 525         2747 map { s/($encword)\s+($encword)/$1$5/g while /$encword\s+$encword/; $_ } # strip space between encoded words that we're about to decode
  525         964  
299 273 100   273 0 9940 map { s/\((?:\\[^\r\n]|[^\\()])*\)//g unless /^".*"$/; $_ } # strip (comments) outside "quoted strings"
  525         1592  
  525         1185  
300             split /("(?:\\[^\r\n]|[^\\"])*")/, shift; # split on "quoted strings"
301             }
302              
303             sub charsetof
304             {
305 87     87 0 165 my $s = shift;
306 87 100 100     473 return 'us-ascii' if !defined $s || $s =~ /^[\x00-\x7f]*$/;
307             #return 'utf-8' if $s =~ /^(?:[\x00-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf4][\x80-\xbf]{3})+$/; # This won't work until perl v5.38
308 33 100       54 return 'utf-8' if defined eval { Encode::decode 'UTF-8', $s, Encode::FB_CROAK };
  33         89  
309 32 50 33     1913 return (defined $ENV{LANG} && $ENV{LANG} =~ /^.+\.(.+)$/) && $1 ne 'UTF-8' ? lc $1 : 'iso-8859-1'; # Make something up
310             }
311              
312             sub header_format # rfc2822, rfc2047
313             {
314 329     329 0 661 my ($h, $l, $c) = @_;
315 329         1496 $h =~ s/^\s+//, $h =~ s/\s+$//, $h =~ tr/ \t\n\r/ /s;
316 2 100   2   17 use Encode (); $h = Encode::encode('UTF-8', $h) if grep { ord > 255 } split //, $h;
  2         4  
  2         4115  
  329         2319  
  12186         17697  
317 329 50 33     2502 $h = join ' ', map { /^".*"$/ ? $_ : !tr/\x80-\xff// ? $_ : tr/a-zA-Z0-9!*\/+-//c > length >> 1 ? join(' ', map { '=?' . ($c || charsetof($h)) . ($l ? "*$l" : '') . '?b?' . substr(encode_base64($_), 0, -1) . '?=' } (split /\n/, (s/([^\r\n]{38})/$1\n/g, $_))) : join(' ', map { '=?' . ($c || charsetof($h)) . ($l ? "*$l" : '') . '?q?' . substr(encode_quoted_printable($_, 1), 0, -1) . '?=' } (split /\n/, (s/([^\r\n]{17})/$1\n/g, $_))) } map { /^[^\s"]*".*"[^\s"]*$/ ? $_ : split / / } split /(\S*"(?:\\[^\r\n]|[^\\"])*"\S*)/, $h;
  1023 50 33     3115  
  6 100       22  
  13 100       47  
  392 50       1456  
    100          
318 329 100 100     822 my ($f, $p, $lf) = ('', 0); $lf = length $f, $f .= ($lf && $lf + ($lf ? 1 : 0) + length($_) - $p > 78) ? ($p = $lf, "\n") : '', $f .= $f ? ' ' : '', $f .= $_ for map { /^\S*".*"\S*$/ ? $_ : grep { length } split / / } split /(\S*"(?:\\[^\r\n]|[^\\"\r\n])*"\S*)/, $h; # fold
  329 100       1336  
  392 100       1342  
  994         4446  
319 329         967 return $f . "\n";
320             }
321              
322             sub param # rfc2231, rfc2045
323             {
324 80     80 1 292 my ($m, $h, $p) = @_;
325 80         190 my @p; my $decode = 0;
  80         128  
326              
327 80         422 for (header($m, $h))
328             {
329 80         2390 while (/(\b\Q$p\E(?:\*|\*\d\*?)?)=("(?:\\[^\n]|[^"\n])*"|[^\x00-\x20()<>@,;:\\"\/\[\]?=]+)/ig)
330             {
331 98         564 my ($n, $v) = ($1, $2);
332 98 100       832 $v =~ s/^"//, $v =~ s/"$//, $v =~ s/\\(.)/$1/g if $v =~ /^".*"$/;
333 98 100       325 $v =~ s/^(?:us-ascii|utf-8|iso-8859-\d{1,2})'\w+'//i and $decode = 1;
334 98 100 100     341 $v =~ s/%([\da-fA-f]{2})/chr hex $1/eg if $decode && substr($n, -1) eq '*';
  38         137  
335 98         835 push @p, [lc $n, $v];
336             }
337             }
338              
339 80         283 return join '', map { $_->[1] } sort { my ($ad) = $a->[0] =~ /(\d+)/; my ($bd) = $b->[0] =~ /(\d+)/; $ad <=> $bd } @p;
  98         548  
  26         80  
  26         76  
  26         66  
340             }
341              
342             sub mimetype # rfc2045, rfc2046
343             {
344 100     100 1 262 my ($m, $p) = @_;
345 100         225 my ($e) = header($m, 'content-transfer-encoding');
346 100 100 100     630 return 'application/octet-stream' if defined $e && $e !~ /^(?:[78]bit|binary|quoted-printable|base64)$/i;
347 98         213 my ($type) = header($m, 'content-type');
348 98 100 100     1078 return lc $1 if defined $type && $type =~ /^((?:text|image|audio|video|application|message|multipart)\/[^\s;]+)/i;
349 22 50 100     238 return 'message/rfc822' if !defined $type && defined $p && exists $p->{mime_type} && $p->{mime_type} =~ /^multipart\/digest/i;
      66        
      33        
350 20         108 return 'text/plain';
351             }
352              
353             sub encoding # rfc2045
354             {
355 22     22 1 5685 my $m = shift;
356 22         56 my ($e) = header($m, 'content-transfer-encoding');
357 22 100 100     280 return (defined $e && $e =~ /^([78]bit|binary|quoted-printable|base64)$/i) ? lc $1 : (exists $m->{body} && $m->{body} =~ tr/\x80-\xff//) ? '8bit' : '7bit';
    100 66        
358             }
359              
360             my $filename_counter;
361             sub filename # rfc2183, rfc2045?
362             {
363 4     4 1 21 my $p = shift;
364 4   33     21 my $fn = param($p, 'content-disposition', 'filename') || param($p, 'content-type', 'name') || 'attachment' . ++$filename_counter;
365 4         29 $fn =~ s/^.*[\\\/]//, $fn =~ tr/\x00-\x1f !"#\$%&'()*\/:;<=>?@[\\]^`{|}~\x7f/_/s;
366 4         56 return $fn;
367             }
368              
369             sub body
370             {
371 8     8 1 45 my $m = shift;
372 8 100       53 return exists $m->{body} ? decode($m->{body}, encoding($m)) : undef;
373             }
374              
375             sub message
376             {
377 6     6 1 17 my $m = shift;
378 6 100       48 return exists $m->{mime_message} ? $m->{mime_message} : undef;
379             }
380              
381             sub parts
382             {
383 12     12 1 40 my ($m, $p) = @_;
384 12 100       62 return exists $m->{mime_parts} ? [@{$m->{mime_parts}}] : [] unless defined $p;
  6 100       33  
385 2         5 $m->{mime_parts} = [@{$p}];
  2         9  
386             }
387              
388             sub newparam # rfc2231, rfc2045
389             {
390 46     46 1 7577 my ($n, $v, $l, $c) = (@_, '', '');
391 46         82 my $high = $v =~ tr/\x80-\xff//;
392 46         71 my $ctrl = $v =~ tr/\x00-\x06\x0e-\x1f\x7f//;
393 46 100 66     146 my $enc = $high || $ctrl ? '*' : '';
394 46 100 66     107 $c = charsetof($v) if $enc && !$c;
395 46 100 100     99 $l = 'en' if $c && !$l;
396 46 100       106 $v = "$c'$l'$v" if $enc;
397 46         75 my @p; push @p, $_ while $_ = substr $v, 0, 40, '';
  46         218  
398 46         83 s/([\x00-\x20\x7f-\xff])/sprintf '%%%02X', ord $1/eg for grep { tr/\x00-\x06\x0e-\x1f\x7f-\xff// } @p;
  58         140  
  28         98  
399 46         68 s/"/\\"/g, s/^/"/g, s/$/"/g for grep { tr/\x00-\x06\x0e-\x1f\x7f ()<>@,;:\\"\/[]?=// } @p;
  58         196  
400 46 100       300 return "; $n$enc=$p[0]" if @p == 1;
401 6         18 return join '', map { "; $n*$_$enc=$p[$_]" } 0..$#p;
  18         85  
402             }
403              
404             my $messageid_counter;
405             sub newmail # rfc2822, rfc2045, rfc2046, rfc2183 (also rfc3282, rfc3066, rfc2424, rfc2557, rfc2110, rfc3297, rfc2912, rfc2533, rfc1864)
406             {
407 64     64 1 16331 my @a = @_; my %a = @_; my $m = {};
  64         182  
  64         109  
408 2     2 0 18 sub rfc822date { use POSIX; return strftime '%a, %d %b %Y %H:%M:%S +0000', gmtime shift; }
  2     36   4  
  2         13  
  36         1588  
409 64   66     331 my $type = $a{type} || (exists $a{parts} ? 'multipart/mixed' : exists $a{message} ? 'message/rfc822' : 'text/plain');
410 64         179 my $multi = $type =~ /^multipart\//i;
411 64         136 my $msg = $type =~ /^message\/rfc822$/i;
412 64 50 100     301 if (exists $a{filename} && !exists $a{body} && !exists $a{message} && !exists $a{parts} && -r $a{filename} && stat($a{filename}) && open my $fh, '<', $a{filename})
      66        
      66        
      33        
      33        
      33        
413             {
414 2         7 $a{body} = do { local $/; my $b = <$fh>; close $fh; $b };
  2         12  
  2         50  
  2         23  
  2         14  
415 2 50       15 $a{created} = (exists $a{created}) ? $a{created} : rfc822date((stat _)[9]);
416 2 50       21 $a{modified} = (exists $a{modified}) ? $a{modified} : rfc822date((stat _)[9]);
417 2 50       13 $a{read} = (exists $a{read}) ? $a{read} : rfc822date((stat _)[8]);
418 2         16 $a{size} = (stat _)[7];
419             }
420 64 100       172 ($a{filename}) = $a{filename} =~ /([^\\\/]+)$/ if $a{filename};
421 64 100       123 my $bound = $multi ? join '', map { substr $bchar, int(rand(length $bchar)), 1 } 0..30 : '';
  186         452  
422 64   33     312 my $disp = $a{disposition} || ($type =~ /^(?:text\/|message\/rfc822)/i ? 'inline' : 'attachment');
423 64   33     208 my $char = $a{charset} || charsetof($a{body});
424 64   33     326 my $enc = $a{encoding} || ($multi || $msg ? '7bit' : $a{body} ? choose_encoding($a{body}) : '7bit');
425 64 100       163 append_header($m, $a[$_] . ': ' . $a[$_ + 1]) for grep { $_ % 2 == 0 && $a[$_] =~ /^[A-Z]/ } 0..$#a;
  320         1110  
426 64 100 66     179 append_header($m, 'Date: ' . rfc822date(time)) if grep { /^(?:date|from|sender|reply-to)$/i } keys %a and !grep { /^date$/i } keys %a;
  170         513  
  122         301  
427 64 100 66     173 append_header($m, 'MIME-Version: 1.0') if grep { /^(?:date|from|sender|reply-to)$/i } keys %a and !grep { /^mime-version$/ } keys %a;
  170         554  
  122         267  
428 2 100 66 2   8553 use Sys::Hostname; append_header($m, "Message-ID: <@{[time]}.$$.@{[++$messageid_counter]}\@@{[hostname]}>") if grep { /^(?:date|from|sender|reply-to)$/i } keys %a and !grep { /^message-id$/i } keys %a;
  2         2080  
  2         5954  
  64         142  
  30         188  
  30         81  
  30         98  
  170         455  
  122         254  
429 64 100 100     444 append_header($m, "Content-Type: $type" . ($bound ? newparam('boundary', $bound) : '') . ($char =~ /^us-ascii$/i ? '' : newparam('charset', $char))) unless $type =~ /^text\/plain$/i && $char =~ /^us-ascii$/i;
    100          
    100          
430 64 100       223 append_header($m, "Content-Transfer-Encoding: $enc") unless $enc =~ /^7bit$/i;
431 64 50 66     450 append_header($m, "Content-Disposition: $disp" . ($a{filename} ? newparam('filename', $a{filename}) : '') . ($a{size} ? newparam('size', $a{size}) : '') . ($a{created} ? newparam('creation-date', $a{created}) : '') . ($a{modified} ? newparam('modification-date', $a{modified}) : '') . ($a{read} ? newparam('read-date', $a{read}) : '')) if $a{filename} || $a{size} || $a{created} || $a{modified} || $a{read};
    100 33        
    100 33        
    100 33        
    100          
    50          
432 64         108 append_header($m, "Content-@{[ucfirst $_]}: $a{$_}") for grep { $a{$_} } qw(description language duration location base features alternative);
  448         631  
  0         0  
433 64         96 append_header($m, "Content-@{[uc $_]}: $a{$_}") for grep { $a{$_} } qw(id md5);
  128         202  
  0         0  
434 64 100 50     148 ($m->{mime_type}, $m->{mime_boundary}, $m->{mime_parts}) = ($type =~ /^\s*([\w\/.-]+)/, $bound, $a{parts} || []) if $multi;
435 64 100 50     153 ($m->{mime_type}, $m->{mime_message}) = ($type =~ /^\s*([\w\/.-]+)/, $a{message} || {}) if $msg;
436 64 100 50     280 $m->{body} = encode($a{body} || '', $enc) unless $multi || $msg;
      100        
437 64 0 33     155 $m->{mbox} = $a{mbox} if exists $a{mbox} && defined $a{mbox} && length $a{mbox};
      33        
438 64         397 return $m;
439             }
440              
441             sub decode
442             {
443 4     4 0 26 my ($d, $e) = @_;
444 4 50       72 return $e =~ /^base64$/i ? decode_base64($d) : $e =~ /^quoted-printable$/i ? decode_quoted_printable($d) : substr($d, 0, -1);
    100          
445             }
446              
447             sub encode
448             {
449 46     46 0 103 my ($d, $e) = @_;
450 46 100       199 return $e =~ /^base64$/i ? encode_base64($d) : $e =~ /^quoted-printable$/i ? encode_quoted_printable($d) : $d . "\n";
    100          
451             }
452              
453             sub choose_encoding # rfc2822, rfc2045
454             {
455 46     46 0 82 my $len = length $_[0];
456 46         97 my $high = $_[0] =~ tr/\x80-\xff//;
457 46         91 my $ctrl = $_[0] =~ tr/\x00-\x06\x0e-\x1f\x7f//;
458 46         106 my ($maxlen, $pos, $next) = (0, 0, 0);
459              
460 46         129 for (; ($next = index($_[0], "\n", $pos)) != -1; $pos = $next + 1)
461             {
462 38 50       129 $maxlen = $next - $pos if $next - $pos > $maxlen;
463             }
464              
465 46 100       97 $maxlen = $len - $pos if $len - $pos > $maxlen;
466 46 50 33     201 return $ctrl ? 'base64' : $high ? $len > 1024 && $high > $len * 0.167 ? 'base64' : 'quoted-printable' : $maxlen > 998 ? 'quoted-printable' : '7bit';
    100          
    100          
    100          
467             }
468              
469             sub encode_base64 # MIME::Base64 (Gisle Aas)
470             {
471 14     14 0 45 pos $_[0] = 0; # Note: Text must be in canonical form (i.e. with "\r\n")
472 14         44 my $padlen = (3 - length($_[0]) % 3) % 3;
473 14         53 my $encoded = join '', map { pack('u', $_) =~ /^.(\S*)/ } $_[0] =~ /(.{1,45})/gs;
  14         89  
474 14         36 $encoded =~ tr{` -_}{AA-Za-z0-9+/};
475 14 100       155 $encoded =~ s/.{$padlen}$/'=' x $padlen/e if $padlen;
  12         42  
476 14         72 $encoded =~ s/(.{1,76})/$1\n/g;
477 14         61 return $encoded;
478             }
479              
480             sub decode_base64 # MIME::Base64 (Gisle Aas)
481             {
482 12     12 0 7072 my $data = shift;
483 12         52 $data =~ tr{A-Za-z0-9+=/}{}cd;
484 12         62 $data =~ s/=+$//;
485 12         35 $data =~ tr{A-Za-z0-9+/}{ -_};
486 12         131 return join '', map { unpack('u', chr(32 + length($_) * 3 / 4) . $_) } $data =~ /(.{1,60})/gs;
  132         476  
487             }
488              
489             sub encode_quoted_printable
490             {
491 25     25 0 92 my $quoted = shift;
492 25         37 my $qcode = shift;
493 25 50       76 my $binary = ($quoted =~ tr/\x00-\x06\x0e-\x1f\x7f//) ? '' : '\r\n';
494 25         187 $quoted =~ s/([^!-<>-~ \t$binary])/sprintf '=%02X', ord $1/eg;
  26         160  
495 25 100       71 $quoted =~ s/([?_])/sprintf '=%02X', ord $1/eg if $qcode;
  4         18  
496 25         250 $quoted =~ s/((?:[^\r\n]{73,75})(?=[=])|(?:[^\r\n]{75}(?=[ \t]))|(?:[^\r\n]{75})(?=[^\r\n]{2})|(?:[^\r\n]{75})(?=[^\r\n]$))/$1=\n/g;
497 25         80 $quoted =~ s/([ \t])$/sprintf '=%02X', ord $1/emg;
  0         0  
498             # Python and mutt both behave as though this is wrong
499             #$quoted .= "=\n" unless $quoted =~ /\n$/;
500 25         44 $quoted .= "\n";
501 25         116 return $quoted;
502             }
503              
504             sub decode_quoted_printable
505             {
506 19     19 0 2201 my $quoted = shift;
507 19         31 my $qcode = shift;
508 19         37 $quoted =~ tr/\x00-\x08\x0b-\x0c\x0e-\x1f\x7f-\xff//d;
509 19         41 $quoted =~ s/=\n//g;
510 19 50       62 $quoted =~ s/_/ /g if $qcode;
511 19         68 $quoted =~ s/=([0-9A-Fa-f]{2})/chr hex $1/eg;
  20         85  
512 19         61 return $quoted;
513             }
514              
515             my %mimetype =
516             (
517             txt => 'text/plain', csv => 'text/csv', htm => 'text/html', html => 'text/html', vcf => 'text/vcard', ics => 'text/calendar',
518             gif => 'image/gif', jpg => 'image/jpeg', jpeg => 'image/jpeg', jpe => 'image/jpeg', png => 'image/png', bmp => 'image/bmp', tiff => 'image/tiff', tif => 'image/tiff', jp2 => 'image/jp2', jpf => 'image/jpx', jpm => 'image/jpm',
519             mp2 => 'audio/mpeg', mp3 => 'audio/mpeg', au => 'audio/au', aif => 'audio/x-aiff', wav => 'audio/wav',
520             mpeg => 'video/mpeg', mpg => 'video/mpeg', mpe => 'video/mpeg', qt => 'video/quicktime', mov => 'video/quicktime', avi => 'video/x-msvideo', mj2 => 'video/mj2',
521             rtf => 'application/rtf', wri => 'application/vnd.ms-word', pdf => 'application/pdf', ps => 'application/ps', eps => 'application/ps', zip => 'application/zip', other => 'application/octet-stream',
522             doc => 'application/msword',
523             dot => 'application/msword',
524             docx => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
525             dotx => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
526             docm => 'application/vnd.ms-word.document.macroEnabled.12',
527             dotm => 'application/vnd.ms-word.template.macroEnabled.12',
528             xls => 'application/vnd.ms-excel',
529             xlt => 'application/vnd.ms-excel',
530             xla => 'application/vnd.ms-excel',
531             xlsx => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
532             xltx => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
533             xlsm => 'application/vnd.ms-excel.sheet.macroEnabled.12',
534             xltm => 'application/vnd.ms-excel.template.macroEnabled.12',
535             xlam => 'application/vnd.ms-excel.addin.macroEnabled.12',
536             xlsb => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
537             ppt => 'application/vnd.ms-powerpoint',
538             pot => 'application/vnd.ms-powerpoint',
539             pps => 'application/vnd.ms-powerpoint',
540             ppa => 'application/vnd.ms-powerpoint',
541             pptx => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
542             potx => 'application/vnd.openxmlformats-officedocument.presentationml.template',
543             ppsx => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
544             ppam => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
545             pptm => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
546             potm => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
547             ppsm => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12'
548             );
549              
550             my $add_mimetypes;
551             sub add_mimetypes
552             {
553 62 100   62 0 152 return if $add_mimetypes++;
554 2 50       75 open my $fh, '<', '/etc/mime.types' or return;
555              
556 0         0 while (<$fh>)
557             {
558 0 0       0 s/#.*$//, s/^\s+//, s/\s+$//; next unless $_;
  0         0  
559 0 0       0 my ($mimetype, $ext) = /^(\S+)\s+(.*)$/; next unless $ext;
  0         0  
560 0         0 $mimetype{$_} = $mimetype for split /\s+/, $ext;
561             }
562              
563 0         0 close $fh;
564             }
565              
566             sub tnef2mime
567             {
568 62     62 0 103 my $m = shift;
569 62 100 33     622 return $m unless exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i && exists $m->{mime_parts};
      66        
570 60         171 add_mimetypes();
571 60 100 66     97 @{$m->{mime_parts}} = grep { defined $_ } map { (mimetype($_) =~ /^application\/ms-tnef/i && filename($_) =~ /winmail\.dat$/i) ? winmail($_) : $_ } @{$m->{mime_parts}};
  60         159  
  90         221  
  90         192  
  60         127  
572 60         225 return $m;
573             }
574              
575 18     18 0 76 sub MESSAGE { 1 }
576 2     2 0 14 sub ATTACHMENT { 2 }
577 2     2 0 18007 sub MESSAGE_CLASS { 0x00078008 }
578 0     0 0 0 sub ATTACH_ATTACHMENT { 0x00069005 }
579 0     0 0 0 sub ATTACH_DATA { 0x0006800f }
580 0     0 0 0 sub ATTACH_FILENAME { 0x00018010 }
581 0     0 0 0 sub ATTACH_RENDDATA { 0x00069002 }
582 0     0 0 0 sub ATTACH_MODIFIED { 0x00038013 }
583             my $data; my @attachment; my $attachment; my $pos; my $badtnef;
584              
585             sub winmail
586             {
587             sub read_message_attribute
588             {
589 14     14 0 34 my $type = unpack 'C', substr $data, $pos, 1;
590 14 100 66     48 return 0 unless defined $type && $type == MESSAGE; ++$pos;
  12         17  
591 12         25 my $id = unpack 'V', substr $data, $pos, 4; $pos += 4;
  12         17  
592 12         20 my $len = unpack 'V', substr $data, $pos, 4; $pos += 4;
  12         17  
593 12 100       41 ++$badtnef, return 0 if $pos + $len > length $data;
594 10         28 my $buf = substr $data, $pos, $len; $pos += $len;
  10         14  
595 10         17 my $chk = unpack 'v', substr $data, $pos, 2; $pos += 2;
  10         22  
596 10         24 my $tot = unpack '%16C*', $buf;
597 10 50       20 ++$badtnef unless $chk == $tot;
598 10         35 return $chk == $tot;
599             }
600              
601             sub read_attribute_message_class
602             {
603 2     2 0 12 my $type = unpack 'C', substr $data, $pos, 1;
604 2 50 33     28 return unless defined $type && $type == MESSAGE;
605 0         0 my $id = unpack 'V', substr $data, $pos + 1, 4;
606 0 0       0 return unless $id == MESSAGE_CLASS; $pos += 5;
  0         0  
607 0         0 my $len = unpack 'V', substr $data, $pos, 4; $pos += 4;
  0         0  
608 0 0       0 ++$badtnef, return if $pos + $len > length $data;
609 0         0 my $buf = substr $data, $pos, $len; $pos += $len;
  0         0  
610 0         0 my $chk = unpack 'v', substr $data, $pos, 2; $pos += 2;
  0         0  
611 0         0 my $tot = unpack '%16C*', $buf;
612 0 0       0 ++$badtnef unless $chk == $tot;
613             }
614              
615             sub read_attachment_attribute
616             {
617 2     2 0 10 my $type = unpack 'C', substr $data, $pos, 1;
618 2 50 33     28 return 0 unless defined $type && $type == ATTACHMENT; ++$pos;
  0         0  
619 0         0 my $id = unpack 'V', substr $data, $pos, 4; $pos += 4;
  0         0  
620 0 0 0     0 ++$badtnef if $id == ATTACH_RENDDATA && @attachment && !exists $attachment->{body};
      0        
621 0 0       0 push @attachment, $attachment = {} if $id == ATTACH_RENDDATA;
622 0         0 my $len = unpack 'V', substr $data, $pos, 4; $pos += 4;
  0         0  
623 0 0       0 ++$badtnef, return 0 if $pos + $len > length $data;
624 0         0 my $buf = substr $data, $pos, $len; $pos += $len;
  0         0  
625 0         0 my $chk = unpack 'v', substr $data, $pos, 2; $pos += 2;
  0         0  
626 0         0 my $tot = unpack '%16C*', $buf;
627 0 0       0 ++$badtnef, return 0 unless $chk == $tot;
628 0 0       0 $attachment->{body} = $buf, $attachment->{size} = length $buf if $id == ATTACH_DATA;
629 0 0 0     0 $buf =~ s/\x00+$//, $attachment->{filename} = $buf, $attachment->{type} = $mimetype{($attachment->{filename} =~ /\.([^.]+)$/) || 'other'} || 'application/octet-stream' if $id == ATTACH_FILENAME && !exists $attachment->{filename};
      0        
630 0 0 0     0 my $fname; $attachment->{filename} = $fname, $attachment->{type} = $mimetype{($attachment->{filename} =~ /\.([^.]+)$/) || 'other'} || 'application/octet-stream' if $id == ATTACH_ATTACHMENT && ($fname = realname($buf));
  0   0     0  
631 2     2 0 23 use POSIX; sub word { unpack 'v', substr($_[0], $_[1] * 2, 2) }
  2     0   7  
  2         8  
  0         0  
632 0 0       0 $attachment->{modified} = strftime '%a, %d %b %Y %H:%M:%S +0000', gmtime mktime word($buf, 5), word($buf, 4), word($buf, 3), word($buf, 2), word($buf, 1) - 1, word($buf, 0) - 1900 if $id == ATTACH_MODIFIED;
633 0         0 return 1;
634             }
635              
636             sub realname
637             {
638 0     0 0 0 my $buf = shift;
639 0 0       0 my $pos = index $buf, "\x1e\x00\x01\x30\x01"; return unless $pos >= 0; $pos += 8;
  0         0  
  0         0  
640 0         0 my $len = unpack 'V', substr($buf, $pos, 4); $pos += 4;
  0         0  
641 0 0       0 my $name = substr($buf, $pos, $len) or return;
642 0         0 $name =~ s/\x00+$//;
643 0         0 return $name;
644             }
645              
646 2     2 0 27 my $m = shift;
647 2         17 add_mimetypes();
648 2         9 $pos = 0; $data = body($m); @attachment = (); $badtnef = 0;
  2         18  
  2         27  
  2         16  
649 2         15 my $signature = unpack 'V', substr($data, $pos, 4); $pos += 4;
  2         7  
650 2 50       19 return $m unless $signature == 0x223E9F78;
651 2         9 my $key = unpack 'v', substr($data, $pos, 2); $pos += 2;
  2         4  
652 2         7 my $type = unpack 'C', substr($data, $pos, 1);
653 2 50 33     9 return $m unless $type == MESSAGE || $type == ATTACHMENT;
654 2         34 do {} while read_message_attribute();
655 2         13 read_attribute_message_class();
656 2         11 do {} while read_message_attribute();
657 2         14 do {} while read_attachment_attribute();
658 2 50 33     32 ++$badtnef if @attachment && !exists $attachment->{body};
659 2 50       35 return ($badtnef) ? $m : map { newmail(%{$_}) } @attachment;
  0            
  0            
660             }
661              
662             1;
663              
664             # vi:set ts=4 sw=4: