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             # 20230508 raf
9              
10             package MIME::Mini;
11 2     2   1790 use 5.014;
  2         17  
12 2     2   11 use strict;
  2         2  
  2         56  
13 2     2   10 use warnings;
  2         4  
  2         83  
14              
15             our $VERSION = '1.000';
16              
17 2     2   15 use Exporter;
  2         4  
  2         5173  
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 371 my ($mail, $parent) = @_;
37 178 100 100     1045 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     535 my ($content_type) = (exists $mail->{header} && exists $mail->{header}->{'content-type'}) ? @{$mail->{header}->{'content-type'}} : "Content-Type: message/rfc822\n";
  130         268  
39 136         617 my ($type) = $content_type =~ /^content-type:\s*([\w\/.-]+)/i;
40 136 100       672 my $boundary = param($mail, 'content-type', 'boundary') if $type =~ /^multipart\//i;
41 136 100 100     1174 return $mail unless defined $type && ($type =~ /^multipart\//i && $boundary || $type =~ /^message\/rfc822$/i);
      66        
42 82 100       355 ($mail->{mime_boundary}) = $boundary =~ /^(.*\S)/ if $boundary;
43 82         245 $mail->{mime_type} = $type;
44 82 100 100     429 $mail->{mime_message} = mimepart(delete $mail->{body} || '', $mail), return $mail if $type =~ /^message\/(?:rfc822|external-body)$/i;
45 62         211 return tnef2mime(mimeparts($mail, $parent));
46             }
47              
48             sub mimeparts
49             {
50 62     62 0 103 my ($mail, $parent) = @_;
51 62         99 my $state = 'preamble';
52 62         98 my $text = '';
53              
54 62   50     2435 for (split /(?<=\n)/, delete $mail->{body} || '')
55             {
56 872 100       3466 if (/^--\Q$mail->{mime_boundary}\E(--)?/)
57             {
58 146 100       371 if ($state eq 'preamble')
    50          
59             {
60 62         102 $state = 'part';
61 62 100       174 $mail->{mime_preamble} = $text if length $text;
62             }
63             elsif ($state eq 'part')
64             {
65 84 100 66     387 $state = 'epilogue' if defined $1 && $1 eq '--';
66 84         120 push @{$mail->{mime_parts}}, mimepart($text, $mail);
  84         255  
67             }
68              
69 146         390 $text = '', next;
70             }
71              
72 726         1328 $text .= $_;
73             }
74              
75 62 100 100     327 push @{$mail->{mime_parts}}, mimepart($text, $mail) if $state eq 'part' && length $text;
  6         48  
76 62 100 100     326 $mail->{mime_epilogue} = $text if $state eq 'epilogue' && length $text;
77 62         320 return $mail;
78             }
79              
80             sub mimepart
81             {
82 110     110 0 284 my ($mail, $parent) = @_;
83 110         2203 my @lines = split /(?<=\n)/, $mail;
84             # Needed to cope (badly) when message/rfc822 attachments incorrectly start with /^From / (thanks libpst)
85 110 100       293 @lines = ('') unless @lines;
86 110     110   916 formail(sub { shift @lines }, sub { $mail = shift }, $parent);
  844         1744  
  110         267  
87 110         507 return $mail;
88             }
89              
90 140     140 1 226186 my ($rd, $act, $parent) = @_;
91 140         456 my $state = 'header';
92 140         297 my $mail; my $last;
93              
94 140         368 while (defined($_ = $rd->()))
95             {
96 2366         8675 s/\r(?=\n)//g; #, tr/\r/\n/;
97              
98 2366 100 100     6652 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     617 $mail->{body} =~ s/\n\n\z/\n/ if $mail && exists $mail->{mbox} && exists $mail->{body};
      33        
101 68 100 50     205 my $mbox = $_; $act->(mime($mail, $parent)) or return if $mail;
  68         240  
102 68         1133 $mail = { mbox => $mbox }, $state = 'header', undef $last, next;
103             }
104              
105 2298 100       4116 if ($state eq 'header')
    50          
106             {
107 950 100       2790 if (/^([\w-]+):/) # mail header
    100          
108             {
109 682         958 push @{$mail->{headers}}, $_;
  682         1531  
110 682         887 push @{$mail->{header}->{$last = lc $1}}, $_;
  682         3555  
111             }
112             elsif (/^$/) # blank line after mail headers
113             {
114 174         503 $mail->{body} = '', $state = 'body';
115             }
116             else # mail header continuation or error
117             {
118 94 50       218 ${$mail->{headers}}[$#{$mail->{headers}}] .= $_ if defined $last;
  94         287  
  94         138  
119 94 50       185 ${$mail->{header}->{$last}}[$#{$mail->{header}->{$last}}] .= $_ if defined $last;
  94         276  
  94         177  
120             }
121             }
122             elsif ($state eq 'body')
123             {
124 1348 100       2385 s/^>(>*From )/$1/ if exists $mail->{mbox};
125 1348         2696 $mail->{body} .= $_;
126             }
127             }
128              
129 140 100 100     1355 $mail->{body} =~ s/\n\n\z/\n/ if $mail && exists $mail->{mbox} && exists $mail->{body};
      100        
130 140 100       446 $act->(mime($mail, $parent)) if $mail;
131             }
132              
133             sub mail2str
134             {
135 272     272 1 506 my $mail = shift;
136 272         392 my $head = '';
137 272 100       583 $head .= $mail->{mbox} if exists $mail->{mbox};
138 272 100       563 $head .= join '', @{$mail->{headers}} if exists $mail->{headers};
  230         640  
139 272         470 my $body = '';
140 272 100       550 $body .= $mail->{body} if exists $mail->{body};
141 272 100       538 $body .= "$mail->{mime_preamble}" if exists $mail->{mime_preamble};
142 272 100 100     762 $body .= "--$mail->{mime_boundary}\n" if exists $mail->{mime_boundary} && !exists $mail->{mime_parts};
143 272 100       535 $body .= join('', map { "--$mail->{mime_boundary}\n" . mail2str($_) } @{$mail->{mime_parts}}) if exists $mail->{mime_parts};
  132         294  
  70         135  
144 272 100       567 $body .= "--$mail->{mime_boundary}--\n" if exists $mail->{mime_boundary};
145 272 100       460 $body .= "$mail->{mime_epilogue}" if exists $mail->{mime_epilogue};
146 272 100       545 $body .= mail2str($mail->{mime_message}) if exists $mail->{mime_message};
147 272 100       1295 $body =~ s/^(>*From )/>$1/mg, $body =~ s/([^\n])\n?\z/$1\n\n/ if exists $mail->{mbox};
148 272         1900 return $head . "\n" . $body;
149             }
150              
151             my $bchar = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-.\/:=?";
152             sub mail2multipart
153             {
154 24     24 1 158 my $m = shift;
155 24 100 100     203 return $m if exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i;
156 12         31 my $p = {};
157 12         22 append_header($p, $_) for grep { /^content-/i } @{$m->{headers}};
  60         177  
  12         40  
158 12 100       50 $p->{body} = delete $m->{body} if exists $m->{body};
159 12 100       42 $p->{mime_message} = delete $m->{mime_message} if exists $m->{mime_message};
160 12 100       37 $p->{mime_type} = $m->{mime_type} if exists $m->{mime_type};
161 12         43 $m->{mime_type} = 'multipart/mixed';
162 12 50       45 $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         831  
163 12 50       75 $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         143 delete_header($m, qr/content-[^:]*/i);
166 12 100 66     152 append_header($m, 'MIME-Version: 1.0') unless exists $m->{header} && exists $m->{header}->{'mime-version'};
167 12         69 append_header($m, "Content-Type: $m->{mime_type}; boundary=\"$m->{mime_boundary}\"");
168 12         37 $m->{mime_parts} = [$p];
169 12         62 return $m;
170             }
171              
172             sub mail2singlepart
173             {
174 40     40 1 143 my $m = shift;
175 40 100 100     247 $m->{mime_message} = mail2singlepart($m->{mime_message}), return $m if exists $m->{mime_type} && $m->{mime_type} =~ /^message\//i;
176 36 100 66     258 return $m unless exists $m->{mime_type} && $m->{mime_type} =~ /^multipart\//i && @{$m->{mime_parts}} <= 1;
  20   100     74  
177 16         29 my $p = shift @{$m->{mime_parts}};
  16         34  
178 16 50       60 $m->{mime_prev_boundary} = delete $m->{mime_boundary} if exists $m->{mime_boundary};
179 16 100       59 $m->{mime_prev_preamble} = delete $m->{mime_preamble} if exists $m->{mime_preamble};
180 16 50       39 $m->{mime_prev_epilogue} = delete $m->{mime_epilogue} if exists $m->{mime_epilogue};
181 16 100       52 $m->{body} = $p->{body} if exists $p->{body};
182 16 100       50 $m->{mime_message} = $p->{mime_message} if exists $p->{mime_message};
183 16 100       28 delete $m->{mime_type}; $m->{mime_type} = $p->{mime_type} if exists $p->{mime_type};
  16         39  
184 16 100       29 delete $m->{mime_parts}; $m->{mime_parts} = $p->{mime_parts} if exists $p->{mime_parts};
  16         37  
185 16 100       55 $m->{mime_boundary} = $p->{mime_boundary} if exists $p->{mime_boundary};
186 16 50       32 $m->{mime_preamble} = $p->{mime_preamble} if exists $p->{mime_preamble};
187 16 50       56 $m->{mime_epilogue} = $p->{mime_epilogue} if exists $p->{mime_epilogue};
188 16         27 my $explicit = 0;
189 16         88 delete_header($m, qr/content-[^:]*/i);
190 16         35 append_header($m, $_), ++$explicit for grep { /^content-/i } @{$p->{headers}};
  18         82  
  16         42  
191 16 100       61 delete_header($m, 'mime-version') unless $explicit;
192 16         42 return mail2singlepart($m);
193             }
194              
195             sub mail2mbox
196             {
197 58     58 1 400 my $m = shift;
198 58 100       265 return $m if exists $m->{mbox};
199 20         44 my ($f) = header($m, 'sender');
200 20 50       56 ($f) = header($m, 'from') unless defined $f;
201 20 50       90 $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       45 $f = 'unknown' unless defined $f;
203 2     2   1002 use POSIX; $m->{mbox} = "From $f " . ctime(time());
  2         28226  
  2         11  
  20         573  
204 20         107 return $m;
205             }
206              
207             sub insert_header
208             {
209 4     4 1 21 my ($m, $h, $l, $c) = @_;
210 4         19 $h = header_format($h, $l, $c);
211 4         20 my ($n) = $h =~ /^([^:]+):/;
212 4         8 unshift @{$m->{headers}}, $h;
  4         15  
213 4         6 unshift @{$m->{header}->{lc $n}}, $h;
  4         18  
214             }
215              
216             sub append_header
217             {
218 305     305 1 28243 my ($m, $h, $l, $c) = @_;
219 305         643 $h = header_format($h, $l, $c);
220 305         1227 my ($n) = $h =~ /^([^:]+):/;
221 305         471 push @{$m->{headers}}, $h;
  305         731  
222 305         400 push @{$m->{header}->{lc $n}}, $h;
  305         1140  
223             }
224              
225             sub replace_header
226             {
227 20     20 1 77 my ($m, $h, $l, $c) = @_;
228 20         48 $h = header_format($h, $l, $c);
229 20         105 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       30  
  20         47  
  20         41  
  20         172  
  6         13  
  6         14  
  20         42  
231 20         27 splice @{$m->{header}->{lc $n}};
  20         62  
232 20         30 push @{$m->{header}->{lc $n}}, $h;
  20         64  
233             }
234              
235             sub delete_header
236             {
237 46     46 1 151 my ($m, $h, $r) = @_;
238 46 100       130 return undef unless exists $m->{header};
239 44         63 @{$m->{headers}} = grep { !/^$h:/i } @{$m->{headers}};
  44         132  
  222         1084  
  44         86  
240 44         71 delete $m->{header}->{$_} for grep { /^$h$/i } keys %{$m->{header}};
  222         873  
  44         148  
241 44 100 100     144 if ($r && exists $m->{mime_parts}) { delete_header($_, $h, $r) for @{$m->{mime_parts}} }
  2         6  
  2         14  
242 44 100 100     119 if ($r && exists $m->{mime_message}) { delete_header($m->{mime_message}, $h, $r) }
  2         14  
243             }
244              
245             sub insert_part
246             {
247 2     2 1 16 my ($m, $p, $i) = @_;
248 2   50     5 splice @{$m->{mime_parts}}, $i || 0, 0, $p;
  2         11  
249             }
250              
251             sub append_part
252             {
253 10     10 1 28 my ($m, $p) = @_;
254 10         12 push @{$m->{mime_parts}}, $p;
  10         32  
255             }
256              
257             sub replace_part
258             {
259 2     2 1 19 my ($m, $p, $i) = @_;
260 2         4 splice @{$m->{mime_parts}}, $i, 1, $p;
  2         10  
261             }
262              
263             sub delete_part
264             {
265 2     2 1 6 my ($m, $i) = @_;
266 2         5 splice @{$m->{mime_parts}}, $i, 1;
  2         6  
267             }
268              
269             sub header
270             {
271 359     359 1 14539 my ($m, $h) = @_;
272 359 100 100     1606 return () unless exists $m->{header} && exists $m->{header}->{lc $h};
273 257         360 return map { s/\n\s+/ /g; header_display($_) =~ /^$h:\s*(.*)\s*$/i; $1 } @{$m->{header}->{lc $h}};
  257         700  
  257         592  
  257         1381  
  257         611  
274             }
275              
276             sub headers
277             {
278 2     2 1 7 my $m = shift;
279 2 50       9 return () unless exists $m->{headers};
280 2         4 return map { s/\n\s+/ /g; header_display($_) =~ /^([\w-]+:.*)\s*$/; $1 } @{$m->{headers}};
  8         16  
  8         15  
  8         36  
  2         6  
281             }
282              
283             sub header_names
284             {
285 2     2 1 6997 my $m = shift;
286 2 50       10 return () unless exists $m->{header};
287 2         5 return keys %{$m->{header}};
  2         23  
288             }
289              
290             my $encword = qr/=\?([^*?]+)(?:\*\w+)?\?(q|b)\?([^? ]+)\?=/i; # encoded words to display
291             sub header_display # rfc2047, rfc2231
292             {
293 2     2   9152 use Encode ();
  2         20862  
  2         905  
294             return join '',
295 525         875 map { tr/ \t/ /s; $_ } # finally, squeeze multiple whitespace
  525         3543  
296 525         988 map { tr/\x00-\x08\x0b-\x1f\x7f//d; $_ } # strip control characters
  525         882  
297 525 100       1564 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       509  
  525         1535  
298 525         2772 map { s/($encword)\s+($encword)/$1$5/g while /$encword\s+$encword/; $_ } # strip space between encoded words that we're about to decode
  525         1013  
299 273 100   273 0 9787 map { s/\((?:\\[^\r\n]|[^\\()])*\)//g unless /^".*"$/; $_ } # strip (comments) outside "quoted strings"
  525         1652  
  525         1096  
300             split /("(?:\\[^\r\n]|[^\\"])*")/, shift; # split on "quoted strings"
301             }
302              
303             sub charsetof
304             {
305 87     87 0 148 my $s = shift;
306 87 100 100     489 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         88  
309 32 50 33     1823 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 561 my ($h, $l, $c) = @_;
315 329         1483 $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         3812  
  329         2298  
  12188         17659  
317 329 50 33     2545 $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     3408  
  6 100       23  
  13 100       44  
  393 50       1488  
    100          
318 329 100 100     833 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       1332  
  393 100       1329  
  993         4030  
319 329         965 return $f . "\n";
320             }
321              
322             sub param # rfc2231, rfc2045
323             {
324 80     80 1 300 my ($m, $h, $p) = @_;
325 80         140 my @p; my $decode = 0;
  80         193  
326              
327 80         332 for (header($m, $h))
328             {
329 80         2294 while (/(\b\Q$p\E(?:\*|\*\d\*?)?)=("(?:\\[^\n]|[^"\n])*"|[^\x00-\x20()<>@,;:\\"\/\[\]?=]+)/ig)
330             {
331 98         543 my ($n, $v) = ($1, $2);
332 98 100       796 $v =~ s/^"//, $v =~ s/"$//, $v =~ s/\\(.)/$1/g if $v =~ /^".*"$/;
333 98 100       360 $v =~ s/^(?:us-ascii|utf-8|iso-8859-\d{1,2})'\w+'//i and $decode = 1;
334 98 100 100     346 $v =~ s/%([\da-fA-f]{2})/chr hex $1/eg if $decode && substr($n, -1) eq '*';
  38         133  
335 98         850 push @p, [lc $n, $v];
336             }
337             }
338              
339 80         287 return join '', map { $_->[1] } sort { my ($ad) = $a->[0] =~ /(\d+)/; my ($bd) = $b->[0] =~ /(\d+)/; $ad <=> $bd } @p;
  98         520  
  26         84  
  26         72  
  26         71  
340             }
341              
342             sub mimetype # rfc2045, rfc2046
343             {
344 100     100 1 265 my ($m, $p) = @_;
345 100         205 my ($e) = header($m, 'content-transfer-encoding');
346 100 100 100     641 return 'application/octet-stream' if defined $e && $e !~ /^(?:[78]bit|binary|quoted-printable|base64)$/i;
347 98         205 my ($type) = header($m, 'content-type');
348 98 100 100     1089 return lc $1 if defined $type && $type =~ /^((?:text|image|audio|video|application|message|multipart)\/[^\s;]+)/i;
349 22 50 100     190 return 'message/rfc822' if !defined $type && defined $p && exists $p->{mime_type} && $p->{mime_type} =~ /^multipart\/digest/i;
      66        
      33        
350 20         111 return 'text/plain';
351             }
352              
353             sub encoding # rfc2045
354             {
355 22     22 1 5630 my $m = shift;
356 22         51 my ($e) = header($m, 'content-transfer-encoding');
357 22 100 100     275 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 32 my $p = shift;
364 4   33     19 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         74 return $fn;
367             }
368              
369             sub body
370             {
371 8     8 1 42 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 18 my $m = shift;
378 6 100       53 return exists $m->{mime_message} ? $m->{mime_message} : undef;
379             }
380              
381             sub parts
382             {
383 12     12 1 35 my ($m, $p) = @_;
384 12 100       60 return exists $m->{mime_parts} ? [@{$m->{mime_parts}}] : [] unless defined $p;
  6 100       35  
385 2         5 $m->{mime_parts} = [@{$p}];
  2         9  
386             }
387              
388             sub newparam # rfc2231, rfc2045
389             {
390 46     46 1 7918 my ($n, $v, $l, $c) = (@_, '', '');
391 46         78 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     111 $c = charsetof($v) if $enc && !$c;
395 46 100 100     98 $l = 'en' if $c && !$l;
396 46 100       127 $v = "$c'$l'$v" if $enc;
397 46         56 my @p; push @p, $_ while $_ = substr $v, 0, 40, '';
  46         224  
398 46         73 s/([\x00-\x20\x7f-\xff])/sprintf '%%%02X', ord $1/eg for grep { tr/\x00-\x06\x0e-\x1f\x7f-\xff// } @p;
  58         141  
  28         98  
399 46         67 s/"/\\"/g, s/^/"/g, s/$/"/g for grep { tr/\x00-\x06\x0e-\x1f\x7f ()<>@,;:\\"\/[]?=// } @p;
  58         205  
400 46 100       300 return "; $n$enc=$p[0]" if @p == 1;
401 6         18 return join '', map { "; $n*$_$enc=$p[$_]" } 0..$#p;
  18         93  
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 16478 my @a = @_; my %a = @_; my $m = {};
  64         178  
  64         116  
408 2     2 0 25 sub rfc822date { use POSIX; return strftime '%a, %d %b %Y %H:%M:%S +0000', gmtime shift; }
  2     36   5  
  2         15  
  36         1580  
409 64   66     339 my $type = $a{type} || (exists $a{parts} ? 'multipart/mixed' : exists $a{message} ? 'message/rfc822' : 'text/plain');
410 64         181 my $multi = $type =~ /^multipart\//i;
411 64         134 my $msg = $type =~ /^message\/rfc822$/i;
412 64 50 100     361 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         8 $a{body} = do { local $/; my $b = <$fh>; close $fh; $b };
  2         13  
  2         48  
  2         26  
  2         13  
415 2 50       16 $a{created} = (exists $a{created}) ? $a{created} : rfc822date((stat _)[9]);
416 2 50       26 $a{modified} = (exists $a{modified}) ? $a{modified} : rfc822date((stat _)[9]);
417 2 50       16 $a{read} = (exists $a{read}) ? $a{read} : rfc822date((stat _)[8]);
418 2         17 $a{size} = (stat _)[7];
419             }
420 64 100       170 ($a{filename}) = $a{filename} =~ /([^\\\/]+)$/ if $a{filename};
421 64 100       129 my $bound = $multi ? join '', map { substr $bchar, int(rand(length $bchar)), 1 } 0..30 : '';
  186         424  
422 64   33     317 my $disp = $a{disposition} || ($type =~ /^(?:text\/|message\/rfc822)/i ? 'inline' : 'attachment');
423 64   33     219 my $char = $a{charset} || charsetof($a{body});
424 64   33     340 my $enc = $a{encoding} || ($multi || $msg ? '7bit' : $a{body} ? choose_encoding($a{body}) : '7bit');
425 64 100       165 append_header($m, $a[$_] . ': ' . $a[$_ + 1]) for grep { $_ % 2 == 0 && $a[$_] =~ /^[A-Z]/ } 0..$#a;
  320         1133  
426 64 100 66     182 append_header($m, 'Date: ' . rfc822date(time)) if grep { /^(?:date|from|sender|reply-to)$/i } keys %a and !grep { /^date$/i } keys %a;
  170         502  
  122         307  
427 64 100 66     174 append_header($m, 'MIME-Version: 1.0') if grep { /^(?:date|from|sender|reply-to)$/i } keys %a and !grep { /^mime-version$/ } keys %a;
  170         479  
  122         264  
428 2 100 66 2   8776 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         2110  
  2         5957  
  64         153  
  30         173  
  30         81  
  30         99  
  170         488  
  122         243  
429 64 100 100     422 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       255 append_header($m, "Content-Transfer-Encoding: $enc") unless $enc =~ /^7bit$/i;
431 64 50 66     426 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         114 append_header($m, "Content-@{[ucfirst $_]}: $a{$_}") for grep { $a{$_} } qw(description language duration location base features alternative);
  448         691  
  0         0  
433 64         98 append_header($m, "Content-@{[uc $_]}: $a{$_}") for grep { $a{$_} } qw(id md5);
  128         200  
  0         0  
434 64 100 50     137 ($m->{mime_type}, $m->{mime_boundary}, $m->{mime_parts}) = ($type =~ /^\s*([\w\/.-]+)/, $bound, $a{parts} || []) if $multi;
435 64 100 50     165 ($m->{mime_type}, $m->{mime_message}) = ($type =~ /^\s*([\w\/.-]+)/, $a{message} || {}) if $msg;
436 64 100 50     270 $m->{body} = encode($a{body} || '', $enc) unless $multi || $msg;
      100        
437 64 0 33     173 $m->{mbox} = $a{mbox} if exists $a{mbox} && defined $a{mbox} && length $a{mbox};
      33        
438 64         376 return $m;
439             }
440              
441             sub decode
442             {
443 4     4 0 22 my ($d, $e) = @_;
444 4 50       65 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 93 my ($d, $e) = @_;
450 46 100       186 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 86 my $len = length $_[0];
456 46         102 my $high = $_[0] =~ tr/\x80-\xff//;
457 46         92 my $ctrl = $_[0] =~ tr/\x00-\x06\x0e-\x1f\x7f//;
458 46         87 my ($maxlen, $pos, $next) = (0, 0, 0);
459              
460 46         130 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       92 $maxlen = $len - $pos if $len - $pos > $maxlen;
466 46 50 33     194 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 47 pos $_[0] = 0; # Note: Text must be in canonical form (i.e. with "\r\n")
472 14         40 my $padlen = (3 - length($_[0]) % 3) % 3;
473 14         55 my $encoded = join '', map { pack('u', $_) =~ /^.(\S*)/ } $_[0] =~ /(.{1,45})/gs;
  14         90  
474 14         33 $encoded =~ tr{` -_}{AA-Za-z0-9+/};
475 14 100       155 $encoded =~ s/.{$padlen}$/'=' x $padlen/e if $padlen;
  12         42  
476 14         73 $encoded =~ s/(.{1,76})/$1\n/g;
477 14         64 return $encoded;
478             }
479              
480             sub decode_base64 # MIME::Base64 (Gisle Aas)
481             {
482 12     12 0 6963 my $data = shift;
483 12         55 $data =~ tr{A-Za-z0-9+=/}{}cd;
484 12         59 $data =~ s/=+$//;
485 12         37 $data =~ tr{A-Za-z0-9+/}{ -_};
486 12         130 return join '', map { unpack('u', chr(32 + length($_) * 3 / 4) . $_) } $data =~ /(.{1,60})/gs;
  132         450  
487             }
488              
489             sub encode_quoted_printable
490             {
491 25     25 0 94 my $quoted = shift;
492 25         36 my $qcode = shift;
493 25 50       70 my $binary = ($quoted =~ tr/\x00-\x06\x0e-\x1f\x7f//) ? '' : '\r\n';
494 25         189 $quoted =~ s/([^!-<>-~ \t$binary])/sprintf '=%02X', ord $1/eg;
  26         155  
495 25 100       65 $quoted =~ s/([?_])/sprintf '=%02X', ord $1/eg if $qcode;
  4         17  
496 25         278 $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         50 $quoted .= "\n";
501 25         147 return $quoted;
502             }
503              
504             sub decode_quoted_printable
505             {
506 19     19 0 2139 my $quoted = shift;
507 19         28 my $qcode = shift;
508 19         37 $quoted =~ tr/\x00-\x08\x0b-\x0c\x0e-\x1f\x7f-\xff//d;
509 19         38 $quoted =~ s/=\n//g;
510 19 50       61 $quoted =~ s/_/ /g if $qcode;
511 19         61 $quoted =~ s/=([0-9A-Fa-f]{2})/chr hex $1/eg;
  20         80  
512 19         58 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 177 return if $add_mimetypes++;
554 2 50       104 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 116 my $m = shift;
569 62 100 33     592 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     84 @{$m->{mime_parts}} = grep { defined $_ } map { (mimetype($_) =~ /^application\/ms-tnef/i && filename($_) =~ /winmail\.dat$/i) ? winmail($_) : $_ } @{$m->{mime_parts}};
  60         160  
  90         220  
  90         207  
  60         141  
572 60         221 return $m;
573             }
574              
575 18     18 0 66 sub MESSAGE { 1 }
576 2     2 0 12 sub ATTACHMENT { 2 }
577 2     2 0 17851 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 36 my $type = unpack 'C', substr $data, $pos, 1;
590 14 100 66     49 return 0 unless defined $type && $type == MESSAGE; ++$pos;
  12         18  
591 12         23 my $id = unpack 'V', substr $data, $pos, 4; $pos += 4;
  12         18  
592 12         24 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         17 my $buf = substr $data, $pos, $len; $pos += $len;
  10         19  
595 10         20 my $chk = unpack 'v', substr $data, $pos, 2; $pos += 2;
  10         18  
596 10         24 my $tot = unpack '%16C*', $buf;
597 10 50       19 ++$badtnef unless $chk == $tot;
598 10         34 return $chk == $tot;
599             }
600              
601             sub read_attribute_message_class
602             {
603 2     2 0 14 my $type = unpack 'C', substr $data, $pos, 1;
604 2 50 33     24 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 7 my $type = unpack 'C', substr $data, $pos, 1;
618 2 50 33     21 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 21 use POSIX; sub word { unpack 'v', substr($_[0], $_[1] * 2, 2) }
  2     0   17  
  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 26 my $m = shift;
647 2         16 add_mimetypes();
648 2         19 $pos = 0; $data = body($m); @attachment = (); $badtnef = 0;
  2         16  
  2         11  
  2         10  
649 2         29 my $signature = unpack 'V', substr($data, $pos, 4); $pos += 4;
  2         8  
650 2 50       12 return $m unless $signature == 0x223E9F78;
651 2         15 my $key = unpack 'v', substr($data, $pos, 2); $pos += 2;
  2         5  
652 2         13 my $type = unpack 'C', substr($data, $pos, 1);
653 2 50 33     16 return $m unless $type == MESSAGE || $type == ATTACHMENT;
654 2         22 do {} while read_message_attribute();
655 2         10 read_attribute_message_class();
656 2         8 do {} while read_message_attribute();
657 2         11 do {} while read_attachment_attribute();
658 2 50 33     27 ++$badtnef if @attachment && !exists $attachment->{body};
659 2 50       20 return ($badtnef) ? $m : map { newmail(%{$_}) } @attachment;
  0            
  0            
660             }
661              
662             1;
663              
664             # vi:set ts=4 sw=4: