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

Last change on this file since 7901 was 7830, checked in by jrm21, 20 years ago

change a couple of error messages to using gsprintf translated strings instead.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 34.2 KB
RevLine 
[638]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#
[3630]9# Copyright (C) 1999-2002 New Zealand Digital Library Project
[638]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
[1206]28
29# EMAILPlug
[638]30#
[1206]31# by Gordon Paynter ([email protected])
[638]32#
[1206]33# Email plug reads email files. These are named with a simple
[2096]34# number (i.e. as they appear in maildir folders) or with the
35# extension .mbx (for mbox mail file format)
[638]36#
37# Document text:
[1206]38# The document text consists of all the text
39# after the first blank line in the document.
[638]40#
[2730]41# Metadata (not Dublin Core!):
[6916]42# $Headers All the header content (optional, not stored by default)
[638]43# $Subject Subject: header
44# $To To: header
[2730]45# $From From: header
[2630]46# $FromName Name of sender (where available)
47# $FromAddr E-mail address of sender
[638]48# $DateText Date: header
49# $Date Date: header in GSDL format (eg: 19990924)
[2630]50#
[2918]51# $Title made up of Subject, Date and Sender (for default formatting)
[2630]52#
[2918]53#
[2630]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)
[2730]61# * RFC 2047 - MIME (part 3) Message Header Extensions
[2630]62# * RFC 1806 - Content Dispositions (ie inline/attachment)
[3540]63
64# 12/05/02 Added usage datastructure - John Thompson
65
[7830]66use strict;
67no strict "refs"; # so we can use a variable as a filehandle for print $out
68
[638]69package EMAILPlug;
70
[1895]71use SplitPlug;
[7830]72# EMAILPlug is a sub-class of SplitPlug.
73@EMAILPlug::ISA = ('SplitPlug');
[1895]74
[7830]75use unicode; # gs conv functions
76use gsprintf 'gsprintf'; # translations
[2730]77
[638]78use sorttools;
79use util;
80
81
82
[3540]83my $arguments =
[4744]84 [ { 'name' => "process_exp",
[4873]85 'desc' => "{BasPlug.process_exp}",
[6408]86 'type' => "regexp",
[3540]87 'reqd' => "no",
[4744]88 'deft' => &get_default_process_exp() },
89 { 'name' => "no_attachments",
[4873]90 'desc' => "{EMAILPlug.no_attachments}",
[3630]91 'type' => "flag",
[4744]92 'reqd' => "no" },
[6916]93 { 'name' => "headers",
94 'desc' => "{EMAILPlug.headers}",
95 'type' => "flag",
96 'reqd' => "no" },
[4744]97 { 'name' => "split_exp",
[4873]98 'desc' => "{EMAILPlug.split_exp}",
[6408]99 'type' => "regexp",
100 'reqd' => "no",
101 'deft' => &get_default_split_exp() }
102 ];
[3540]103
[4744]104my $options = { 'name' => "EMAILPlug",
[5680]105 'desc' => "{EMAILPlug.desc}",
[6408]106 'abstract' => "no",
107 'inherits' => "yes",
[4744]108 'args' => $arguments };
[3540]109
[638]110# Create a new EMAILPlug object with which to parse a file.
[1206]111# Accomplished by creating a new BasPlug and using bless to
[638]112# turn it into an EMAILPlug.
113
114sub new {
115 my ($class) = @_;
[3523]116 my $self = new BasPlug ($class, @_);
[5924]117 $self->{'plugin_type'} = "EMAILPlug";
[3540]118 # 14-05-02 To allow for proper inheritance of arguments - John Thompson
119 my $option_list = $self->{'option_list'};
120 push( @{$option_list}, $options );
121
[3523]122 if (!parsargv::parse(\@_,
123 q^split_exp/.*/^, \$self->{'split_exp'},
[3630]124 q^no_attachments^, \$self->{'ignore_attachments'},
[6916]125 q^headers^, \$self->{'header_metadata'},
[3523]126 "allow_extra_options")) {
127 print STDERR "\nIncorrect options passed to $class.";
128 print STDERR "\nCheck your collect.cfg configuration file\n";
[4873]129 $self->print_txt_usage(""); # Use default resource bundle
[3523]130 die "\n";
131 }
[3630]132 $self->{'assoc_filenames'} = {}; # to save attach names so we don't clobber
133
[2730]134 # this might not actually be true at read-time, but after processing
135 # it should all be utf8.
136 $self->{'input_encoding'}="utf8";
[638]137 return bless $self, $class;
138}
139
[1244]140sub get_default_process_exp {
141 my $self = shift (@_);
[2096]142 # mbx/email for mailbox file format, \d+ for maildir (each message is
143 # in a separate file, with a unique number for filename)
[3111]144 # mozilla and IE will save individual mbx format files with a ".eml" ext.
145 return q@([\\/]\d+|\.(mbx|email|eml))$@;
[638]146}
147
[1895]148# This plugin splits the mbox mail files at lines starting with From<sp>
[3111]149# It is supposed to be "\n\nFrom ", but this isn't always used.
[1895]150sub get_default_split_exp {
[2096]151 return q^\nFrom .*\n^;
[3523]152
[1895]153}
154
155
[1244]156# do plugin specific processing of doc_obj
157sub process {
[2630]158
[638]159 my $self = shift (@_);
[6332]160 my ($textref, $pluginfo, $base_dir, $file, $metadata, $doc_obj, $gli) = @_;
[1424]161 my $outhandle = $self->{'outhandle'};
162
[1244]163 # Check that we're dealing with a valid mail file
[3111]164 # mbox message files start with "From "
165 # maildir messages usually start with Return-Path and Delivered-To
166 # mh is very similar to maildir
167 my $startoffile=substr($$textref,0,256);
168 if (($startoffile !~ /^(From )/) &&
169 ($startoffile !~ /^(From|To|Envelope.*|Received|Return-Path|Date|Subject|Content\-.*|MIME-Version|Forwarded):/im)) {
170 return undef;
171 }
[638]172
[1757]173
[6332]174 print STDERR "<Processing n='$file' p='EMAILPlug'>\n" if ($gli);
175
[7830]176 gsprintf($outhandle, "EMAILPlug: {common.processing} $file\n")
[1244]177 if $self->{'verbosity'} > 1;
[1206]178
[1244]179 my $cursection = $doc_obj->get_top_section();
[638]180
[1206]181 #
182 # Parse the document's text and extract metadata
183 #
[638]184
[2652]185 # Protect backslashes
186 $$textref =~ s@\\@\\\\@g;
187
[1206]188 # Separate header from body of message
[1244]189 my $Headers = $$textref;
[2630]190 $Headers =~ s/\r?\n\r?\n(.*)$//s;
191 $$textref = $1;
[4089]192 $Headers .= "\n";
[2779]193
[2630]194 # Unfold headers - see rfc822
195 $Headers =~ s/\r?\n[\t\ ]+/ /gs;
[1206]196 # Extract basic metadata from header
197 my @headers = ("From", "To", "Subject", "Date");
198 my %raw;
[1658]199 foreach my $name (@headers) {
200 $raw{$name} = "No $name value";
201 }
[1206]202
[3132]203 # Get a default encoding for the header - RFC says should be ascii...
[6062]204 my $default_header_encoding="iso_8859_1";
[3132]205
206 # We don't know what character set is the user's default...
207 # We could use textcat to guess... for now we'll look at mime content-type
208# if ($Headers =~ /([[:^ascii:]])/) {
209# }
210 if ($Headers =~ /^Content\-type:.*charset=\"?([a-z0-9\-_]+)/mi) {
211 $default_header_encoding=$1;
212 $default_header_encoding =~ s@\-@_@g;
213 $default_header_encoding =~ tr/A-Z/a-z/;
214 }
215
216
[1658]217 # Examine each line of the headers
218 my ($line, $name, $value);
219 my @parts;
220 foreach $line (split(/\n/, $Headers)) {
221
222 # Ignore lines with no content or which begin with whitespace
223 next unless ($line =~ /:/);
224 next if ($line =~ /^\s/);
225
226 # Find out what metadata is on this line
227 @parts = split(/:/, $line);
228 $name = shift @parts;
[3136]229 # get fieldname in canonical form - first cap, then lower case.
[3134]230 $name =~ tr/A-Z/a-z/;
[3136]231 # uppercase the first character according to the current locale
[2630]232 $name=~s/(.+)/\u$1/;
[1658]233 next unless $name;
234 next unless ($raw{$name});
235
236 # Find the value of that metadata
237 $value = join(":", @parts);
238 $value =~ s/^\s+//;
239 $value =~ s/\s+$//;
[2730]240 # decode headers if stored using =?<charset>?[BQ]?<data>?= (rfc2047)
[3132]241 if ($value =~ /=\?.*\?[BbQq]\?.*\?=/) {
[2730]242 my $original_value=$value;
243 my $encoded=$value;
244 $value="";
[3215]245 # we should ignore spaces between consecutive encoded-texts
246 $encoded =~ s@\?=\s+=\?@\?==\?@g;
247 while ($encoded =~ s/(.*?)=\?([^\?]*)\?([bq])\?([^\?]+)\?=//i) {
[2730]248 my ($charset, $encoding, $data)=($2,$3,$4);
[2847]249 my ($decoded_data);
[2730]250 $value.="$1"; # any leading chars
251 $data=~s/^\s*//; $data=~s/\s*$//; # strip whitespace from ends
252 chomp $data;
253 $encoding =~ tr/BQ/bq/;
254 if ($encoding eq "q") { # quoted printable
[2733]255 $data =~ s/_/\ /g; # from rfc2047 (sec 4.2.2)
[2730]256 $decoded_data=qp_decode($data);
[3215]257 # qp_decode adds \n, which is default for body text
258 chomp($decoded_data);
[2730]259 } else { # base 64
260 $decoded_data=base64_decode($data);
261 }
[2847]262 $self->convert2unicode($charset, \$decoded_data);
263 $value .= $decoded_data;
264 } # end of while loop
[3132]265
[2847]266 # get any trailing characters
[3132]267 $self->convert2unicode($default_header_encoding, \$encoded);
[2847]268 $value.=$encoded;
269
[2730]270 if ($value =~ /^\s*$/) { # we couldn't extract anything...
[3132]271 $self->convert2unicode($default_header_encoding,
272 \$original_value);
[6062]273 $value=$original_value;
[2730]274 }
[3136]275 } # end of if =?...?=
276
277 # In the absense of other charset information, assume the
[6062]278 # header is the default (usually "iso_8859_1") and convert to unicode.
[3136]279 else {
[3132]280 $self->convert2unicode($default_header_encoding, \$value);
281 }
[2730]282
[1658]283 # Store the metadata
[6062]284 $value =~ s@_@\\_@g; # protect against GS macro language
[1206]285 $raw{$name} = $value;
286 }
287
[2630]288 # Extract the name and e-mail address from the From metadata
[6062]289 my $frommeta = $raw{"From"};
[2680]290 my $fromnamemeta;
291 my $fromaddrmeta;
292
293 $frommeta =~ s/\s*$//; # Remove trailing space, if any
294
295 if ($frommeta =~ m/(.+)\s*<(.+)>/) {
296 $fromnamemeta=$1;
297 $fromaddrmeta=$2;
298 } elsif ($frommeta =~ m/(.+@.+)\s+\((.*)\)/) {
299 $fromnamemeta=$2;
300 $fromaddrmeta=$1;
301 }
[2630]302 if (!defined($fromaddrmeta)) {
303 $fromaddrmeta=$frommeta;
304 }
[2680]305 $fromaddrmeta=~s/<//; $fromaddrmeta=~s/>//;
[2717]306 # minor attempt to prevent spam-bots from harvesting addresses...
307 $fromaddrmeta=~s/@/&#64;/;
[6062]308
[2630]309 $doc_obj->add_utf8_metadata ($cursection, "FromAddr", $fromaddrmeta);
310
[3328]311 if (defined($fromnamemeta) && $fromnamemeta) { # must be > 0 long
[3215]312 $fromnamemeta =~ s/\"//g; # remove quotes
313 $fromnamemeta =~ s/\s+$//; # remove trailing whitespace
[2630]314 }
315 else {
316 $fromnamemeta = $fromaddrmeta;
317 }
318 # if name is an address
319 $fromnamemeta =~ s/<//g; $fromnamemeta =~ s/>//g;
[2717]320 $fromnamemeta=~s/@/&#64\;/;
[2630]321 $doc_obj->add_utf8_metadata ($cursection, "FromName", $fromnamemeta);
322
323 $raw{"From"}=$frommeta;
324
[1206]325 # Process Date information
[1658]326 if ($raw{"Date"} !~ /No Date/) {
[1206]327 $raw{"DateText"} = $raw{"Date"};
[638]328
[1206]329 # Convert the date text to internal date format
330 $value = $raw{"Date"};
331 my ($day, $month, $year) = $value =~ /(\d?\d)\s([A-Z][a-z][a-z])\s(\d\d\d?\d?)/;
[3143]332 # make some assumptions about the year formatting...
333 # some (old) software thinks 2001 is 101, some think 2001 is 01
334 if ($year < 20) { $year += 2000; } # assume not really 1920...
335 elsif ($year < 150) { $year += 1900; } # assume not really 2150...
[1206]336 $raw{"Date"} = &sorttools::format_date($day, $month, $year);
[638]337
[1206]338 } else {
339 # We have not extracted a date
340 $raw{"DateText"} = "Unknown.";
341 $raw{"Date"} = "19000000";
[638]342 }
343
[1244]344 # Add extracted metadata to document object
[1206]345 foreach my $name (keys %raw) {
346 $value = $raw{$name};
347 if ($value) {
[2730]348 # assume subject, etc headers have no special HTML meaning.
[1206]349 $value = &text_into_html($value);
[2730]350 # escape [] so it isn't re-interpreted as metadata
351 $value =~ s/\[/&#91;/g; $value =~ s/\]/&#93;/g;
[1206]352 } else {
353 $value = "No $name field";
354 }
[1244]355 $doc_obj->add_utf8_metadata ($cursection, $name, $value);
[1206]356 }
[638]357
[7703]358
359 # extract a message ID from the headers, if there is one, and we'll use
360 # that as the greenstone doc ID. Having a predictable ID means we can
361 # link to other messages, eg from In-Reply-To or References headers...
362 if ($Headers =~ m@^Message-ID:(.+)$@mi) {
363 my $id=escape_msg_id($1);
364 $doc_obj->{'msgid'}=$id;
365 }
366 # link to another message, if this is a reply
367 if ($Headers =~ m@^In-Reply-To:(.+)$@mi) {
368 my $id=escape_msg_id($1);
369 $doc_obj->add_utf8_metadata ($cursection, 'InReplyTo', $id);
370 } elsif ($Headers =~ m@^References:.*\s([^\s]+)$@mi) {
371 # References can have multiple, get the last one
372 my $id=escape_msg_id($1);
373 # not necessarily in-reply-to, but same thread...
374 $doc_obj->add_utf8_metadata ($cursection, 'InReplyTo', $id);
375 }
376
377
378
[2630]379 my $mimetype="text/plain";
380 my $mimeinfo="";
[3132]381 my $charset = $default_header_encoding;
[3073]382 # Do MIME and encoding stuff. Allow \s in mimeinfo in case there is
383 # more than one parameter given to Content-type.
384 # eg: Content-type: text/plain; charset="us-ascii"; format="flowed"
[3630]385 if ($Headers =~ m@^content\-type:\s*([\w\.\-/]+)\s*(\;\s*.+)?\s*$@mi)
[2630]386 {
387 $mimetype=$1;
388 $mimetype =~ tr/[A-Z]/[a-z]/;
[3073]389
390 if ($mimetype eq "text") { # for pre-RFC2045 messages (c. 1996)
391 $mimetype = "text/plain";
392 }
393
[2630]394 $mimeinfo=$2;
[3073]395 if (!defined $mimeinfo) {
396 $mimeinfo="";
397 } else { # strip leading and trailing stuff
398 $mimeinfo =~ s/^\;\s*//;
399 $mimeinfo =~ s/\s*$//;
400 }
401 if ($mimeinfo =~ /charset=\"([^\"]+)\"/i) {
[2847]402 $charset = $1;
403 }
[2630]404 }
405
[2680]406 my $transfer_encoding="7bit";
407 if ($Headers =~ /^content-transfer-encoding:\s*([^\s]+)\s*$/mi) {
408 $transfer_encoding=$1;
409 }
[6062]410
[2886]411 if ($mimetype eq "text/html") {
412 $$textref= $self->text_from_part("$Headers\n$$textref");
413 } elsif ($mimetype ne "text/plain") {
[3630]414 $self->{'doc_obj'} = $doc_obj; # in case we need to associate files...
[6062]415 $$textref=$self->text_from_mime_message($mimetype,$mimeinfo,$$textref);
416 } else { # mimetype eq text/plain
417 $$textref = &text_into_html($$textref);
418 $$textref =~ s@_@\\_@g; # protect against GS macro language
419
420 if ($transfer_encoding =~ /quoted\-printable/) {
421 $$textref=qp_decode($$textref);
422 } elsif ($transfer_encoding =~ /base64/) {
423 $$textref=base64_decode($$textref);
424 }
[2886]425 $self->convert2unicode($charset, $textref);
[2680]426 }
[6062]427
[2630]428
[6916]429 if ($self->{'header_metadata'} && $self->{'header_metadata'} == 1) {
430 # Add "All headers" metadata
431 $Headers = &text_into_html($Headers);
[2630]432
[6916]433 $Headers = "No headers" unless ($Headers =~ /\w/);
434 $Headers =~ s/@/&#64\;/g;
435 # escape [] so it isn't re-interpreted as metadata
436 $Headers =~ s/\[/&#91;/g; $Headers =~ s/\]/&#93;/g;
437 $self->convert2unicode($charset, \$Headers);
[2754]438
[6916]439 $Headers =~ s@_@\\_@g; # protect against GS macro language
440 $doc_obj->add_utf8_metadata ($cursection, "Headers", $Headers);
441 }
[6062]442
443
[2918]444 # Add Title metadata
445 my $Title = text_into_html($raw{'Subject'});
[7703]446 $Title .= "<br>From: " . text_into_html($fromnamemeta);
[2918]447 $Title .= "<br>Date: " . text_into_html($raw{'DateText'});
[3073]448 $Title =~ s/\[/&#91;/g; $Title =~ s/\]/&#93;/g;
[2918]449
450 $doc_obj->add_utf8_metadata ($cursection, "Title", $Title);
451
452
[1244]453 # Add text to document object
454 $$textref = "No message" unless ($$textref =~ /\w/);
[6062]455
[1244]456 $doc_obj->add_utf8_text($cursection, $$textref);
[638]457
[1244]458 return 1;
[638]459}
460
461
462# Convert a text string into HTML.
463#
464# The HTML is going to be inserted into a GML file, so
465# we have to be careful not to use symbols like ">",
466# which ocurs frequently in email messages (and use
467# &gt instead.
468#
469# This function also turns links and email addresses into hyperlinks,
470# and replaces carriage returns with <BR> tags (and multiple carriage
471# returns with <P> tags).
472
[1206]473
[638]474sub text_into_html {
475 my ($text) = @_;
476
[1244]477 # Convert problem characters into HTML symbols
[3132]478 $text =~ s/&/&amp;/g;
479 $text =~ s/</&lt;/g;
480 $text =~ s/>/&gt;/g;
481 $text =~ s/\"/&quot;/g;
[638]482
[2630]483 # convert email addresses and URIs into links
484# don't markup email addresses for now
485# $text =~ s/([\w\d\.\-]+@[\w\d\.\-]+)/<a href=\"mailto:$1\">$1<\/a>/g;
[638]486
[2918]487 # try to munge email addresses a little bit...
488 $text =~ s/@/&#64;/;
[2730]489 # assume hostnames are \.\w\- only, then might have a trailing '/.*'
490 # assume URI doesn't finish with a '.'
[3398]491 $text =~ s@((http|ftp|https)://[\w\-]+(\.[\w\-]+)*/?((&amp;|\.)?[\w\?\=\-_/~]+)*(\#[\w\.\-_]*)?)@<a href=\"$1\">$1<\/a>@g;
[2630]492
493
[638]494 # Clean up whitespace and convert \n charaters to <BR> or <P>
[3132]495 $text =~ s/ +/ /g;
496 $text =~ s/\s*$//g;
497 $text =~ s/^\s*//g;
498 $text =~ s/\n/\n<br>/g;
499 $text =~ s/<br>\s*<br>/<p>/gi;
[638]500
501 return $text;
502}
503
504
[2630]505
506
507#Process a MIME message.
508# the textref we are given DOES NOT include the header.
509sub text_from_mime_message {
[2847]510 my $self = shift(@_);
[6062]511 my ($mimetype,$mimeinfo,$text)=(@_);
512 my $outhandle=$self->{'outhandle'};
[2630]513 # Check for multiparts - $mimeinfo will be a boundary
514 if ($mimetype =~ /multipart/) {
[6062]515 my $boundary="";
[2732]516 if ($mimeinfo =~ m@boundary=(\"[^\"]+\"|[^\s]+)\s*$@im) {
[2630]517 $boundary=$1;
[2732]518 if ($boundary =~ m@^\"@) {
519 $boundary =~ s@^\"@@; $boundary =~ s@\"$@@;
520 }
521 } else {
522 print $outhandle "EMAILPlug: (warning) couldn't parse MIME boundary\n";
[2630]523 }
524 # parts start with "--$boundary"
525 # message ends with "--$boundary--"
526 # RFC says boundary is <70 chars, [A-Za-z'()+_,-./:=?], so escape any
[2680]527 # that perl might want to interpolate. Also allows spaces...
[2630]528 $boundary=~s/\\/\\\\/g;
529 $boundary=~s/([\?\+\.\(\)\:\/\'])/\\$1/g;
[2681]530 my @message_parts = split("\r?\n\-\-$boundary", "\n$text");
[2630]531 # remove first "part" and last "part" (final --)
532 shift @message_parts;
533 my $last=pop @message_parts;
[2680]534 # if our boundaries are a bit dodgy and we only found 1 part...
535 if (!defined($last)) {$last="";}
[2630]536 # make sure it is only -- and whitespace
537 if ($last !~ /^\-\-\s*$/ms) {
538 print $outhandle "EMAILPlug: (warning) last part of MIME message isn't empty\n";
539 }
540 foreach my $message_part (@message_parts) {
541 # remove the leading newline left from split.
542 $message_part=~s/^\r?\n//;
543 }
544 if ($mimetype eq "multipart/alternative") {
545 # check for an HTML version first, then TEXT, otherwise use first.
546 my $part_text="";
547 foreach my $message_part (@message_parts) {
548 if ($message_part =~ m@\s*content\-type:\s*text/html@mis)
549 {
550 # Use the HTML version
[2847]551 $part_text= $self->text_from_part($message_part);
[2630]552 $mimetype="text/html";
553 last;
554 }
555 }
556 if ($part_text eq "") { # try getting a text part instead
557 foreach my $message_part (@message_parts) {
558 if ($message_part =~ m@^content\-type:\s*text/plain@mis)
559 {
560 # Use the plain version
[2847]561 $part_text= $self->text_from_part($message_part);
[2732]562 if ($part_text =~/[^\s]/) {
[3721]563 $part_text = text_into_html($part_text);
[2732]564 }
[2630]565 $mimetype="text/plain";
566 last;
567 }
568 }
569 }
[3721]570 if ($part_text eq "") { #use first part (no html/text part found)
571 $part_text = $self->text_from_part(shift @message_parts);
572 $part_text = text_into_html($part_text);
[2630]573 }
574 if ($part_text eq "") { # we couldn't get anything!!!
575 # or it was an empty message...
576 # do nothing...
[7830]577 gsprintf($outhandle, "{BasPlug.empty_file} - empty body?\n");
[2630]578 } else {
[3721]579 $text = $part_text;
[2630]580 }
[3352]581 } elsif ($mimetype =~ m@multipart/(mixed|digest|related|signed)@) {
[3721]582 $text = "";
[3352]583 # signed is for PGP/GPG messages... the last part is a hash
584 if ($mimetype =~ m@multipart/signed@) {
585 pop @message_parts;
586 }
[6062]587 my $is_first_part=1;
[2630]588 foreach my $message_part (@message_parts) {
[6062]589 if ($is_first_part && $text ne "") {$is_first_part=0;}
590
[2630]591 if ($mimetype eq "multipart/digest") {
[6062]592 # default type - RTFRFC!! Set if not already set
593 $message_part =~ m@^(.*)\n\r?\n@s;
594 my $part_header=$1;
595 if ($part_header !~ m@^content-type@mi) {
596 $message_part="Content-type: message/rfc822\n"
597 . $message_part; # prepend default type
[4224]598 }
[2630]599 }
600
[6062]601 $text .= $self->process_multipart_part($message_part,
602 $is_first_part);
[2630]603 } # foreach message part.
604 } else {
605 # we can't handle this multipart type (not mixed or alternative)
606 # the RFC also mentions "parallel".
607 }
[3627]608 } # end of ($mimetype =~ multipart)
[2918]609 elsif ($mimetype =~ m@message/rfc822@) {
610 my $msg_header = $text;
611 $msg_header =~ s/\r?\n\r?\n(.*)$//s;
612 $text = $1;
613
[3630]614 if ($msg_header =~ /^content\-type:\s*([\w\.\-\/]+)\s*\;?\s*(.+?)\s*$/mi)
[2918]615 {
616 $mimetype=$1;
617 $mimetype =~ tr/[A-Z]/[a-z]/;
618 $mimeinfo=$2;
[6062]619 #if ($mimeinfo =~ /charset=\"([^\"]+)\"/) {
620 # $charset = $1;
621 #}
[2918]622 my $msg_text;
623 if ($mimetype =~ m@multipart/@) {
624 $msg_text = text_from_mime_message($self, $mimetype, $mimeinfo,
[6062]625 $text);
626 } else {$msg_text=text_from_part($text);}
[2918]627
[3630]628 my $brief_header=text_into_html(get_brief_headers($msg_header));
[2918]629 $text= "\n<b>&lt;&lt;attached message&gt;&gt;</b><br>";
630 $text.= "<table><tr><td width=\"5%\"> </td>\n";
631 $text.="<td>" . $brief_header . "\n</p>" . $msg_text
632 . "</td></tr></table>";
633 }
634 } else {
[2886]635 # we don't do any processing of the content.
636 }
637
[2630]638 return $text;
639}
640
641
[6062]642
[7703]643# used for turning a message id into a more friendly string for greenstone
644sub escape_msg_id {
645#msgid
646 my $id=shift;
647 chomp $id; $id =~ s!\s!!g; # remove spaces
648 $id =~ s![<>\[\]]!!g; # remove [ ] < and >
649 $id =~ s![_&]!-!g; # replace symbols that might cause problems
650 $id =~ s!@!-!g; # replace @ symbol, to avoid spambots
651 return $id;
652}
[6062]653
[7703]654
655
[6062]656sub process_multipart_part {
657 my $self = shift;
658 my $message_part = shift;
659 my $is_first_part = shift;
660
661 my $return_text="";
662 my $part_header=$message_part;
663 my $part_body;
664 if ($message_part=~ /^\s*\n/) {
665 # no header... use defaults
666 $part_body=$message_part;
667 $part_header="Content-type: text/plain; charset=us-ascii";
668 } elsif ($part_header=~s/\r?\n\r?\n(.*)$//s) {
669 $part_body=$1;
670 } else {
671 # something's gone wrong...
672 $part_header="";
673 $part_body=$message_part;
674 }
675
676 $part_header =~ s/\r?\n[\t\ ]+/ /gs; #unfold
677 my $part_content_type="";
678 my $part_content_info="";
679
680 if ($part_header =~ m@^content\-type:\s*([\w\.\-/]+)\s*(\;.*)?$@mi) {
681 $part_content_type=$1; $part_content_type =~ tr/A-Z/a-z/;
682 $part_content_info=$2;
683 if (!defined($part_content_info)) {
684 $part_content_info="";
685 } else {
686 $part_content_info =~ s/^\;\s*//;
687 $part_content_info =~ s/\s*$//;
688 }
689 }
690 my $filename="";
[6079]691 if ($part_header =~ m@name=\"?([^\"\n]+)\"?@mis) {
[6062]692 $filename=$1;
[6079]693 $filename =~ s@\r?\s*$@@; # remove trailing space, if any
[6062]694 }
695
696 # disposition - either inline or attachment.
697 # NOT CURRENTLY USED - we display all text types instead...
698 # $part_header =~ /^content\-disposition:\s*([\w+])/mis;
699
700 # add <<attachment>> to each part except the first...
701 if (!$is_first_part) {
702 $return_text.="\n<p><hr><strong>&lt;&lt;attachment&gt;&gt;";
703 # add part info header
704 my $header_text="<br>Type: $part_content_type<br>\n";
705 if ($filename ne "") {
706 $header_text.="Filename: $filename\n";
707 }
708 $header_text =~ s@_@\\_@g;
709 $return_text.=$header_text . "</strong></p>\n<p>\n";
710 }
711
712 if ($part_content_type =~ m@text/@)
713 {
714 my $part_text= $self->text_from_part($message_part);
715 if ($part_content_type !~ m@text/(ht|x)ml@) {
716 $part_text = text_into_html($part_text);
717 }
718 if ($part_text eq "") {
719 $part_text = ' ';
720 }
721 $return_text .= $part_text;
722 } elsif ($part_content_type =~ m@message/rfc822@) {
723 # This is a forwarded message
724 my $message_part_headers=$part_body;
725 $message_part_headers=~s/\r?\n\r?\n(.*)$//s;
726 my $message_part_body=$1;
727 $message_part_headers =~ s/\r?\n[\t\ ]+/ /gs; #unfold
728
729 my $rfc822_formatted_body=""; # put result in here
730 if ($message_part_headers =~
731 /^content\-type:\s*([\w\.\-\/]+)\s*(\;.*)?$/ims)
732 {
733 # The message header uses MIME flags
734 my $message_content_type=$1;
735 my $message_content_info=$2;
736 if (!defined($message_content_info)) {
737 $message_content_info="";
738 } else {
739 $message_content_info =~ s/^\;\s*//;
740 $message_content_info =~ s/\s*$//;
741 }
742 $message_content_type =~ tr/A-Z/a-z/;
743 if ($message_content_type =~ /multipart/) {
744 $rfc822_formatted_body=
745 $self->text_from_mime_message($message_content_type,
746 $message_content_info,
747 $message_part_body);
748 } else {
749 $message_part_body= $self->text_from_part($part_body);
750 $rfc822_formatted_body=text_into_html($message_part_body);
751 }
752 } else {
753 # message doesn't use MIME flags
754 $rfc822_formatted_body=text_into_html($message_part_body);
755 $rfc822_formatted_body =~ s@_@\\_@g;
756 }
757 # Add the returned text to the output
758 # don't put all the headers...
759# $message_part_headers =~ s/^(X\-.*|received|message\-id|return\-path):.*\n//img;
760 my $brief_headers=get_brief_headers($message_part_headers);
761 $return_text.=text_into_html($brief_headers);
762 $return_text.="</p><p>\n";
763 $return_text.=$rfc822_formatted_body;
764 $return_text.="</p>\n";
765 # end of message/rfc822
766 } elsif ($part_content_type =~ /multipart/) {
767 # recurse again
768
769 my $tmptext= $self->text_from_mime_message($part_content_type,
770 $part_content_info,
771 $part_body);
772 $return_text.=$tmptext;
773 } else {
774 # this part isn't text/* or another message...
775 if ($is_first_part) {
776 # this is the first part of a multipart, or only part!
777 $return_text="\n<p><hr><strong>&lt;&lt;attachment&gt;&gt;";
778 # add part info header
779 my $header_text="<br>Type: $part_content_type<br>\n";
780 $header_text.="Filename: $filename</strong></p>\n<p>\n";
781 $header_text =~ s@_@\\_@g;
782 $return_text.=$header_text;
783 }
784
785 # save attachment by default
786 if (!$self->{'ignore_attachments'}
787 && $filename ne "") { # this part has a file...
788 my $encoding="8bit";
789 if ($part_header =~
790 /^content-transfer-encoding:\s*(\w+)/mi ) {
791 $encoding=$1; $encoding =~ tr/A-Z/a-z/;
792 }
793 my $tmpdir=$ENV{'GSDLHOME'} . "/tmp";
794 my $save_filename=$filename;
795
796 # make sure we don't clobber files with same name;
797 # need to keep state between .mbx files
798 my $assoc_files=$self->{'assoc_filenames'};
799 if ($assoc_files->{$filename}) { # it's been set...
800 $assoc_files->{$filename}++;
801 $filename =~ m/(.+)\.(\w+)$/;
802 my ($filestem, $ext)=($1,$2);
803 $save_filename="${filestem}_"
804 . $assoc_files->{$filename} . ".$ext";
805 } else { # first file with this name
806 $assoc_files->{$filename}=1;
807 }
808 open (SAVE, ">$tmpdir/$save_filename") ||
809 warn "EMAILPlug: Can't save attachment as $tmpdir/$save_filename: $!";
810 my $part_text = $message_part;
811 $part_text =~ s/(.*?)\r?\n\r?\n//s; # remove header
812 if ($encoding eq "base64") {
813 print SAVE base64_decode($part_text);
814 } elsif ($encoding eq "quoted-printable") {
815 print SAVE qp_decode($part_text);
816 } else { # 7bit, 8bit, binary, etc...
817 print SAVE $part_text;
818 }
819 close SAVE;
820 my $doc_obj=$self->{'doc_obj'};
821 $doc_obj->associate_file("$tmpdir/$save_filename",
822 "$save_filename",
823 $part_content_type # mimetype
824 );
825 # clean up tmp area...
826 # Can't do this as it hasn't been copied/linked yet!!!
827# &util::rm("$tmpdir/$save_filename");
828 my $outhandle=$self->{'outhandle'};
829 print $outhandle "EMAILPlug: saving attachment \"$filename\"\n"; #
830
831 # be nice if "download" was a translatable macro :(
832 $return_text .="<a href=\"_httpdocimg_/$save_filename\">download</a>";
833 } # end of save attachment
834 } # end of !text/message part
835
836
837 return $return_text;
838}
839
840
[3630]841# Return only the "important" headers from a set of message headers
842sub get_brief_headers {
843 my $msg_header = shift;
844 my $brief_header = "";
[2630]845
[3630]846 # Order matters!
847 if ($msg_header =~ /^(From:.*)$/im) {$brief_header.="$1\n";}
848 if ($msg_header =~ /^(To:.*)$/im) {$brief_header.="$1\n";}
849 if ($msg_header =~ /^(Cc:.*)$/im) {$brief_header.="$1\n";}
850 if ($msg_header =~ /^(Subject:.*)$/im) {$brief_header.="$1\n";}
851 if ($msg_header =~ /^(Date:.*)$/im) {$brief_header.="$1\n";}
[2630]852
[3630]853 return $brief_header;
854}
[2630]855
856
857# Process a MIME part. Return "" if we can't decode it.
[6062]858# should only be called for parts with type "text/*" ?
[2630]859sub text_from_part {
[3132]860 my $self = shift;
[3136]861 my $text = shift || '';
862 my $part_header = $text;
863
[2681]864 # check for empty part header (leading blank line)
865 if ($text =~ /^\s*\r?\n/) {
866 $part_header="Content-type: text/plain; charset=us-ascii";
867 } else {
868 $part_header =~ s/\r?\n\r?\n(.*)$//s;
869 $text=$1; if (!defined($text)) {$text="";}
870 }
[2630]871 $part_header =~ s/\r?\n[\t ]+/ /gs; #unfold
[3630]872 $part_header =~ /content\-type:\s*([\w\.\-\/]+).*?charset=\"?([^\;\"\s]+)\"?/is;
[2730]873 my $type=$1;
874 my $charset=$2;
875 if (!defined($type)) {$type="";}
876 if (!defined($charset)) {$charset="ascii";}
[2630]877 my $encoding="";
[2638]878 if ($part_header =~ /^content\-transfer\-encoding:\s*([^\s]+)/mis) {
[2630]879 $encoding=$1; $encoding=~tr/A-Z/a-z/;
880 }
881 # Content-Transfer-Encoding is per-part
882 if ($encoding ne "") {
883 if ($encoding =~ /quoted\-printable/) {
884 $text=qp_decode($text);
885 } elsif ($encoding =~ /base64/) {
886 $text=base64_decode($text);
887 } elsif ($encoding !~ /[78]bit/) { # leave 7/8 bit as is.
888 # rfc2045 also allows binary, which we ignore (for now).
[3630]889 my $outhandle=$self->{'outhandle'};
890 print $outhandle "EMAILPlug: unknown transfer encoding: $encoding\n";
[2630]891 return "";
892 }
893 }
894 if ($type eq "text/html") {
895 # only get stuff between <body> tags, or <html> tags.
[2730]896 $text =~ s@^.*<html[^>]*>@@is;
897 $text =~ s@</html>.*$@@is;
898 $text =~ s/^.*?<body[^>]*>//si;
899 $text =~ s/<\/body>.*$//si;
[2630]900 }
901 elsif ($type eq "text/xml") {
902 $text=~s/</&lt;/g;$text=~s/>/&gt;/g;
903 $text="<pre>\n$text\n</pre>\n";
904 }
[2730]905 # convert to unicode
[2847]906 $self->convert2unicode($charset, \$text);
[6062]907
908 $text =~ s@_@\\_@g; # protect against GS macro language
[2630]909 return $text;
910}
911
912
[6062]913
914
[2630]915# decode quoted-printable text
916sub qp_decode {
917 my $text=shift;
918
919 # if a line ends with "=\s*", it is a soft line break, otherwise
920 # keep in any newline characters.
921
[3627]922 $text =~ s/=\s*\r?\n//mg;
923 $text =~ s/=([0-9A-Fa-f]{2})/chr (hex "0x$1")/eg;
[2630]924 return $text;
925}
926
927# decode base64 text. This is fairly slow (since it's interpreted perl rather
928# than compiled XS stuff like in the ::MIME modules, but this is more portable
929# for us at least).
930# see rfc2045 for description, but basically, bits 7 and 8 are set to zero;
931# 4 bytes of encoded text become 3 bytes of binary - remove 2 highest bits
[2638]932# from each encoded byte.
[2630]933
934
935sub base64_decode {
936 my $enc_text = shift;
937# A=>0, B=>1, ..., '+'=>62, '/'=>63
938# also '=' is used for padding at the end, but we remove it anyway.
939 my $mimechars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
940# map each MIME char into it's value, for more efficient lookup.
941 my %index;
942 map { $index{$_} = index ($mimechars, $_) } (split ('', $mimechars));
943# remove all non-base64 chars. eval to get variable in transliteration...
944# also remove '=' - we'll assume (!!) that there are no errors in the encoding
945 eval "\$enc_text =~ tr|$mimechars||cd";
946 my $decoded="";
947 while (length ($enc_text)>3)
948 {
949 my $fourchars=substr($enc_text,0,4,"");
950 my @chars=(split '',$fourchars);
951 $decoded.=chr( $index{$chars[0]} << 2 | $index{$chars[1]} >> 4);
952 $decoded.=chr( ($index{$chars[1]} & 15) << 4 | $index{$chars[2]} >> 2);
953 $decoded.=chr( ($index{$chars[2]} & 3 ) << 6 | $index{$chars[3]});
954 }
955# if there are any input chars left, there are either
956# 2 encoded bytes (-> 1 raw byte) left or 3 encoded (-> 2 raw) bytes left.
957 my @chars=(split '',$enc_text);
958 if (length($enc_text)) {
959 $decoded.=chr($index{$chars[0]} << 2 | (int $index{$chars[1]} >> 4));
960 }
961 if (length($enc_text)==3) {
962 $decoded.=chr( ($index{$chars[1]} & 15) << 4 | $index{$chars[2]} >> 2);
963 }
964 return $decoded;
965}
966
[2847]967sub convert2unicode {
968 my $self = shift(@_);
969 my ($charset, $textref) = @_;
[2630]970
[7703]971 if (!$$textref) {
972 # nothing to do!
973 return;
974 }
975
[2847]976 # first get our character encoding name in the right form.
977 $charset = "iso_8859_1" unless defined $charset;
[7703]978 $charset =~ tr/A-Z/a-z/; # lowercase
979 $charset =~ s/\-/_/g;
980 $charset =~ s/gb2312/gb/;
[2847]981 # assumes EUC-KR, not ISO-2022 !?
[7703]982 $charset =~ s/^ks_c_5601_1987/korean/;
983 if ($charset eq 'utf_8') {$charset='utf8'}
[2847]984
[7703]985 my $outhandle = $self->{'outhandle'};
986
987 if ($charset eq "utf8") {
988 # no conversion needed, but lets check that it's valid utf8
989 # see utf-8 manpage for valid ranges
990 $$textref =~ m/^/g; # to set \G
991 my $badbytesfound=0;
992 while ($$textref =~ m!\G.*?([\x80-\xff]+)!sg) {
993 my $highbytes=$1;
994 my $highbyteslength=length($highbytes);
995 # replace any non utf8 complaint bytes
996 $highbytes =~ /^/g; # set pos()
997 while ($highbytes =~
998 m!\G (?: [\xc0-\xdf][\x80-\xbf] | # 2 byte utf-8
999 [\xe0-\xef][\x80-\xbf]{2} | # 3 byte
1000 [\xf0-\xf7][\x80-\xbf]{3} # 4 byte
1001 [\xf8-\xfb][\x80-\xbf]{4} # 5 byte
1002 [\xfc-\xfd][\x80-\xbf]{5} # 6 byte
1003 )*([\x80-\xff])? !xg
1004 ) {
1005 my $badbyte=$1;
1006 if (!defined $badbyte) {next} # hit end of string
1007 my $pos=pos($highbytes);
1008 substr($highbytes, $pos-1, 1, "\xc2\x80");
1009 # update the position to continue searching (for \G)
1010 pos($highbytes) = $pos+1; # set to just after the \x80
1011 $badbytesfound=1;
1012 }
1013 if ($badbytesfound==1) {
1014 # claims to be utf8, but it isn't!
1015 print $outhandle "EMAILPlug: Headers claim utf-8 but bad bytes "
1016 . "detected and removed.\n";
1017
1018 my $replength=length($highbytes);
1019 my $textpos=pos($$textref);
1020 # replace bad bytes with good bytes
1021 substr( $$textref, $textpos-$replength,
1022 $replength, $highbytes);
1023 # update the position to continue searching (for \G)
1024 pos($$textref)=$textpos+($replength-$highbyteslength);
1025 }
1026 }
[3206]1027 return;
1028 }
1029
[2847]1030 # It appears that we can't always trust ascii text so we'll treat it
1031 # as iso-8859-1 (letting characters above 0x80 through without
1032 # converting them to utf-8 will result in invalid XML documents
1033 # which can't be parsed at build time).
1034 $charset = "iso_8859_1" if ($charset eq "us_ascii" || $charset eq "ascii");
1035
[3073]1036 if ($charset eq "iso_8859_1") {
1037 # test if the mailer lied, and it has win1252 chars in it...
1038 # 1252 has characters between 0x80 and 0x9f, 8859-1 doesn't
1039 if ($$textref =~ m/[\x80-\x9f]/) {
1040 print $outhandle "EMAILPlug: Headers claim ISO charset but MS ";
1041 print $outhandle "codepage 1252 detected.\n";
1042 $charset = "windows_1252";
1043 }
1044 }
[3351]1045 my $utf8_text=&unicode::unicode2utf8(&unicode::convert2unicode($charset,$textref));
1046
1047 if ($utf8_text ne "") {
1048 $$textref=$utf8_text;
1049 } else {
1050 # we didn't get any text... unsupported encoding perhaps? Or it is
1051 # empty anyway. We'll try to continue, assuming 8859-1. We could strip
1052 # characters out here if this causes problems...
[3726]1053 my $outhandle=$self->{'outhandle'};
1054 print $outhandle "EMAILPlug: falling back to iso-8859-1\n";
[3351]1055 $$textref=&unicode::unicode2utf8(&unicode::convert2unicode("iso_8859_1",$textref));
1056
1057 }
[2847]1058}
1059
1060
[7703]1061sub set_OID {
1062 my $self = shift (@_);
1063 my ($doc_obj, $id, $segment_number) = @_;
1064
1065 if ( exists $doc_obj->{'msgid'} ) {
1066 $doc_obj->set_OID($doc_obj->{'msgid'});
1067 } else {
1068 $doc_obj->set_OID("$id\_$segment_number");
1069 }
1070}
1071
1072
[1206]1073# Perl packages have to return true if they are run.
10741;
Note: See TracBrowser for help on using the repository browser.