]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/MIME.pm
* Fix copyright/license statements in a bunch of the modules
[debbugs.git] / Debbugs / MIME.pm
1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
5 #
6 # [Other people have contributed to this file; their copyrights should
7 # go here too.]
8 # Copyright 2006 by Don Armstrong <don@donarmstrong.com>.
9
10
11 package Debbugs::MIME;
12
13 use strict;
14
15 use base qw(Exporter);
16 use vars qw($VERSION @EXPORT_OK);
17
18 BEGIN {
19     $VERSION = 1.00;
20
21     @EXPORT_OK = qw(parse decode_rfc1522 encode_rfc1522 convert_to_utf8 create_mime_message);
22 }
23
24 use File::Path;
25 use MIME::Parser;
26
27 # for decode_rfc1522
28 use MIME::WordDecoder qw();
29 use Encode qw(decode encode encode_utf8 decode_utf8 is_utf8);
30
31 # for encode_rfc1522
32 use MIME::Words qw();
33
34 sub getmailbody ($);
35 sub getmailbody ($)
36 {
37     my $entity = shift;
38     my $type = $entity->effective_type;
39     if ($type eq 'text/plain' or
40             ($type =~ m#text/# and $type ne 'text/html') or
41             $type eq 'application/pgp') {
42         return $entity->bodyhandle;
43     } elsif ($type eq 'multipart/alternative') {
44         # RFC 2046 says we should use the last part we recognize.
45         for my $part (reverse $entity->parts) {
46             my $ret = getmailbody($part);
47             return $ret if $ret;
48         }
49     } else {
50         # For other multipart types, we just pretend they're
51         # multipart/mixed and run through in order.
52         for my $part ($entity->parts) {
53             my $ret = getmailbody($part);
54             return $ret if $ret;
55         }
56     }
57     return undef;
58 }
59
60 sub parse ($)
61 {
62     # header and decoded body respectively
63     my (@headerlines, @bodylines);
64
65     my $parser = new MIME::Parser;
66     mkdir "mime.tmp.$$", 0777;
67     $parser->output_under("mime.tmp.$$");
68     my $entity = eval { $parser->parse_data($_[0]) };
69
70     if ($entity and $entity->head->tags) {
71         @headerlines = @{$entity->head->header};
72         chomp @headerlines;
73
74         my $entity_body = getmailbody($entity);
75         @bodylines = $entity_body ? $entity_body->as_lines() : ();
76         chomp @bodylines;
77     } else {
78         # Legacy pre-MIME code, kept around in case MIME::Parser fails.
79         my @msg = split /\n/, $_[0];
80         my $i;
81
82         for ($i = 0; $i <= $#msg; ++$i) {
83             $_ = $msg[$i];
84             last unless length;
85             while ($msg[$i + 1] =~ /^\s/) {
86                 ++$i;
87                 $_ .= "\n" . $msg[$i];
88             }
89             push @headerlines, $_;
90         }
91
92         @bodylines = @msg[$i .. $#msg];
93     }
94
95     rmtree "mime.tmp.$$", 0, 1;
96
97     # Remove blank lines.
98     shift @bodylines while @bodylines and $bodylines[0] !~ /\S/;
99
100     # Strip off RFC2440-style PGP clearsigning.
101     if (@bodylines and $bodylines[0] =~ /^-----BEGIN PGP SIGNED/) {
102         shift @bodylines while @bodylines and length $bodylines[0];
103         shift @bodylines while @bodylines and $bodylines[0] !~ /\S/;
104         for my $findsig (0 .. $#bodylines) {
105             if ($bodylines[$findsig] =~ /^-----BEGIN PGP SIGNATURE/) {
106                 $#bodylines = $findsig - 1;
107                 last;
108             }
109         }
110         map { s/^- // } @bodylines;
111     }
112
113     return { header => [@headerlines], body => [@bodylines]};
114 }
115
116 =head2 create_mime_message
117
118      create_mime_message([To=>'don@debian.org'],$body,[$attach1, $attach2]);
119
120 Creates a MIME encoded message with headers given by the first
121 argument, and a message given by the second.
122
123 Optional attachments can be specified in the third arrayref argument.
124
125 Headers are passed directly to MIME::Entity::build, the message is the
126 first attachment.
127
128 Each of the elements of the attachment arrayref is attached as an
129 rfc822 message if it is a scalar or an arrayref; otherwise if it is a
130 hashref, the contents are passed as an argument to
131 MIME::Entity::attach
132
133 =cut
134
135 sub create_mime_message{
136      my ($headers,$body,$attachments) = @_;
137      $attachments = [] if not defined $attachments;
138
139      die "The first argument to create_mime_message must be an arrayref" unless ref($headers) eq 'ARRAY';
140      die "The third argument to create_mime_message must be an arrayref" unless ref($attachments) eq 'ARRAY';
141
142      # Build the message
143      # MIME::Entity is stupid, and doesn't rfc1522 encode its headers, so we do it for it.
144      my $msg = MIME::Entity->build('Content-Type' => 'text/plain; charset=utf-8',
145                                    'Encoding'     => 'quoted-printable',
146                                    (map{encode_rfc1522($_)} @{$headers}),
147                                    Data    => $body
148                                   );
149
150      # Attach the attachments
151      for my $attachment (@{$attachments}) {
152           if (ref($attachment) eq 'HASH') {
153                $msg->attach(%{$attachment});
154           }
155           else {
156                # This is *craptacular*, but because various MTAs
157                # (sendmail and exim4, at least) appear to eat From
158                # lines in message/rfc822 attachments, we need eat
159                # the entire From line ourselves so the MTA doesn't
160                # leave \n detrius around.
161                if (ref($attachment) eq 'ARRAY' and $attachment->[1] =~ /^From /) {
162                     # make a copy so that we don't screw up anything
163                     # that is expecting this arrayref to stay constant
164                     $attachment = [@{$attachment}];
165                     # remove the from line
166                     splice @$attachment, 1, 1;
167                }
168                elsif (not ref($attachment)) {
169                     # It's a scalar; remove the from line
170                     $attachment =~ s/^(Received:[^\n]+\n)(From [^\n]+\n)/$1/s;
171                }
172                $msg->attach(Type => 'message/rfc822',
173                             Data => $attachment,
174                             Encoding => '7bit',
175                            );
176           }
177      }
178      return $msg->as_string;
179 }
180
181
182 # Bug #61342 et al.
183
184 sub convert_to_utf8 {
185      my ($data, $charset) = @_;
186      # raw data just gets returned (that's the charset WordDecorder
187      # uses when it doesn't know what to do)
188      return $data if $charset eq 'raw' or is_utf8($data,1);
189      my $result;
190      eval {
191           # this encode/decode madness is to make sure that the data
192           # really is valid utf8 and that the is_utf8 flag is off.
193           $result = encode("utf8",decode($charset,$data))
194      };
195      if ($@) {
196           warn "Unable to decode charset; '$charset' and '$data': $@";
197           return $data;
198      }
199      return $result;
200 }
201
202
203 =head2 decode_rfc1522
204
205     decode_rfc1522('=?iso-8859-1?Q?D=F6n_Armstr=F3ng?= <don@donarmstrong.com>')
206
207 Turn RFC-1522 names into the UTF-8 equivalent.
208
209 =cut
210
211 BEGIN {
212     # Set up the default RFC1522 decoder, which turns all charsets that
213     # are supported into the appropriate UTF-8 charset.
214     MIME::WordDecoder->default(new MIME::WordDecoder(
215         ['*' => \&convert_to_utf8,
216         ]));
217 }
218
219 sub decode_rfc1522 ($)
220 {
221     my ($string) = @_;
222
223     # this is craptacular, but leading space is hacked off by unmime.
224     # Save it.
225     my $leading_space = '';
226     $leading_space = $1 if $string =~ s/^(\s+)//;
227     # unmime calls the default MIME::WordDecoder handler set up at
228     # initialization time.
229     return $leading_space . MIME::WordDecoder::unmime($string);
230 }
231
232 =head2 encode_rfc1522
233
234      encode_rfc1522('Dön Armströng <don@donarmstrong.com>')
235
236 Encodes headers according to the RFC1522 standard by calling
237 MIME::Words::encode_mimeword on distinct words as appropriate.
238
239 =cut
240
241 # We cannot use MIME::Words::encode_mimewords because that function
242 # does not handle spaces properly at all.
243
244 sub encode_rfc1522 ($) {
245      my ($rawstr) = @_;
246
247      # We process words in reverse so we can preserve spacing between
248      # encoded words. This regex splits on word|nonword boundaries and
249      # nonword|nonword boundaries.
250      my @words = reverse split /(?:(?<=[\s\n])|(?=[\s\n]))/m, $rawstr;
251
252      my $previous_word_encoded = 0;
253      my $string = '';
254      for my $word (@words) {
255           if ($word !~ m#[\x00-\x1F\x7F-\xFF]#o and $word ne ' ') {
256                $string = $word.$string;
257                $previous_word_encoded=0;
258           }
259           elsif ($word =~ /^[\s\n]$/) {
260                $string = $word.$string;
261                $previous_word_encoded = 0 if $word eq "\n";
262           }
263           else {
264                my $encoded = MIME::Words::encode_mimeword($word, 'q', 'UTF-8');
265                # RFC 1522 mandates that segments be at most 76 characters
266                # long. If that's the case, we split the word up into 10
267                # character pieces and encode it. We must use the Encode
268                # magic here to avoid breaking on bit boundaries here.
269                if (length $encoded > 75) {
270                     # Turn utf8 into the internal perl representation
271                     # so . is a character, not a byte.
272                     my $tempstr = decode_utf8($word,Encode::FB_DEFAULT);
273                     my @encoded;
274                     # Strip it into 10 character long segments, and encode
275                     # the segments
276                     # XXX It's possible that these segments are > 76 characters
277                     while ($tempstr =~ s/(.{1,10})$//) {
278                          # turn the character back into the utf8 representation.
279                          my $tempword = encode_utf8($1);
280                          # It may actually be better to eventually use
281                          # the base64 encoding here, but I'm not sure
282                          # if that's as widely supported as quoted
283                          # printable.
284                          unshift @encoded, MIME::Words::encode_mimeword($tempword,'q','UTF-8');
285                     }
286                     $encoded = join(" ",@encoded);
287                     # If the previous word was encoded, we must
288                     # include a trailing _ that gets encoded as a
289                     # space.
290                     $encoded =~ s/\?\=$/_\?\=/ if $previous_word_encoded;
291                     $string = $encoded.$string;
292                }
293                else {
294                     # If the previous word was encoded, we must
295                     # include a trailing _ that gets encoded as a
296                     # space.
297                     $encoded =~ s/\?\=$/_\?\=/ if $previous_word_encoded;
298                     $string = $encoded.$string;
299                }
300                $previous_word_encoded = 1;
301           }
302      }
303      return $string;
304 }
305
306 1;