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-2024 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             # 20240424 raf
9              
10             package MIME::Mini;
11 2     2   11303 use 5.014;
  2         8  
12 2     2   10 use strict;
  2         7  
  2         59  
13 2     2   7 use warnings;
  2         3  
  2         141  
14              
15             our $VERSION = '1.002';
16              
17 2     2   44 use Exporter;
  2         20  
  2         5480  
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 372 my ($mail, $parent) = @_;
37 178 100 100     1243 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     620 my ($content_type) = (exists $mail->{header} && exists $mail->{header}->{'content-type'}) ? @{$mail->{header}->{'content-type'}} : "Content-Type: message/rfc822\n";
  130         281  
39 136         756 my ($type) = $content_type =~ /^content-type:\s*([\w\/.-]+)/i;
40 136 100       719 my $boundary = param($mail, 'content-type', 'boundary') if $type =~ /^multipart\//i;
41 136 100 100     1140 return $mail unless defined $type && ($type =~ /^multipart\//i && $boundary || $type =~ /^message\/rfc822$/i);
      66        
42 82 100       404 ($mail->{mime_boundary}) = $boundary =~ /^(.*\S)/ if $boundary;
43 82         208 $mail->{mime_type} = $type;
44 82 100 100     436 $mail->{mime_message} = mimepart(delete $mail->{body} || '', $mail), return $mail if $type =~ /^message\/(?:rfc822|external-body)$/i;
45 62         213 return tnef2mime(mimeparts($mail, $parent));
46             }
47              
48             sub mimeparts
49             {
50 62     62 0 118 my ($mail, $parent) = @_;
51 62         116 my $state = 'preamble';
52 62         110 my $text = '';
53              
54 62   50     3373 for (split /(?<=\n)/, delete $mail->{body} || '')
55             {
56 872 100       4242 if (/^--\Q$mail->{mime_boundary}\E(--)?/)
57             {
58 146 100       412 if ($state eq 'preamble')
    50          
59             {
60 62         156 $state = 'part';
61 62 100       233 $mail->{mime_preamble} = $text if length $text;
62             }
63             elsif ($state eq 'part')
64             {
65 84 100 66     468 $state = 'epilogue' if defined $1 && $1 eq '--';
66 84         101 push @{$mail->{mime_parts}}, mimepart($text, $mail);
  84         262  
67             }
68              
69 146         338 $text = '', next;
70             }
71              
72 726         1130 $text .= $_;
73             }
74              
75 62 100 100     386 push @{$mail->{mime_parts}}, mimepart($text, $mail) if $state eq 'part' && length $text;
  6         34  
76 62 100 100     280 $mail->{mime_epilogue} = $text if $state eq 'epilogue' && length $text;
77 62         335 return $mail;
78             }
79              
80             sub mimepart
81             {
82 110     110 0 262 my ($mail, $parent) = @_;
83 110         2846 my @lines = split /(?<=\n)/, $mail;
84             # Needed to cope (badly) when message/rfc822 attachments incorrectly start with /^From / (thanks libpst)
85 110 100       298 @lines = ('') unless @lines;
86 110     110   922 formail(sub { shift @lines }, sub { $mail = shift }, $parent);
  844         1668  
  110         258  
87 110         706 return $mail;
88             }
89              
90 140     140 1 428228 my ($rd, $act, $parent) = @_;
91 140         414 my $state = 'header';
92 140         293 my $mail; my $last;
93              
94 140         371 while (defined($_ = $rd->()))
95             {
96 2366         8514 s/\r(?=\n)//g; #, tr/\r/\n/;
97              
98 2366 100 100     6767 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     717 $mail->{body} =~ s/\n\n\z/\n/ if $mail && exists $mail->{mbox} && exists $mail->{body};
      33        
101 68 100 50     176 my $mbox = $_; $act->(mime($mail, $parent)) or return if $mail;
  68         426  
102 68         1392 $mail = { mbox => $mbox }, $state = 'header', undef $last, next;
103             }
104              
105 2298 100       4219 if ($state eq 'header')
    50          
106             {
107 950 100       2906 if (/^([\w-]+):/) # mail header
    100          
108             {
109 682         876 push @{$mail->{headers}}, $_;
  682         1532  
110 682         924 push @{$mail->{header}->{$last = lc $1}}, $_;
  682         3597  
111             }
112             elsif (/^$/) # blank line after mail headers
113             {
114 174         600 $mail->{body} = '', $state = 'body';
115             }
116             else # mail header continuation or error
117             {
118 94 50       193 ${$mail->{headers}}[$#{$mail->{headers}}] .= $_ if defined $last;
  94         258  
  94         149  
119 94 50       201 ${$mail->{header}->{$last}}[$#{$mail->{header}->{$last}}] .= $_ if defined $last;
  94         300  
  94         160  
120             }
121             }
122             elsif ($state eq 'body')
123             {
124 1348 100       2471 s/^>(>*From )/$1/ if exists $mail->{mbox};
125 1348         3033 $mail->{body} .= $_;
126             }
127             }
128              
129 140 100 100     1554 $mail->{body} =~ s/\n\n\z/\n/ if $mail && exists $mail->{mbox} && exists $mail->{body};
      100        
130 140 100       531 $act->(mime($mail, $parent)) if $mail;
131             }
132              
133             sub mail2str
134             {
135 272     272 1 515 my $mail = shift;
136 272         427 my $head = '';
137 272 100       637 $head .= $mail->{mbox} if exists $mail->{mbox};
138 272 100       616 $head .= join '', @{$mail->{headers}} if exists $mail->{headers};
  230         667  
139 272         435 my $body = '';
140 272 100       567 $body .= $mail->{body} if exists $mail->{body};
141 272 100       501 $body .= "$mail->{mime_preamble}" if exists $mail->{mime_preamble};
142 272 100 100     812 $body .= "--$mail->{mime_boundary}\n" if exists $mail->{mime_boundary} && !exists $mail->{mime_parts};
143 272 100       617 $body .= join('', map { "--$mail->{mime_boundary}\n" . mail2str($_) } @{$mail->{mime_parts}}) if exists $mail->{mime_parts};
  132         344  
  70         168  
144 272 100       570 $body .= "--$mail->{mime_boundary}--\n" if exists $mail->{mime_boundary};
145 272 100       505 $body .= "$mail->{mime_epilogue}" if exists $mail->{mime_epilogue};
146 272 100       597 $body .= mail2str($mail->{mime_message}) if exists $mail->{mime_message};
147 272 100       1474 $body =~ s/^(>*From )/>$1/mg, $body =~ s/([^\n])\n?\z/$1\n\n/ if exists $mail->{mbox};
148 272         2085 return $head . "\n" . $body;
149             }
150              
151             my $bchar = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-.\/:=?";
152             sub mail2multipart
153             {
154 24     24 1 161 my $m = shift;
155 24 100 100     159 return $m if exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i;
156 12         27 my $p = {};
157 12         30 append_header($p, $_) for grep { /^content-/i } @{$m->{headers}};
  60         150  
  12         39  
158 12 100       71 $p->{body} = delete $m->{body} if exists $m->{body};
159 12 100       46 $p->{mime_message} = delete $m->{mime_message} if exists $m->{mime_message};
160 12 100       42 $p->{mime_type} = $m->{mime_type} if exists $m->{mime_type};
161 12         47 $m->{mime_type} = 'multipart/mixed';
162 12 50       55 $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         861  
163 12 50       91 $m->{mime_preamble} = delete $m->{mime_prev_preamble} if exists $m->{mime_prev_preamble};
164 12 50       40 $m->{mime_epilogue} = delete $m->{mime_prev_epilogue} if exists $m->{mime_prev_epilogue};
165 12         164 delete_header($m, qr/content-[^:]*/i);
166 12 100 66     159 append_header($m, 'MIME-Version: 1.0') unless exists $m->{header} && exists $m->{header}->{'mime-version'};
167 12         58 append_header($m, "Content-Type: $m->{mime_type}; boundary=\"$m->{mime_boundary}\"");
168 12         53 $m->{mime_parts} = [$p];
169 12         68 return $m;
170             }
171              
172             sub mail2singlepart
173             {
174 40     40 1 122 my $m = shift;
175 40 100 100     225 $m->{mime_message} = mail2singlepart($m->{mime_message}), return $m if exists $m->{mime_type} && $m->{mime_type} =~ /^message\//i;
176 36 100 66     299 return $m unless exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i && @{$m->{mime_parts}} <= 1;
  20   100     70  
177 16         32 my $p = shift @{$m->{mime_parts}};
  16         34  
178 16 50       67 $m->{mime_prev_boundary} = delete $m->{mime_boundary} if exists $m->{mime_boundary};
179 16 100       52 $m->{mime_prev_preamble} = delete $m->{mime_preamble} if exists $m->{mime_preamble};
180 16 50       37 $m->{mime_prev_epilogue} = delete $m->{mime_epilogue} if exists $m->{mime_epilogue};
181 16 100       46 $m->{body} = $p->{body} if exists $p->{body};
182 16 100       34 $m->{mime_message} = $p->{mime_message} if exists $p->{mime_message};
183 16 100       25 delete $m->{mime_type}; $m->{mime_type} = $p->{mime_type} if exists $p->{mime_type};
  16         37  
184 16 100       28 delete $m->{mime_parts}; $m->{mime_parts} = $p->{mime_parts} if exists $p->{mime_parts};
  16         39  
185 16 100       31 $m->{mime_boundary} = $p->{mime_boundary} if exists $p->{mime_boundary};
186 16 50       31 $m->{mime_preamble} = $p->{mime_preamble} if exists $p->{mime_preamble};
187 16 50       31 $m->{mime_epilogue} = $p->{mime_epilogue} if exists $p->{mime_epilogue};
188 16         22 my $explicit = 0;
189 16         88 delete_header($m, qr/content-[^:]*/i);
190 16         31 append_header($m, $_), ++$explicit for grep { /^content-/i } @{$p->{headers}};
  18         74  
  16         31  
191 16 100       39 delete_header($m, 'mime-version') unless $explicit;
192 16         40 return mail2singlepart($m);
193             }
194              
195             sub mail2mbox
196             {
197 58     58 1 544 my $m = shift;
198 58 100       288 return $m if exists $m->{mbox};
199 20         47 my ($f) = header($m, 'sender');
200 20 50       52 ($f) = header($m, 'from') unless defined $f;
201 20 50       91 $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       46 $f = 'unknown' unless defined $f;
203 2     2   1399 use POSIX; $m->{mbox} = "From $f " . ctime(time());
  2         15545  
  2         16  
  20         568  
204 20         78 return $m;
205             }
206              
207             sub insert_header
208             {
209 4     4 1 27 my ($m, $h, $l, $c) = @_;
210 4         12 $h = header_format($h, $l, $c);
211 4         21 my ($n) = $h =~ /^([^:]+):/;
212 4         16 unshift @{$m->{headers}}, $h;
  4         15  
213 4         7 unshift @{$m->{header}->{lc $n}}, $h;
  4         20  
214             }
215              
216             sub append_header
217             {
218 305     305 1 717922 my ($m, $h, $l, $c) = @_;
219 305         698 $h = header_format($h, $l, $c);
220 305         1220 my ($n) = $h =~ /^([^:]+):/;
221 305         430 push @{$m->{headers}}, $h;
  305         792  
222 305         431 push @{$m->{header}->{lc $n}}, $h;
  305         1345  
223             }
224              
225             sub replace_header
226             {
227 20     20 1 91 my ($m, $h, $l, $c) = @_;
228 20         64 $h = header_format($h, $l, $c);
229 20         150 my ($n) = $h =~ /^([^:]+):/;
230 20 50       39 my $seen = 0; @{$m->{headers}} = grep { defined $_ } map { /^\Q$n\E:/i ? $seen ? undef : do { ++$seen; $h } : $_ } @{$m->{headers}};
  20 100       42  
  20         75  
  20         38  
  20         270  
  6         11  
  6         15  
  20         54  
231 20         35 splice @{$m->{header}->{lc $n}};
  20         80  
232 20         31 push @{$m->{header}->{lc $n}}, $h;
  20         88  
233             }
234              
235             sub delete_header
236             {
237 46     46 1 138 my ($m, $h, $r) = @_;
238 46 100       123 return undef unless exists $m->{header};
239 44         78 @{$m->{headers}} = grep { !/^$h:/i } @{$m->{headers}};
  44         143  
  222         1248  
  44         101  
240 44         65 delete $m->{header}->{$_} for grep { /^$h$/i } keys %{$m->{header}};
  222         953  
  44         158  
241 44 100 100     135 if ($r && exists $m->{mime_parts}) { delete_header($_, $h, $r) for @{$m->{mime_parts}} }
  2         5  
  2         14  
242 44 100 100     109 if ($r && exists $m->{mime_message}) { delete_header($m->{mime_message}, $h, $r) }
  2         11  
243             }
244              
245             sub insert_part
246             {
247 2     2 1 16 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 24 my ($m, $p) = @_;
254 10         15 push @{$m->{mime_parts}}, $p;
  10         32  
255             }
256              
257             sub replace_part
258             {
259 2     2 1 13 my ($m, $p, $i) = @_;
260 2         5 splice @{$m->{mime_parts}}, $i, 1, $p;
  2         11  
261             }
262              
263             sub delete_part
264             {
265 2     2 1 6 my ($m, $i) = @_;
266 2         4 splice @{$m->{mime_parts}}, $i, 1;
  2         6  
267             }
268              
269             sub header
270             {
271 359     359 1 18621 my ($m, $h) = @_;
272 359 100 100     1662 return () unless exists $m->{header} && exists $m->{header}->{lc $h};
273 257         346 return map { s/\n\s+/ /g; header_display($_) =~ /^$h:\s*(.*)\s*$/i; $1 } @{$m->{header}->{lc $h}};
  257         798  
  257         589  
  257         1502  
  257         682  
274             }
275              
276             sub headers
277             {
278 2     2 1 6 my $m = shift;
279 2 50       13 return () unless exists $m->{headers};
280 2         5 return map { s/\n\s+/ /g; header_display($_) =~ /^([\w-]+:.*)\s*$/; $1 } @{$m->{headers}};
  8         20  
  8         23  
  8         74  
  2         7  
281             }
282              
283             sub header_names
284             {
285 2     2 1 10921 my $m = shift;
286 2 50       11 return () unless exists $m->{header};
287 2         5 return keys %{$m->{header}};
  2         28  
288             }
289              
290             my $encword = qr/=\?([^*?]+)(?:\*\w+)?\?(q|b)\?([^? ]+)\?=/i; # encoded words to display
291             sub header_display # rfc2047, rfc2231
292             {
293 2     2   9179 use Encode ();
  2         31234  
  2         1041  
294             return join '',
295 525         869 map { tr/ \t/ /s; $_ } # finally, squeeze multiple whitespace
  525         5604  
296 525         1067 map { tr/\x00-\x08\x0b-\x1f\x7f//d; $_ } # strip control characters
  525         900  
297 525 100       1774 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       412  
  525         1302  
298 525         3589 map { s/($encword)\s+($encword)/$1$5/g while /$encword\s+$encword/; $_ } # strip space between encoded words that we're about to decode
  525         1048  
299 273 100   273 0 16163 map { s/\((?:\\[^\r\n]|[^\\()])*\)//g unless /^".*"$/; $_ } # strip (comments) outside "quoted strings"
  525         1787  
  525         1066  
300             split /("(?:\\[^\r\n]|[^\\"])*")/, shift; # split on "quoted strings"
301             }
302              
303             sub charsetof
304             {
305 87     87 0 171 my $s = shift;
306 87 100 100     574 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       89 return 'utf-8' if defined eval { Encode::decode 'UTF-8', $s, Encode::FB_CROAK };
  33         224  
309 32 50 33     1898 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 737 my ($h, $l, $c) = @_;
315 329         1734 $h =~ s/^\s+//, $h =~ s/\s+$//, $h =~ tr/ \t\n\r/ /s;
316 2 100   2   20 use Encode (); $h = Encode::encode('UTF-8', $h) if grep { ord > 255 } split //, $h;
  2         3  
  2         3913  
  329         2746  
  12188         17923  
317 329 50 33     3295 $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     3822  
  6 100       32  
  13 100       50  
  393 50       1634  
    100          
318 329 100 100     896 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       1587  
  393 100       1589  
  993         4681  
319 329         1068 return $f . "\n";
320             }
321              
322             sub param # rfc2231, rfc2045
323             {
324 80     80 1 256 my ($m, $h, $p) = @_;
325 80         159 my @p; my $decode = 0;
  80         182  
326              
327 80         415 for (header($m, $h))
328             {
329 80         3714 while (/(\b\Q$p\E(?:\*|\*\d\*?)?)=("(?:\\[^\n]|[^"\n])*"|[^\x00-\x20()<>@,;:\\"\/\[\]?=]+)/ig)
330             {
331 98         454 my ($n, $v) = ($1, $2);
332 98 100       835 $v =~ s/^"//, $v =~ s/"$//, $v =~ s/\\(.)/$1/g if $v =~ /^".*"$/;
333 98 100       348 $v =~ s/^(?:us-ascii|utf-8|iso-8859-\d{1,2})'\w+'//i and $decode = 1;
334 98 100 100     406 $v =~ s/%([\da-fA-f]{2})/chr hex $1/eg if $decode && substr($n, -1) eq '*';
  38         142  
335 98         1104 push @p, [lc $n, $v];
336             }
337             }
338              
339 80         323 return join '', map { $_->[1] } sort { my ($ad) = $a->[0] =~ /(\d+)/; my ($bd) = $b->[0] =~ /(\d+)/; $ad <=> $bd } @p;
  98         576  
  26         93  
  26         77  
  26         71  
340             }
341              
342             sub mimetype # rfc2045, rfc2046
343             {
344 100     100 1 226 my ($m, $p) = @_;
345 100         266 my ($e) = header($m, 'content-transfer-encoding');
346 100 100 100     830 return 'application/octet-stream' if defined $e && $e !~ /^(?:[78]bit|binary|quoted-printable|base64)$/i;
347 98         198 my ($type) = header($m, 'content-type');
348 98 100 100     1198 return lc $1 if defined $type && $type =~ /^((?:text|image|audio|video|application|message|multipart)\/[^\s;]+)/i;
349 22 50 100     240 return 'message/rfc822' if !defined $type && defined $p && exists $p->{mime_type} && $p->{mime_type} =~ /^multipart\/digest/i;
      66        
      33        
350 20         128 return 'text/plain';
351             }
352              
353             sub encoding # rfc2045
354             {
355 22     22 1 9709 my $m = shift;
356 22         62 my ($e) = header($m, 'content-transfer-encoding');
357 22 100 100     315 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 20 my $p = shift;
364 4   33     21 my $fn = param($p, 'content-disposition', 'filename') || param($p, 'content-type', 'name') || 'attachment' . ++$filename_counter;
365 4         35 $fn =~ s/^.*[\\\/]//, $fn =~ tr/\x00-\x1f !"#\$%&'()*\/:;<=>?@[\\]^`{|}~\x7f/_/s;
366 4         53 return $fn;
367             }
368              
369             sub body
370             {
371 8     8 1 35 my $m = shift;
372 8 100       86 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       90 return exists $m->{mime_message} ? $m->{mime_message} : undef;
379             }
380              
381             sub parts
382             {
383 12     12 1 38 my ($m, $p) = @_;
384 12 100       73 return exists $m->{mime_parts} ? [@{$m->{mime_parts}}] : [] unless defined $p;
  6 100       38  
385 2         6 $m->{mime_parts} = [@{$p}];
  2         11  
386             }
387              
388             sub newparam # rfc2231, rfc2045
389             {
390 46     46 1 14000 my ($n, $v, $l, $c) = (@_, '', '');
391 46         84 my $high = $v =~ tr/\x80-\xff//;
392 46         69 my $ctrl = $v =~ tr/\x00-\x06\x0e-\x1f\x7f//;
393 46 100 66     149 my $enc = $high || $ctrl ? '*' : '';
394 46 100 66     130 $c = charsetof($v) if $enc && !$c;
395 46 100 100     110 $l = 'en' if $c && !$l;
396 46 100       118 $v = "$c'$l'$v" if $enc;
397 46         63 my @p; push @p, $_ while $_ = substr $v, 0, 40, '';
  46         250  
398 46         81 s/([\x00-\x20\x7f-\xff])/sprintf '%%%02X', ord $1/eg for grep { tr/\x00-\x06\x0e-\x1f\x7f-\xff// } @p;
  58         146  
  28         97  
399 46         71 s/"/\\"/g, s/^/"/g, s/$/"/g for grep { tr/\x00-\x06\x0e-\x1f\x7f ()<>@,;:\\"\/[]?=// } @p;
  58         263  
400 46 100       301 return "; $n$enc=$p[0]" if @p == 1;
401 6         17 return join '', map { "; $n*$_$enc=$p[$_]" } 0..$#p;
  18         109  
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 31581 my @a = @_; my %a = @_; my $m = {};
  64         282  
  64         116  
408 2     2 0 23 sub rfc822date { use POSIX; return strftime '%a, %d %b %Y %H:%M:%S +0000', gmtime shift; }
  2     36   3  
  2         20  
  36         1267  
409 64   66     373 my $type = $a{type} || (exists $a{parts} ? 'multipart/mixed' : exists $a{message} ? 'message/rfc822' : 'text/plain');
410 64         149 my $multi = $type =~ /^multipart\//i;
411 64         145 my $msg = $type =~ /^message\/rfc822$/i;
412 64 50 100     386 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         6 $a{body} = do { local $/; my $b = <$fh>; close $fh; $b };
  2         11  
  2         34  
  2         18  
  2         11  
415 2 50       15 $a{created} = (exists $a{created}) ? $a{created} : rfc822date((stat _)[9]);
416 2 50       11 $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         9 $a{size} = (stat _)[7];
419             }
420 64 100       186 ($a{filename}) = $a{filename} =~ /([^\\\/]+)$/ if $a{filename};
421 64 100       169 my $bound = $multi ? join '', map { substr $bchar, int(rand(length $bchar)), 1 } 0..30 : '';
  186         449  
422 64   33     427 my $disp = $a{disposition} || ($type =~ /^(?:text\/|message\/rfc822)/i ? 'inline' : 'attachment');
423 64   33     253 my $char = $a{charset} || charsetof($a{body});
424 64   33     387 my $enc = $a{encoding} || ($multi || $msg ? '7bit' : $a{body} ? choose_encoding($a{body}) : '7bit');
425 64 100       181 append_header($m, $a[$_] . ': ' . $a[$_ + 1]) for grep { $_ % 2 == 0 && $a[$_] =~ /^[A-Z]/ } 0..$#a;
  320         1177  
426 64 100 66     168 append_header($m, 'Date: ' . rfc822date(time)) if grep { /^(?:date|from|sender|reply-to)$/i } keys %a and !grep { /^date$/i } keys %a;
  170         505  
  122         293  
427 64 100 66     168 append_header($m, 'MIME-Version: 1.0') if grep { /^(?:date|from|sender|reply-to)$/i } keys %a and !grep { /^mime-version$/ } keys %a;
  170         541  
  122         260  
428 2 100 66 2   7997 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         2502  
  2         6349  
  64         150  
  30         106  
  30         77  
  30         209  
  170         428  
  122         224  
429 64 100 100     598 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       227 append_header($m, "Content-Transfer-Encoding: $enc") unless $enc =~ /^7bit$/i;
431 64 50 66     531 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         117 append_header($m, "Content-@{[ucfirst $_]}: $a{$_}") for grep { $a{$_} } qw(description language duration location base features alternative);
  448         658  
  0         0  
433 64         93 append_header($m, "Content-@{[uc $_]}: $a{$_}") for grep { $a{$_} } qw(id md5);
  128         191  
  0         0  
434 64 100 50     163 ($m->{mime_type}, $m->{mime_boundary}, $m->{mime_parts}) = ($type =~ /^\s*([\w\/.-]+)/, $bound, $a{parts} || []) if $multi;
435 64 100 50     185 ($m->{mime_type}, $m->{mime_message}) = ($type =~ /^\s*([\w\/.-]+)/, $a{message} || {}) if $msg;
436 64 100 50     396 $m->{body} = encode($a{body} || '', $enc) unless $multi || $msg;
      100        
437 64 0 33     162 $m->{mbox} = $a{mbox} if exists $a{mbox} && defined $a{mbox} && length $a{mbox};
      33        
438 64         436 return $m;
439             }
440              
441             sub decode
442             {
443 4     4 0 16 my ($d, $e) = @_;
444 4 50       76 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 109 my ($d, $e) = @_;
450 46 100       241 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 85 my $len = length $_[0];
456 46         97 my $high = $_[0] =~ tr/\x80-\xff//;
457 46         82 my $ctrl = $_[0] =~ tr/\x00-\x06\x0e-\x1f\x7f//;
458 46         101 my ($maxlen, $pos, $next) = (0, 0, 0);
459              
460 46         147 for (; ($next = index($_[0], "\n", $pos)) != -1; $pos = $next + 1)
461             {
462 38 50       157 $maxlen = $next - $pos if $next - $pos > $maxlen;
463             }
464              
465 46 100       101 $maxlen = $len - $pos if $len - $pos > $maxlen;
466 46 50 33     204 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 60 pos $_[0] = 0; # Note: Text must be in canonical form (i.e. with "\r\n")
472 14         59 my $padlen = (3 - length($_[0]) % 3) % 3;
473 14         61 my $encoded = join '', map { pack('u', $_) =~ /^.(\S*)/ } $_[0] =~ /(.{1,45})/gs;
  14         112  
474 14         63 $encoded =~ tr{` -_}{AA-Za-z0-9+/};
475 14 100       214 $encoded =~ s/.{$padlen}$/'=' x $padlen/e if $padlen;
  12         43  
476 14         78 $encoded =~ s/(.{1,76})/$1\n/g;
477 14         72 return $encoded;
478             }
479              
480             sub decode_base64 # MIME::Base64 (Gisle Aas)
481             {
482 12     12 0 9221 my $data = shift;
483 12         72 $data =~ tr{A-Za-z0-9+=/}{}cd;
484 12         91 $data =~ s/=+$//;
485 12         38 $data =~ tr{A-Za-z0-9+/}{ -_};
486 12         120 return join '', map { unpack('u', chr(32 + length($_) * 3 / 4) . $_) } $data =~ /(.{1,60})/gs;
  132         544  
487             }
488              
489             sub encode_quoted_printable
490             {
491 25     25 0 71 my $quoted = shift;
492 25         35 my $qcode = shift;
493 25 50       67 my $binary = ($quoted =~ tr/\x00-\x06\x0e-\x1f\x7f//) ? '' : '\r\n';
494 25         267 $quoted =~ s/([^!-<>-~ \t$binary])/sprintf '=%02X', ord $1/eg;
  26         162  
495 25 100       68 $quoted =~ s/([?_])/sprintf '=%02X', ord $1/eg if $qcode;
  4         13  
496 25         208 $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         76 $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         45 $quoted .= "\n";
501 25         115 return $quoted;
502             }
503              
504             sub decode_quoted_printable
505             {
506 19     19 0 1903 my $quoted = shift;
507 19         62 my $qcode = shift;
508 19         40 $quoted =~ tr/\x00-\x08\x0b-\x0c\x0e-\x1f\x7f-\xff//d;
509 19         77 $quoted =~ s/=\n//g;
510 19 50       69 $quoted =~ s/_/ /g if $qcode;
511 19         81 $quoted =~ s/=([0-9A-Fa-f]{2})/chr hex $1/eg;
  20         91  
512 19         128 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 154 return if $add_mimetypes++;
554 2 50       313 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 102 my $m = shift;
569 62 100 33     548 return $m unless exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i && exists $m->{mime_parts};
      66        
570 60         173 add_mimetypes();
571 60 100 66     88 @{$m->{mime_parts}} = grep { defined $_ } map { (mimetype($_) =~ /^application\/ms-tnef/i && filename($_) =~ /winmail\.dat$/i) ? winmail($_) : $_ } @{$m->{mime_parts}};
  60         155  
  90         174  
  90         303  
  60         125  
572 60         297 return $m;
573             }
574              
575 18     18 0 84 sub MESSAGE { 1 }
576 2     2 0 9 sub ATTACHMENT { 2 }
577 2     2 0 20235 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 31 my $type = unpack 'C', substr $data, $pos, 1;
590 14 100 66     37 return 0 unless defined $type && $type == MESSAGE; ++$pos;
  12         18  
591 12         21 my $id = unpack 'V', substr $data, $pos, 4; $pos += 4;
  12         15  
592 12         25 my $len = unpack 'V', substr $data, $pos, 4; $pos += 4;
  12         15  
593 12 100       28 ++$badtnef, return 0 if $pos + $len > length $data;
594 10         22 my $buf = substr $data, $pos, $len; $pos += $len;
  10         15  
595 10         17 my $chk = unpack 'v', substr $data, $pos, 2; $pos += 2;
  10         14  
596 10         22 my $tot = unpack '%16C*', $buf;
597 10 50       27 ++$badtnef unless $chk == $tot;
598 10         58 return $chk == $tot;
599             }
600              
601             sub read_attribute_message_class
602             {
603 2     2 0 8 my $type = unpack 'C', substr $data, $pos, 1;
604 2 50 33     69 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 13 my $type = unpack 'C', substr $data, $pos, 1;
618 2 50 33     23 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 28 use POSIX; sub word { unpack 'v', substr($_[0], $_[1] * 2, 2) }
  2     0   3  
  2         18  
  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 22 my $m = shift;
647 2         17 add_mimetypes();
648 2         7 $pos = 0; $data = body($m); @attachment = (); $badtnef = 0;
  2         13  
  2         11  
  2         6  
649 2         15 my $signature = unpack 'V', substr($data, $pos, 4); $pos += 4;
  2         12  
650 2 50       17 return $m unless $signature == 0x223E9F78;
651 2         13 my $key = unpack 'v', substr($data, $pos, 2); $pos += 2;
  2         6  
652 2         7 my $type = unpack 'C', substr($data, $pos, 1);
653 2 50 33     15 return $m unless $type == MESSAGE || $type == ATTACHMENT;
654 2         12 do {} while read_message_attribute();
655 2         26 read_attribute_message_class();
656 2         7 do {} while read_message_attribute();
657 2         14 do {} while read_attachment_attribute();
658 2 50 33     8 ++$badtnef if @attachment && !exists $attachment->{body};
659 2 50       14 return ($badtnef) ? $m : map { newmail(%{$_}) } @attachment;
  0            
  0            
660             }
661              
662             1;
663              
664             # vi:set ts=4 sw=4: