source: trunk/gsdl/perllib/plugins/EMAILPlug.pm@ 3215

Last change on this file since 3215 was 3215, checked in by jrm21, 22 years ago

Fixed up some regexs for mime header encodings - eg people with accents in
their first names was not always displayed properly.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 26.0 KB
Line 
1###########################################################################
2#
3# EMAILPlug.pm - a plugin for parsing email files
4#
5# A component of the Greenstone digital library software
6# from the New Zealand Digital Library Project at the
7# University of Waikato, New Zealand.
8#
9# Copyright (C) 1999-2001 New Zealand Digital Library Project
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24#
25###########################################################################
26
27
28
29# EMAILPlug
30#
31# by Gordon Paynter ([email protected])
32#
33# Email plug reads email files. These are named with a simple
34# number (i.e. as they appear in maildir folders) or with the
35# extension .mbx (for mbox mail file format)
36#
37# Document text:
38# The document text consists of all the text
39# after the first blank line in the document.
40#
41# Metadata (not Dublin Core!):
42# $Headers All the header content
43# $Subject Subject: header
44# $To To: header
45# $From From: header
46# $FromName Name of sender (where available)
47# $FromAddr E-mail address of sender
48# $DateText Date: header
49# $Date Date: header in GSDL format (eg: 19990924)
50#
51# $Title made up of Subject, Date and Sender (for default formatting)
52#
53#
54# John McPherson - June/July 2001
55# added (basic) MIME support and quoted-printable and base64 decodings.
56# Minor fixes for names that are actually email addresses (ie <...> was lost)
57#
58# See: * RFC 822 - ARPA Internet Text Messages
59# * RFC 2045 - Multipurpose Internet Mail Extensions (MIME) -part1
60# * RFC 2046 - MIME (part 2) Media Types (and multipart messages)
61# * RFC 2047 - MIME (part 3) Message Header Extensions
62# * RFC 1806 - Content Dispositions (ie inline/attachment)
63package EMAILPlug;
64
65use SplitPlug;
66
67use unicode;
68
69use sorttools;
70use util;
71
72
73# EMAILPlug is a sub-class of SplitPlug.
74
75sub BEGIN {
76 @ISA = ('SplitPlug');
77}
78
79# Create a new EMAILPlug object with which to parse a file.
80# Accomplished by creating a new BasPlug and using bless to
81# turn it into an EMAILPlug.
82
83sub new {
84 my ($class) = @_;
85 my $self = new BasPlug ("EMAILPlug", @_);
86 # this might not actually be true at read-time, but after processing
87 # it should all be utf8.
88 $self->{'input_encoding'}="utf8";
89 return bless $self, $class;
90}
91
92sub get_default_process_exp {
93 my $self = shift (@_);
94 # mbx/email for mailbox file format, \d+ for maildir (each message is
95 # in a separate file, with a unique number for filename)
96 # mozilla and IE will save individual mbx format files with a ".eml" ext.
97 return q@([\\/]\d+|\.(mbx|email|eml))$@;
98}
99
100# This plugin splits the mbox mail files at lines starting with From<sp>
101# It is supposed to be "\n\nFrom ", but this isn't always used.
102sub get_default_split_exp {
103 return q^\nFrom .*\n^;
104}
105
106
107# do plugin specific processing of doc_obj
108sub process {
109
110 my $self = shift (@_);
111 my ($textref, $pluginfo, $base_dir, $file, $metadata, $doc_obj) = @_;
112 my $outhandle = $self->{'outhandle'};
113
114 # Check that we're dealing with a valid mail file
115 # mbox message files start with "From "
116 # maildir messages usually start with Return-Path and Delivered-To
117 # mh is very similar to maildir
118 my $startoffile=substr($$textref,0,256);
119 if (($startoffile !~ /^(From )/) &&
120 ($startoffile !~ /^(From|To|Envelope.*|Received|Return-Path|Date|Subject|Content\-.*|MIME-Version|Forwarded):/im)) {
121 return undef;
122 }
123
124
125 print $outhandle "EMAILPlug: processing $file\n"
126 if $self->{'verbosity'} > 1;
127
128 my $cursection = $doc_obj->get_top_section();
129
130 #
131 # Parse the document's text and extract metadata
132 #
133
134 # Protect backslashes
135 $$textref =~ s@\\@\\\\@g;
136
137 # Separate header from body of message
138 my $Headers = $$textref;
139 $Headers =~ s/\r?\n\r?\n(.*)$//s;
140 $$textref = $1;
141
142 # Unfold headers - see rfc822
143 $Headers =~ s/\r?\n[\t\ ]+/ /gs;
144 # Extract basic metadata from header
145 my @headers = ("From", "To", "Subject", "Date");
146 my %raw;
147 foreach my $name (@headers) {
148 $raw{$name} = "No $name value";
149 }
150
151 # Get a default encoding for the header - RFC says should be ascii...
152 my $default_heading_encoding="iso_8859_1";
153
154 # We don't know what character set is the user's default...
155 # We could use textcat to guess... for now we'll look at mime content-type
156# if ($Headers =~ /([[:^ascii:]])/) {
157# }
158 if ($Headers =~ /^Content\-type:.*charset=\"?([a-z0-9\-_]+)/mi) {
159 $default_header_encoding=$1;
160 $default_header_encoding =~ s@\-@_@g;
161 $default_header_encoding =~ tr/A-Z/a-z/;
162 }
163
164
165 # Examine each line of the headers
166 my ($line, $name, $value);
167 my @parts;
168 foreach $line (split(/\n/, $Headers)) {
169
170 # Ignore lines with no content or which begin with whitespace
171 next unless ($line =~ /:/);
172 next if ($line =~ /^\s/);
173
174 # Find out what metadata is on this line
175 @parts = split(/:/, $line);
176 $name = shift @parts;
177 # get fieldname in canonical form - first cap, then lower case.
178 $name =~ tr/A-Z/a-z/;
179 # uppercase the first character according to the current locale
180 $name=~s/(.+)/\u$1/;
181 next unless $name;
182 next unless ($raw{$name});
183
184 # Find the value of that metadata
185 $value = join(":", @parts);
186 $value =~ s/^\s+//;
187 $value =~ s/\s+$//;
188
189 # decode headers if stored using =?<charset>?[BQ]?<data>?= (rfc2047)
190 if ($value =~ /=\?.*\?[BbQq]\?.*\?=/) {
191 my $original_value=$value;
192 my $encoded=$value;
193 $value="";
194 # we should ignore spaces between consecutive encoded-texts
195 $encoded =~ s@\?=\s+=\?@\?==\?@g;
196 while ($encoded =~ s/(.*?)=\?([^\?]*)\?([bq])\?([^\?]+)\?=//i) {
197 my ($charset, $encoding, $data)=($2,$3,$4);
198 my ($decoded_data);
199 $value.="$1"; # any leading chars
200 $data=~s/^\s*//; $data=~s/\s*$//; # strip whitespace from ends
201 chomp $data;
202 $encoding =~ tr/BQ/bq/;
203 if ($encoding eq "q") { # quoted printable
204 $data =~ s/_/\ /g; # from rfc2047 (sec 4.2.2)
205 $decoded_data=qp_decode($data);
206 # qp_decode adds \n, which is default for body text
207 chomp($decoded_data);
208 } else { # base 64
209 $decoded_data=base64_decode($data);
210 }
211 $self->convert2unicode($charset, \$decoded_data);
212 $value .= $decoded_data;
213 } # end of while loop
214
215 # get any trailing characters
216 $self->convert2unicode($default_header_encoding, \$encoded);
217 $value.=$encoded;
218
219 if ($value =~ /^\s*$/) { # we couldn't extract anything...
220 $self->convert2unicode($default_header_encoding,
221 \$original_value);
222 $value=original_value;
223 }
224 } # end of if =?...?=
225
226 # In the absense of other charset information, assume the
227 # header is the default (usually "iso_8859_1") and convert it to unicode.
228 else {
229 $self->convert2unicode($default_header_encoding, \$value);
230 }
231
232 # Store the metadata
233 $raw{$name} = $value;
234 }
235
236 # Extract the name and e-mail address from the From metadata
237 $frommeta = $raw{"From"};
238 my $fromnamemeta;
239 my $fromaddrmeta;
240
241 $frommeta =~ s/\s*$//; # Remove trailing space, if any
242
243 if ($frommeta =~ m/(.+)\s*<(.+)>/) {
244 $fromnamemeta=$1;
245 $fromaddrmeta=$2;
246 } elsif ($frommeta =~ m/(.+@.+)\s+\((.*)\)/) {
247 $fromnamemeta=$2;
248 $fromaddrmeta=$1;
249 }
250 if (!defined($fromaddrmeta)) {
251 $fromaddrmeta=$frommeta;
252 }
253 $fromaddrmeta=~s/<//; $fromaddrmeta=~s/>//;
254 # minor attempt to prevent spam-bots from harvesting addresses...
255 $fromaddrmeta=~s/@/&#64;/;
256 $doc_obj->add_utf8_metadata ($cursection, "FromAddr", $fromaddrmeta);
257
258 if (defined($fromnamemeta)) {
259 $fromnamemeta =~ s/\"//g; # remove quotes
260 $fromnamemeta =~ s/\s+$//; # remove trailing whitespace
261 }
262 else {
263 $fromnamemeta = $fromaddrmeta;
264 }
265 # if name is an address
266 $fromnamemeta =~ s/<//g; $fromnamemeta =~ s/>//g;
267 $fromnamemeta=~s/@/&#64\;/;
268 $doc_obj->add_utf8_metadata ($cursection, "FromName", $fromnamemeta);
269
270 $raw{"From"}=$frommeta;
271
272 # Process Date information
273 if ($raw{"Date"} !~ /No Date/) {
274 $raw{"DateText"} = $raw{"Date"};
275
276 # Convert the date text to internal date format
277 $value = $raw{"Date"};
278 my ($day, $month, $year) = $value =~ /(\d?\d)\s([A-Z][a-z][a-z])\s(\d\d\d?\d?)/;
279 # make some assumptions about the year formatting...
280 # some (old) software thinks 2001 is 101, some think 2001 is 01
281 if ($year < 20) { $year += 2000; } # assume not really 1920...
282 elsif ($year < 150) { $year += 1900; } # assume not really 2150...
283 $raw{"Date"} = &sorttools::format_date($day, $month, $year);
284
285 } else {
286 # We have not extracted a date
287 $raw{"DateText"} = "Unknown.";
288 $raw{"Date"} = "19000000";
289 }
290
291 # Add extracted metadata to document object
292 foreach my $name (keys %raw) {
293 $value = $raw{$name};
294 if ($value) {
295 # assume subject, etc headers have no special HTML meaning.
296 $value = &text_into_html($value);
297 # escape [] so it isn't re-interpreted as metadata
298 $value =~ s/\[/&#91;/g; $value =~ s/\]/&#93;/g;
299 } else {
300 $value = "No $name field";
301 }
302 $doc_obj->add_utf8_metadata ($cursection, $name, $value);
303 }
304
305 my $mimetype="text/plain";
306 my $mimeinfo="";
307 my $charset = $default_header_encoding;
308 # Do MIME and encoding stuff. Allow \s in mimeinfo in case there is
309 # more than one parameter given to Content-type.
310 # eg: Content-type: text/plain; charset="us-ascii"; format="flowed"
311 if ($Headers =~ /^content\-type:\s*([\w\/\-]+)\s*(\;\s*.*)\s*$/mi)
312 {
313 $mimetype=$1;
314 $mimetype =~ tr/[A-Z]/[a-z]/;
315
316 if ($mimetype eq "text") { # for pre-RFC2045 messages (c. 1996)
317 $mimetype = "text/plain";
318 }
319
320 $mimeinfo=$2;
321 if (!defined $mimeinfo) {
322 $mimeinfo="";
323 } else { # strip leading and trailing stuff
324 $mimeinfo =~ s/^\;\s*//;
325 $mimeinfo =~ s/\s*$//;
326 }
327 if ($mimeinfo =~ /charset=\"([^\"]+)\"/i) {
328 $charset = $1;
329 }
330 }
331
332 my $transfer_encoding="7bit";
333 if ($Headers =~ /^content-transfer-encoding:\s*([^\s]+)\s*$/mi) {
334 $transfer_encoding=$1;
335 }
336 if ($mimetype eq "text/html") {
337 $$textref= $self->text_from_part("$Headers\n$$textref");
338 } elsif ($mimetype ne "text/plain") {
339 $$textref= $self->text_from_mime_message($mimetype,$mimeinfo,$$textref,
340 $outhandle);
341 } elsif ($transfer_encoding =~ /quoted\-printable/) {
342 $$textref=qp_decode($$textref);
343 $self->convert2unicode($charset, $textref);
344 } elsif ($transfer_encoding =~ /base64/) {
345 $$textref=base64_decode($$textref);
346 $self->convert2unicode($charset, $textref);
347 } else {
348 $self->convert2unicode($charset, $textref);
349 }
350
351
352 # Add "All headers" metadata
353 $Headers = &text_into_html($Headers);
354
355 $Headers = "No headers" unless ($Headers =~ /\w/);
356 $Headers =~ s/@/&#64\;/g;
357 # escape [] so it isn't re-interpreted as metadata
358 $Headers =~ s/\[/&#91;/g; $Headers =~ s/\]/&#93;/g;
359 $self->convert2unicode($charset, \$Headers);
360 $doc_obj->add_utf8_metadata ($cursection, "Headers", $Headers);
361
362
363 # Add Title metadata
364 my $Title = text_into_html($raw{'Subject'});
365 $Title .= "<br>From: " . text_into_html($raw{'From'});
366 $Title .= "<br>Date: " . text_into_html($raw{'DateText'});
367 $Title =~ s/\[/&#91;/g; $Title =~ s/\]/&#93;/g;
368
369 $doc_obj->add_utf8_metadata ($cursection, "Title", $Title);
370
371
372 # Add text to document object
373 if ($mimetype eq "text/plain") {
374 $$textref = &text_into_html($$textref);
375 }
376 $$textref = "No message" unless ($$textref =~ /\w/);
377 $doc_obj->add_utf8_text($cursection, $$textref);
378
379 return 1;
380}
381
382
383# Convert a text string into HTML.
384#
385# The HTML is going to be inserted into a GML file, so
386# we have to be careful not to use symbols like ">",
387# which ocurs frequently in email messages (and use
388# &gt instead.
389#
390# This function also turns links and email addresses into hyperlinks,
391# and replaces carriage returns with <BR> tags (and multiple carriage
392# returns with <P> tags).
393
394
395sub text_into_html {
396 my ($text) = @_;
397
398 # Convert problem characters into HTML symbols
399 $text =~ s/&/&amp;/g;
400 $text =~ s/</&lt;/g;
401 $text =~ s/>/&gt;/g;
402 $text =~ s/\"/&quot;/g;
403
404 # convert email addresses and URIs into links
405# don't markup email addresses for now
406# $text =~ s/([\w\d\.\-]+@[\w\d\.\-]+)/<a href=\"mailto:$1\">$1<\/a>/g;
407
408 # try to munge email addresses a little bit...
409 $text =~ s/@/&#64;/;
410 # assume hostnames are \.\w\- only, then might have a trailing '/.*'
411 # assume URI doesn't finish with a '.'
412 $text =~ s@((http|ftp|https)://[\w\-]+(\.[\w\-]+)*/?((&amp;|\.)?[\w\?\=\-_/~]+)*)@<a href=\"$1\">$1<\/a>@g;
413
414
415 # Clean up whitespace and convert \n charaters to <BR> or <P>
416 $text =~ s/ +/ /g;
417 $text =~ s/\s*$//g;
418 $text =~ s/^\s*//g;
419 $text =~ s/\n/\n<br>/g;
420 $text =~ s/<br>\s*<br>/<p>/gi;
421
422 return $text;
423}
424
425
426
427
428#Process a MIME message.
429# the textref we are given DOES NOT include the header.
430sub text_from_mime_message {
431 my $self = shift(@_);
432 my ($mimetype,$mimeinfo,$text,$outhandle)=(@_);
433
434 # Check for multiparts - $mimeinfo will be a boundary
435 if ($mimetype =~ /multipart/) {
436 $boundary="";
437 if ($mimeinfo =~ m@boundary=(\"[^\"]+\"|[^\s]+)\s*$@im) {
438 $boundary=$1;
439 if ($boundary =~ m@^\"@) {
440 $boundary =~ s@^\"@@; $boundary =~ s@\"$@@;
441 }
442 } else {
443 print $outhandle "EMAILPlug: (warning) couldn't parse MIME boundary\n";
444 }
445 # parts start with "--$boundary"
446 # message ends with "--$boundary--"
447 # RFC says boundary is <70 chars, [A-Za-z'()+_,-./:=?], so escape any
448 # that perl might want to interpolate. Also allows spaces...
449 $boundary=~s/\\/\\\\/g;
450 $boundary=~s/([\?\+\.\(\)\:\/\'])/\\$1/g;
451 my @message_parts = split("\r?\n\-\-$boundary", "\n$text");
452 # remove first "part" and last "part" (final --)
453 shift @message_parts;
454 my $last=pop @message_parts;
455 # if our boundaries are a bit dodgy and we only found 1 part...
456 if (!defined($last)) {$last="";}
457 # make sure it is only -- and whitespace
458 if ($last !~ /^\-\-\s*$/ms) {
459 print $outhandle "EMAILPlug: (warning) last part of MIME message isn't empty\n";
460 }
461 foreach my $message_part (@message_parts) {
462 # remove the leading newline left from split.
463 $message_part=~s/^\r?\n//;
464 }
465 if ($mimetype eq "multipart/alternative") {
466 # check for an HTML version first, then TEXT, otherwise use first.
467 my $part_text="";
468 foreach my $message_part (@message_parts) {
469 if ($message_part =~ m@\s*content\-type:\s*text/html@mis)
470 {
471 # Use the HTML version
472 $part_text= $self->text_from_part($message_part);
473 $mimetype="text/html";
474 last;
475 }
476 }
477 if ($part_text eq "") { # try getting a text part instead
478 foreach my $message_part (@message_parts) {
479 if ($message_part =~ m@^content\-type:\s*text/plain@mis)
480 {
481 # Use the plain version
482 $part_text= $self->text_from_part($message_part);
483 if ($part_text =~/[^\s]/) {
484 $part_text="<pre>".$part_text."</pre>";
485 }
486 $mimetype="text/plain";
487 last;
488 }
489 }
490 }
491 if ($part_text eq "") { # use first part
492 $part_text= $self->text_from_part(shift @message_parts);
493 }
494 if ($part_text eq "") { # we couldn't get anything!!!
495 # or it was an empty message...
496 # do nothing...
497 print $outhandle "EMAILPlug: no text - empty body?\n";
498 } else {
499 $text=$part_text;
500 }
501 } elsif ($mimetype =~ m@multipart/(mixed|digest|related)@) {
502 $text="";
503 foreach my $message_part (@message_parts) {
504 my $part_header=$message_part;
505 my $part_body;
506 if ($message_part=~ /^\s*\n/) {
507 # no header... use defaults
508 $part_body=$message_part;
509 $part_header="Content-type: text/plain; charset=us-ascii";
510 } elsif ($part_header=~s/\r?\n\r?\n(.*)$//sg) {
511 $part_body=$1;
512 } else {
513 # something's gone wrong...
514 $part_header="";
515 $part_body=$message_part;
516 }
517
518 $part_header =~ s/\r?\n[\t\ ]+/ /gs; #unfold
519 my $part_content_type="";
520 my $part_content_info="";
521 if ($mimetype eq "multipart/digest") {
522 # default type - RTFRFC!!
523 $part_content_type="message/rfc822";
524 }
525 if ($part_header =~ m@^content\-type:\s*([\w+/\-]+)\s*\;?\s*(.*?)\s*$@mi) {
526 $part_content_type=$1; $part_content_type =~ tr/A-Z/a-z/;
527 $part_content_info=$2;
528 }
529 my $filename="";
530 if ($part_header =~ m@name=\"?([\w\.\-\\/]+)\"?@mis) {
531 $filename=$1;
532 }
533
534 # disposition - either inline or attachment.
535 # NOT CURRENTLY USED - we display all text types instead...
536 # $part_header =~ /^content\-disposition:\s*([\w+])/mis;
537
538 # add <<attachment>> to each part except the first...
539 if ($text ne "") {
540 $text.="\n<p><hr><strong>&lt;&lt;attachment&gt;&gt;</>";
541 # add part info header
542 $text.="<br>Type: $part_content_type<br>\n";
543 if ($filename ne "") {
544 $text.="Filename: $filename\n";
545 }
546 $text.="</strong></p>\n";
547 }
548
549 if ($part_content_type =~ m@text/@)
550 {
551 my $part_text= $self->text_from_part($message_part);
552 if ($part_content_type !~ m@text/(ht|x)ml@) {
553 $part_text=text_into_html($part_text);
554 }
555 if ($part_text eq "") {
556 $part_text='&lt;&lt;empty message&gt;&gt;';
557 }
558 $text.=$part_text;
559 } elsif ($part_content_type =~ m@message/rfc822@) {
560 # This is a forwarded message
561 my $message_part_headers=$part_body;
562 $message_part_headers =~ s/\r?\n[\t\ ]+/ /gs; #unfold
563 $message_part_headers=~s/\r?\n\r?\n(.*)$//s;
564 my $message_part_body=$1;
565
566 my $rfc822_formatted_body=""; # put result in here
567 if ($message_part_headers =~
568 /^content\-type:\s*([\w\/\-]+)\s*\;?\s*(.*?)\s*$/ims)
569 {
570 # The message header uses MIME flags
571 my $message_content_type=$1;
572 my $message_content_info=$2;
573 if (!defined($message_content_info)) {
574 $message_content_info="";
575 }
576 $message_content_type =~ tr/A-Z/a-z/;
577 if ($message_content_type =~ /multipart/) {
578 $rfc822_formatted_body=
579 $self->text_from_mime_message($message_content_type,
580 $message_content_info,
581 $message_part_body,
582 $outhandle);
583 } else {
584 $message_part_body= $self->text_from_part($part_body);
585 $rfc822_formatted_body=text_into_html($message_part_body);
586 }
587 } else {
588 # message doesn't use MIME flags
589 $rfc822_formatted_body=text_into_html($message_part_body);
590 }
591 # Add the returned text to the output
592 # don't put all the headers...
593 $message_part_headers =~ s/^(X\-.*|received|message\-id|return\-path):.*\n//img;
594 $text.=text_into_html($message_part_headers);
595 $text.="<p>\n";
596 $text.=$rfc822_formatted_body;
597 # end of message/rfc822
598 } elsif ($part_content_type =~ /multipart/) {
599 # recurse again
600
601 $tmptext= $self->text_from_mime_message($part_content_type,
602 $part_content_info,
603 $part_body,
604 $outhandle);
605 $text.=$tmptext;
606 } elsif ($text eq "") {
607 # we can't do anything with this part, but if it's the first
608 # part then make sure it is mentioned..
609
610 $text.="\n<p><hr><strong>&lt;&lt;attachment&gt;&gt;</>";
611 # add part info header
612 $text.="<br>Type: $part_content_type<br>\n";
613 if ($filename ne "") {
614 $text.="Filename: $filename\n";
615 }
616 $text.="</strong></p>\n";
617 }
618 } # foreach message part.
619 } else {
620 # we can't handle this multipart type (not mixed or alternative)
621 # the RFC also mentions "parallel".
622 }
623 } # end of ($mimetype !~ multipart)
624 elsif ($mimetype =~ m@message/rfc822@) {
625 my $msg_header = $text;
626 $msg_header =~ s/\r?\n\r?\n(.*)$//s;
627 $text = $1;
628
629 if ($msg_header =~ /^content\-type:\s*([\w\/\-]+)\s*\;?\s*(.+?)\s*$/mi)
630 {
631 $mimetype=$1;
632 $mimetype =~ tr/[A-Z]/[a-z]/;
633 $mimeinfo=$2;
634 if ($mimeinfo =~ /charset=\"([^\"]+)\"/) {
635 $charset = $1;
636 }
637 my $msg_text;
638 if ($mimetype =~ m@multipart/@) {
639 $msg_text = text_from_mime_message($self, $mimetype, $mimeinfo,
640 $text, $outhandle);
641 } else {$msg_text=$text;}
642
643 my $brief_header="";
644 if ($msg_header =~ /^(From:.*)$/im) {$brief_header.="$1<br>";}
645 if ($msg_header =~ /^(To:.*)$/im) {$brief_header.="$1<br>";}
646 if ($msg_header =~ /^(Cc:.*)$/im) {$brief_header.="$1<br>";}
647 if ($msg_header =~ /^(Subject:.*)$/im) {$brief_header.="$1<br>";}
648 if ($msg_header =~ /^(Date:.*)$/im) {$brief_header.="$1<br>";}
649 $text= "\n<b>&lt;&lt;attached message&gt;&gt;</b><br>";
650 $text.= "<table><tr><td width=\"5%\"> </td>\n";
651 $text.="<td>" . $brief_header . "\n</p>" . $msg_text
652 . "</td></tr></table>";
653 }
654 } else {
655 # we don't do any processing of the content.
656 }
657
658 return $text;
659}
660
661
662
663
664
665
666# Process a MIME part. Return "" if we can't decode it.
667sub text_from_part {
668 my $self = shift;
669 my $text = shift || '';
670 my $part_header = $text;
671
672 # check for empty part header (leading blank line)
673 if ($text =~ /^\s*\r?\n/) {
674 $part_header="Content-type: text/plain; charset=us-ascii";
675 } else {
676 $part_header =~ s/\r?\n\r?\n(.*)$//s;
677 $text=$1; if (!defined($text)) {$text="";}
678 }
679 $part_header =~ s/\r?\n[\t ]+/ /gs; #unfold
680 $part_header =~ /content\-type:\s*([\w\/]+).*?charset=\"?([^\;\"\s]+)\"?/is;
681 my $type=$1;
682 my $charset=$2;
683 if (!defined($type)) {$type="";}
684 if (!defined($charset)) {$charset="ascii";}
685 my $encoding="";
686 if ($part_header =~ /^content\-transfer\-encoding:\s*([^\s]+)/mis) {
687 $encoding=$1; $encoding=~tr/A-Z/a-z/;
688 }
689 # Content-Transfer-Encoding is per-part
690 if ($encoding ne "") {
691 if ($encoding =~ /quoted\-printable/) {
692 $text=qp_decode($text);
693 } elsif ($encoding =~ /base64/) {
694 $text=base64_decode($text);
695 } elsif ($encoding !~ /[78]bit/) { # leave 7/8 bit as is.
696 # rfc2045 also allows binary, which we ignore (for now).
697 # maybe this shouldn't go to stderr, but anyway...
698 print STDERR "EMAILPlug: unknown encoding: $encoding\n";
699 return "";
700 }
701 }
702 if ($type eq "text/html") {
703 # only get stuff between <body> tags, or <html> tags.
704 $text =~ s@^.*<html[^>]*>@@is;
705 $text =~ s@</html>.*$@@is;
706 $text =~ s/^.*?<body[^>]*>//si;
707 $text =~ s/<\/body>.*$//si;
708 }
709 elsif ($type eq "text/xml") {
710 $text=~s/</&lt;/g;$text=~s/>/&gt;/g;
711 $text="<pre>\n$text\n</pre>\n";
712 }
713 # convert to unicode
714 $self->convert2unicode($charset, \$text);
715 return $text;
716}
717
718
719# decode quoted-printable text
720sub qp_decode {
721 my $text=shift;
722
723 my @lines=split('\n', $text);
724
725 # if a line ends with "=\s*", it is a soft line break, otherwise
726 # keep in any newline characters.
727 foreach my $line (@lines) {
728 if ($line !~ s/=\s*$//) {$line.="\n";}
729
730 if ($line =~ /=[0-9A-Fa-f]{2}/) { # it contains an escaped char
731 my @hexcode_segments=split('=',$line);
732 shift @hexcode_segments;
733 my @hexcodes;
734 foreach my $hexcode (@hexcode_segments) {
735 $hexcode =~ s/^(..).*$/$1/; # only need first 2 chars
736 chomp($hexcode); # just in case...
737 my $char=chr (hex "0x$hexcode");
738 $line =~ s/=$hexcode/$char/g;
739 }
740 }
741 }
742 $text= join('', @lines);
743 return $text;
744}
745
746# decode base64 text. This is fairly slow (since it's interpreted perl rather
747# than compiled XS stuff like in the ::MIME modules, but this is more portable
748# for us at least).
749# see rfc2045 for description, but basically, bits 7 and 8 are set to zero;
750# 4 bytes of encoded text become 3 bytes of binary - remove 2 highest bits
751# from each encoded byte.
752
753
754sub base64_decode {
755 my $enc_text = shift;
756# A=>0, B=>1, ..., '+'=>62, '/'=>63
757# also '=' is used for padding at the end, but we remove it anyway.
758 my $mimechars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
759# map each MIME char into it's value, for more efficient lookup.
760 my %index;
761 map { $index{$_} = index ($mimechars, $_) } (split ('', $mimechars));
762# remove all non-base64 chars. eval to get variable in transliteration...
763# also remove '=' - we'll assume (!!) that there are no errors in the encoding
764 eval "\$enc_text =~ tr|$mimechars||cd";
765 my $decoded="";
766 while (length ($enc_text)>3)
767 {
768 my $fourchars=substr($enc_text,0,4,"");
769 my @chars=(split '',$fourchars);
770 $decoded.=chr( $index{$chars[0]} << 2 | $index{$chars[1]} >> 4);
771 $decoded.=chr( ($index{$chars[1]} & 15) << 4 | $index{$chars[2]} >> 2);
772 $decoded.=chr( ($index{$chars[2]} & 3 ) << 6 | $index{$chars[3]});
773 }
774# if there are any input chars left, there are either
775# 2 encoded bytes (-> 1 raw byte) left or 3 encoded (-> 2 raw) bytes left.
776 my @chars=(split '',$enc_text);
777 if (length($enc_text)) {
778 $decoded.=chr($index{$chars[0]} << 2 | (int $index{$chars[1]} >> 4));
779 }
780 if (length($enc_text)==3) {
781 $decoded.=chr( ($index{$chars[1]} & 15) << 4 | $index{$chars[2]} >> 2);
782 }
783 return $decoded;
784}
785
786sub convert2unicode {
787 my $self = shift(@_);
788 my ($charset, $textref) = @_;
789
790 # first get our character encoding name in the right form.
791 $charset = "iso_8859_1" unless defined $charset;
792 $charset=~tr/A-Z/a-z/;
793 $charset=~s/\-/_/g;
794 $charset=~s/gb2312/gb/;
795 # assumes EUC-KR, not ISO-2022 !?
796 $charset=~s/ks_c_5601_1987/korean/;
797
798 if ($charset eq "utf_8") {
799 # nothing to do!
800 return;
801 }
802
803 # It appears that we can't always trust ascii text so we'll treat it
804 # as iso-8859-1 (letting characters above 0x80 through without
805 # converting them to utf-8 will result in invalid XML documents
806 # which can't be parsed at build time).
807 $charset = "iso_8859_1" if ($charset eq "us_ascii" || $charset eq "ascii");
808
809 if ($charset eq "iso_8859_1") {
810 # test if the mailer lied, and it has win1252 chars in it...
811 # 1252 has characters between 0x80 and 0x9f, 8859-1 doesn't
812 if ($$textref =~ m/[\x80-\x9f]/) {
813 my $outhandle = $self->{'outhandle'};
814 print $outhandle "EMAILPlug: Headers claim ISO charset but MS ";
815 print $outhandle "codepage 1252 detected.\n";
816 $charset = "windows_1252";
817 }
818 }
819 $$textref=&unicode::unicode2utf8(&unicode::convert2unicode($charset,$textref));
820}
821
822
823# Perl packages have to return true if they are run.
8241;
Note: See TracBrowser for help on using the repository browser.