X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FEmailParser.pm;h=695b7449fbe5bf809c6ac501365261f6ab032e92;hp=e9a00f16c03c7dc0247bc9d2c71e7703162f187b;hb=919e930aa9279b3c5cd12b593889cd6de79d67bf;hpb=5fc8c5edf574ab024d4646914b6432d458e2ffbd diff --git a/rt/lib/RT/EmailParser.pm b/rt/lib/RT/EmailParser.pm index e9a00f16c..695b7449f 100644 --- a/rt/lib/RT/EmailParser.pm +++ b/rt/lib/RT/EmailParser.pm @@ -1,33 +1,61 @@ -# BEGIN LICENSE BLOCK -# -# Copyright (c) 1996-2003 Jesse Vincent -# -# (Except where explictly superceded by other copyright notices) -# +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# -# Unless otherwise specified, all modifications, corrections or -# extensions to this work which alter its source code become the -# property of Best Practical Solutions, LLC when submitted for -# inclusion in the work. -# -# -# END LICENSE BLOCK +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + package RT::EmailParser; use base qw/RT::Base/; use strict; -use Mail::Address; +use warnings; + + +use Email::Address; use MIME::Entity; use MIME::Head; use MIME::Parser; @@ -44,17 +72,13 @@ use File::Temp qw/tempdir/; =head1 DESCRIPTION -=begin testing - -ok(require RT::EmailParser); - -=end testing =head1 METHODS =head2 new +Returns a new RT::EmailParser object =cut @@ -67,120 +91,81 @@ sub new { } +=head2 SmartParseMIMEEntityFromScalar Message => SCALAR_REF [, Decode => BOOL, Exact => BOOL ] } -# {{{ sub debug - -sub debug { - my $val = shift; - my ($debug); - if ($val) { - $RT::Logger->debug( $val . "\n" ); - if ($debug) { - print STDERR "$val\n"; - } - } - if ($debug) { - return (1); - } -} - -# }}} - -# {{{ sub CheckForLoops - -sub CheckForLoops { - my $self = shift; - - my $head = $self->Head; +Parse a message stored in a scalar from scalar_ref. - #If this instance of RT sent it our, we don't want to take it in - my $RTLoop = $head->get("X-RT-Loop-Prevention") || ""; - chomp($RTLoop); #remove that newline - if ( $RTLoop =~ /^\Q$RT::rtname\E/o ) { - return (1); - } - - # TODO: We might not trap the case where RT instance A sends a mail - # to RT instance B which sends a mail to ... - return (undef); -} - -# }}} - -# {{{ sub CheckForSuspiciousSender +=cut -sub CheckForSuspiciousSender { +sub SmartParseMIMEEntityFromScalar { my $self = shift; + my %args = ( Message => undef, Decode => 1, Exact => 0, @_ ); + + eval { + my ( $fh, $temp_file ); + for ( 1 .. 10 ) { + + # on NFS and NTFS, it is possible that tempfile() conflicts + # with other processes, causing a race condition. we try to + # accommodate this by pausing and retrying. + last + if ( $fh, $temp_file ) = + eval { File::Temp::tempfile( UNLINK => 0 ) }; + sleep 1; + } + if ($fh) { + + #thank you, windows + binmode $fh; + $fh->autoflush(1); + print $fh $args{'Message'}; + close($fh); + if ( -f $temp_file ) { + + # We have to trust the temp file's name -- untaint it + $temp_file =~ /(.*)/; + my $entity = $self->ParseMIMEEntityFromFile( $1, $args{'Decode'}, $args{'Exact'} ); + unlink($1); + return $entity; + } + } + }; - #if it's from a postmaster or mailer daemon, it's likely a bounce. - - #TODO: better algorithms needed here - there is no standards for - #bounces, so it's very difficult to separate them from anything - #else. At the other hand, the Return-To address is only ment to be - #used as an error channel, we might want to put up a separate - #Return-To address which is treated differently. - - #TODO: search through the whole email and find the right Ticket ID. - - my ( $From, $junk ) = $self->ParseSenderAddressFromHead(); - - if ( ( $From =~ /^mailer-daemon/i ) or ( $From =~ /^postmaster/i ) ) { - return (1); - + #If for some reason we weren't able to parse the message using a temp file + # try it with a scalar + if ( $@ || !$self->Entity ) { + return $self->ParseMIMEEntityFromScalar( $args{'Message'}, $args{'Decode'}, $args{'Exact'} ); } - return (undef); - } -# }}} - -# {{{ sub CheckForAutoGenerated -sub CheckForAutoGenerated { - my $self = shift; - my $head = $self->Head; - my $Precedence = $head->get("Precedence") || ""; - if ( $Precedence =~ /^(bulk|junk)/i ) { - return (1); - } - else { - return (undef); - } -} +=head2 ParseMIMEEntityFromSTDIN -# }}} +Parse a message from standard input -# {{{ sub ParseMIMEEntityFromSTDIN +=cut sub ParseMIMEEntityFromSTDIN { my $self = shift; - return $self->ParseMIMEEntityFromFileHandle(\*STDIN); + return $self->ParseMIMEEntityFromFileHandle(\*STDIN, @_); } -# }}} - =head2 ParseMIMEEntityFromScalar $message -Takes either a scalar or a reference to a scalr which contains a stringified MIME message. +Takes either a scalar or a reference to a scalar which contains a stringified MIME message. Parses it. Returns true if it wins. Returns false if it loses. - =cut sub ParseMIMEEntityFromScalar { my $self = shift; - my $message = shift; - - $self->_DoParse('parse_data', $message); - + return $self->_ParseMIMEEntity( shift, 'parse_data', @_ ); } -# {{{ ParseMIMEEntityFromFilehandle *FH - =head2 ParseMIMEEntityFromFilehandle *FH Parses a mime entity from a filehandle passed in as an argument @@ -189,16 +174,9 @@ Parses a mime entity from a filehandle passed in as an argument sub ParseMIMEEntityFromFileHandle { my $self = shift; - my $filehandle = shift; - - $self->_DoParse('parse', $filehandle); - + return $self->_ParseMIMEEntity( shift, 'parse', @_ ); } -# }}} - -# {{{ ParseMIMEEntityFromFile - =head2 ParseMIMEEntityFromFile Parses a mime entity from a filename passed in as an argument @@ -207,52 +185,80 @@ Parses a mime entity from a filename passed in as an argument sub ParseMIMEEntityFromFile { my $self = shift; - - my $file = shift; - $self->_DoParse('parse_open', $file); + return $self->_ParseMIMEEntity( shift, 'parse_open', @_ ); } -# }}} - -# {{{ _DoParse - -=head2 _DoParse PARSEMETHOD CONTENT - - -A helper for the various parsers to turn around and do the dispatch to the actual parser - -=cut -sub _DoParse { +sub _ParseMIMEEntity { my $self = shift; + my $message = shift; my $method = shift; - my $file = shift; + my $postprocess = (@_ ? shift : 1); + my $exact = shift; # Create a new parser object: - my $parser = MIME::Parser->new(); $self->_SetupMIMEParser($parser); - + $parser->decode_bodies(0) if $exact; # TODO: XXX 3.0 we really need to wrap this in an eval { } - - unless ( $self->{'entity'} = $parser->$method($file) ) { - + unless ( $self->{'entity'} = $parser->$method($message) ) { + $RT::Logger->crit("Couldn't parse MIME stream and extract the submessages"); # Try again, this time without extracting nested messages $parser->extract_nested_messages(0); - unless ( $self->{'entity'} = $parser->$method($file) ) { + unless ( $self->{'entity'} = $parser->$method($message) ) { $RT::Logger->crit("couldn't parse MIME stream"); return ( undef); } } - $self->_PostProcessNewEntity(); - return (1); + + $self->_PostProcessNewEntity if $postprocess; + + return $self->{'entity'}; } -# }}} +sub _DecodeBodies { + my $self = shift; + return unless $self->{'entity'}; + + my @parts = $self->{'entity'}->parts_DFS; + $self->_DecodeBody($_) foreach @parts; +} + +sub _DecodeBody { + my $self = shift; + my $entity = shift; + + my $old = $entity->bodyhandle or return; + return unless $old->is_encoded; + + require MIME::Decoder; + my $encoding = $entity->head->mime_encoding; + my $decoder = MIME::Decoder->new($encoding); + unless ( $decoder ) { + $RT::Logger->error("Couldn't find decoder for '$encoding', switching to binary"); + $old->is_encoded(0); + return; + } + require MIME::Body; + # XXX: use InCore for now, but later must switch to files + my $new = MIME::Body::InCore->new(); + $new->binmode(1); + $new->is_encoded(0); + + my $source = $old->open('r') or die "couldn't open body: $!"; + my $destination = $new->open('w') or die "couldn't open body: $!"; + { + local $@; + eval { $decoder->decode($source, $destination) }; + $RT::Logger->error($@) if $@; + } + $source->close or die "can't close: $!"; + $destination->close or die "can't close: $!"; -# {{{ _PostProcessNewEntity + $entity->bodyhandle( $new ); +} =head2 _PostProcessNewEntity @@ -265,54 +271,26 @@ sub _PostProcessNewEntity { #Now we've got a parsed mime object. - # try to convert text parts into utf-8 charset - RT::I18N::SetMIMEEntityToEncoding($self->{'entity'}, 'utf-8'); - - # Unfold headers that are have embedded newlines + # Better do this before conversion or it will break + # with multiline encoded Subject (RFC2047) (fsck.com #5594) $self->Head->unfold; - -} - -# }}} - -# {{{ sub ParseTicketId - -sub ParseTicketId { - my $self = shift; - - my $Subject = shift; - - if ( $Subject =~ s/\[\Q$RT::rtname\E\s+\#(\d+)\s*\]//i ) { - my $id = $1; - $RT::Logger->debug("Found a ticket ID. It's $id"); - return ($id); - } - else { - return (undef); - } + # try to convert text parts into utf-8 charset + RT::I18N::SetMIMEEntityToEncoding($self->{'entity'}, 'utf-8'); } -# }}} - - - -# {{{ ParseCcAddressesFromHead - =head2 ParseCcAddressesFromHead HASHREF Takes a hashref object containing QueueObj, Head and CurrentUser objects. Returns a list of all email addresses in the To and Cc -headers b the current Queue\'s email addresses, the CurrentUser\'s -email address and anything that the $RTAddressRegexp matches. +headers b the current Queue's email addresses, the CurrentUser's +email address and anything that the RT->Config->Get('RTAddressRegexp') matches. =cut sub ParseCcAddressesFromHead { - my $self = shift; - my %args = ( QueueObj => undef, CurrentUser => undef, @@ -321,171 +299,77 @@ sub ParseCcAddressesFromHead { my (@Addresses); - my @ToObjs = Mail::Address->parse( $self->Head->get('To') ); - my @CcObjs = Mail::Address->parse( $self->Head->get('Cc') ); + my @ToObjs = Email::Address->parse( Encode::decode( "UTF-8", $self->Head->get('To') ) ); + my @CcObjs = Email::Address->parse( Encode::decode( "UTF-8", $self->Head->get('Cc') ) ); foreach my $AddrObj ( @ToObjs, @CcObjs ) { my $Address = $AddrObj->address; - my $user = RT::User->new($RT::SystemUser); + my $user = RT::User->new(RT->SystemUser); $Address = $user->CanonicalizeEmailAddress($Address); - next if ( $args{'CurrentUser'}->EmailAddress =~ /^$Address$/i ); - next if ( $args{'QueueObj'}->CorrespondAddress =~ /^$Address$/i ); - next if ( $args{'QueueObj'}->CommentAddress =~ /^$Address$/i ); - next if ( IsRTAddress($Address) ); + next if lc $args{'CurrentUser'}->EmailAddress eq lc $Address; + next if $self->IsRTAddress($Address); push ( @Addresses, $Address ); } return (@Addresses); } -# }}} - -# {{{ ParseSenderAdddressFromHead - -=head2 ParseSenderAddressFromHead - -Takes a MIME::Header object. Returns a tuple: (user@host, friendly name) -of the From (evaluated in order of Reply-To:, From:, Sender) - -=cut - -sub ParseSenderAddressFromHead { - my $self = shift; - - #Figure out who's sending this message. - my $From = $self->Head->get('Reply-To') - || $self->Head->get('From') - || $self->Head->get('Sender'); - return ( $self->ParseAddressFromHeader($From) ); -} - -# }}} -# {{{ ParseErrorsToAdddressFromHead +=head2 IsRTaddress ADDRESS -=head2 ParseErrorsToAddressFromHead +Takes a single parameter, an email address. +Returns true if that address matches the C config option. +Returns false, otherwise. -Takes a MIME::Header object. Return a single value : user@host -of the From (evaluated in order of Errors-To:,Reply-To:, From:, Sender) =cut -sub ParseErrorsToAddressFromHead { +sub IsRTAddress { my $self = shift; + my $address = shift; - #Figure out who's sending this message. - - foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) { - - # If there's a header of that name - my $headerobj = $self->Head->get($header); - if ($headerobj) { - my ( $addr, $name ) = $self->ParseAddressFromHeader($headerobj); + return undef unless defined($address) and $address =~ /\S/; - # If it's got actual useful content... - return ($addr) if ($addr); - } + if ( my $address_re = RT->Config->Get('RTAddressRegexp') ) { + return $address =~ /$address_re/i ? 1 : undef; } -} - -# }}} - -# {{{ ParseAddressFromHeader - -=head2 ParseAddressFromHeader ADDRESS - -Takes an address from $self->Head->get('Line') and returns a tuple: user@host, friendly name - -=cut - -sub ParseAddressFromHeader { - my $self = shift; - my $Addr = shift; - - my @Addresses = Mail::Address->parse($Addr); - my $AddrObj = $Addresses[0]; - - unless ( ref($AddrObj) ) { - return ( undef, undef ); + # we don't warn here, but do in config check + if ( my $correspond_address = RT->Config->Get('CorrespondAddress') ) { + return 1 if lc $correspond_address eq lc $address; + } + if ( my $comment_address = RT->Config->Get('CommentAddress') ) { + return 1 if lc $comment_address eq lc $address; } - my $Name = ( $AddrObj->phrase || $AddrObj->comment || $AddrObj->address ); - - #Lets take the from and load a user object. - my $Address = $AddrObj->address; - - return ( $Address, $Name ); -} - -# }}} - -# {{{ IsRTAddress - -=item IsRTaddress ADDRESS - -Takes a single parameter, an email address. -Returns true if that address matches the $RTAddressRegexp. -Returns false, otherwise. - -=begin testing - -is(RT::EmailParser::IsRTAddress("","rt\@example.com"),1, "Regexp matched rt address" ); -is(RT::EmailParser::IsRTAddress("","frt\@example.com"),undef, "Regexp didn't match non-rt address" ); - -=end testing - -=cut + my $queue = RT::Queue->new( RT->SystemUser ); + $queue->LoadByCols( CorrespondAddress => $address ); + return 1 if $queue->id; -sub IsRTAddress { - my $self = shift; - my $address = shift; + $queue->LoadByCols( CommentAddress => $address ); + return 1 if $queue->id; - # Example: the following rule would tell RT not to Cc - # "tickets@noc.example.com" - if ( defined($RT::RTAddressRegexp) && - $address =~ /$RT::RTAddressRegexp/ ) { - return(1); - } else { - return (undef); - } + return undef; } -# }}} - - -# {{{ CullRTAddresses -=item CullRTAddresses ARRAY +=head2 CullRTAddresses ARRAY Takes a single argument, an array of email addresses. Returns the same array with any IsRTAddress()es weeded out. -=begin testing - -@before = ("rt\@example.com", "frt\@example.com"); -@after = ("frt\@example.com"); -ok(eq_array(RT::EmailParser::CullRTAddresses("",@before),@after), "CullRTAddresses only culls RT addresses"); - -=end testing =cut sub CullRTAddresses { my $self = shift; - my @addresses= (@_); - my @addrlist; + my @addresses = (@_); - foreach my $addr( @addresses ) { - push (@addrlist, $addr) unless IsRTAddress("", $addr); - } - return (@addrlist); + return grep { !$self->IsRTAddress($_) } @addresses; } -# }}} -# {{{ LookupExternalUserInfo # LookupExternalUserInfo is a site-definable method for synchronizing @@ -501,7 +385,7 @@ sub CullRTAddresses { # template for the rejection message. -=item LookupExternalUserInfo +=head2 LookupExternalUserInfo LookupExternalUserInfo is a site-definable method for synchronizing incoming users with an external data source. @@ -514,12 +398,12 @@ sub CullRTAddresses { It returns (FoundInExternalDatabase, ParamHash); - FoundInExternalDatabase must be set to 1 before return if the user was - found in the external database. + FoundInExternalDatabase must be set to 1 before return if the user + was found in the external database. - ParamHash is a Perl parameter hash which can contain at least the following - fields. These fields are used to populate RT's users database when the user - is created + ParamHash is a Perl parameter hash which can contain at least the + following fields. These fields are used to populate RT's users + database when the user is created. EmailAddress is the email address that RT should use for this user. Name is the 'Name' attribute RT should use for this user. @@ -542,15 +426,9 @@ sub LookupExternalUserInfo { $params{'EmailAddress'} = $EmailAddress; $params{'RealName'} = $RealName; - # See RT's contributed code for examples. - # http://www.fsck.com/pub/rt/contrib/ return ($FoundInExternalDatabase, %params); } -# }}} - -# {{{ Accessor methods for parsed email messages - =head2 Head Return the parsed head from this message @@ -573,8 +451,7 @@ sub Entity { return $self->{'entity'}; } -# }}} -# {{{ _SetupMIMEParser + =head2 _SetupMIMEParser $parser @@ -593,39 +470,225 @@ A private instance method which sets up a mime parser to do its job sub _SetupMIMEParser { my $self = shift; my $parser = shift; - - # Set up output directory for files: - - my $tmpdir = File::Temp::tempdir( TMPDIR => 1, CLEANUP => 1 ); - push ( @{ $self->{'AttachmentDirs'} }, $tmpdir ); - $parser->output_dir($tmpdir); - $parser->filer->ignore_filename(1); + + # Set up output directory for files; we use $RT::VarPath instead + # of File::Spec->tmpdir (e.g., /tmp) beacuse it isn't always + # writable. + my $tmpdir; + if ( -w $RT::VarPath ) { + $tmpdir = File::Temp::tempdir( DIR => $RT::VarPath, CLEANUP => 1 ); + } elsif (-w File::Spec->tmpdir) { + $tmpdir = File::Temp::tempdir( TMPDIR => 1, CLEANUP => 1 ); + } else { + $RT::Logger->crit("Neither the RT var directory ($RT::VarPath) nor the system tmpdir (@{[File::Spec->tmpdir]}) are writable; falling back to in-memory parsing!"); + } #If someone includes a message, extract it $parser->extract_nested_messages(1); - $parser->extract_uuencode(1); ### default is false - # Set up the prefix for files with auto-generated names: - $parser->output_prefix("part"); + if ($tmpdir) { + # If we got a writable tmpdir, write to disk + push ( @{ $self->{'AttachmentDirs'} ||= [] }, $tmpdir ); + $parser->output_dir($tmpdir); + $parser->filer->ignore_filename(1); + + # Set up the prefix for files with auto-generated names: + $parser->output_prefix("part"); + + # From the MIME::Parser docs: + # "Normally, tmpfiles are created when needed during parsing, and destroyed automatically when they go out of scope" + # Turns out that the default is to recycle tempfiles + # Temp files should never be recycled, especially when running under perl taint checking + + $parser->tmp_recycling(0) if $parser->can('tmp_recycling'); + } else { + # Otherwise, fall back to storing it in memory + $parser->output_to_core(1); + $parser->tmp_to_core(1); + $parser->use_inner_files(1); + } + +} + +=head2 ParseEmailAddress string + +Returns a list of Email::Address objects +Works around the bug that Email::Address 1.889 and earlier +doesn't handle local-only email addresses (when users pass +in just usernames on the RT system in fields that expect +Email Addresses) + +We don't handle the case of +bob, fred@bestpractical.com +because we don't want to fail parsing +bob, "Falcone, Fred" +The next release of Email::Address will have a new method +we can use that removes the bandaid + +=cut + +sub ParseEmailAddress { + my $self = shift; + my $address_string = shift; + + $address_string =~ s/^\s+|\s+$//g; + + my @addresses; + # if it looks like a username / local only email + if ($address_string !~ /@/ && $address_string =~ /^\w+$/) { + my $user = RT::User->new( RT->SystemUser ); + my ($id, $msg) = $user->Load($address_string); + if ($id) { + push @addresses, Email::Address->new($user->Name,$user->EmailAddress); + } else { + $RT::Logger->error("Unable to parse an email address from $address_string: $msg"); + } + } else { + @addresses = Email::Address->parse($address_string); + } + + $self->CleanupAddresses(@addresses); + + return @addresses; + +} + +=head2 CleanupAddresses ARRAY + +Massages an array of L objects to make their email addresses +more palatable. + +Currently this strips off surrounding single quotes around C<< ->address >> and +B<< modifies the L objects in-place >>. + +Returns the list of objects for convienence in C/C chains. + +=cut + +sub CleanupAddresses { + my $self = shift; + + for my $addr (@_) { + next unless defined $addr; + # Outlook sometimes sends addresses surrounded by single quotes; + # clean them all up + if ((my $email = $addr->address) =~ s/^'(.+)'$/$1/) { + $addr->address($email); + } + } + return @_; +} + +=head2 RescueOutlook - # do _not_ store each msg as in-core scalar; +Outlook 2007/2010 have a bug when you write an email with the html format. +it will send a 'multipart/alternative' with both 'text/plain' and 'text/html' +in it. it's cool to have a 'text/plain' part, but the problem is the part is +not so right: all the "\n" in your main message will become "\n\n" :/ - $parser->output_to_core(0); +this method will fix this bug, i.e. replaces "\n\n" to "\n". +return 1 if it does find the problem in the entity and get it fixed. + +=cut + + +sub RescueOutlook { + my $self = shift; + my $mime = $self->Entity(); + return unless $mime && $self->LooksLikeMSEmail($mime); + + my $text_part; + if ( $mime->head->get('Content-Type') =~ m{multipart/mixed} ) { + my $first = $mime->parts(0); + if ( $first && $first->head->get('Content-Type') =~ m{multipart/alternative} ) + { + my $inner_first = $first->parts(0); + if ( $inner_first && $inner_first->head->get('Content-Type') =~ m{text/plain} ) + { + $text_part = $inner_first; + } + } + } + elsif ( $mime->head->get('Content-Type') =~ m{multipart/alternative} ) { + my $first = $mime->parts(0); + if ( $first && $first->head->get('Content-Type') =~ m{text/plain} ) { + $text_part = $first; + } + } + + # Add base64 since we've seen examples of double newlines with + # this type too. Need an example of a multi-part base64 to + # handle that permutation if it exists. + elsif ( ($mime->head->get('Content-Transfer-Encoding')||'') =~ m{base64} ) { + $text_part = $mime; # Assuming single part, already decoded. + } + + if ($text_part) { + + # use the unencoded string + my $content = $text_part->bodyhandle->as_string; + if ( $content =~ s/\n\n/\n/g ) { + + # Outlook puts a space on extra newlines, remove it + $content =~ s/\ +$//mg; + + # only write only if we did change the content + if ( my $io = $text_part->open("w") ) { + $io->print($content); + $io->close; + $RT::Logger->debug( + "Removed extra newlines from MS Outlook message."); + return 1; + } + else { + $RT::Logger->error("Can't write to body to fix newlines"); + } + } + } + + return; } -# }}} +=head1 LooksLikeMSEmail + +Try to determine if the current email may have +come from MS Outlook or gone through Exchange, and therefore +may have extra newlines added. + +=cut + +sub LooksLikeMSEmail { + my $self = shift; + my $mime = shift; + + my $mailer = $mime->head->get('X-Mailer'); + + # 12.0 is outlook 2007, 14.0 is 2010 + return 1 if ( $mailer && $mailer =~ /Microsoft(?:.*?)Outlook 1[2-4]\./ ); + + if ( RT->Config->Get('CheckMoreMSMailHeaders') ) { + + # Check for additional headers that might + # indicate this came from Outlook or through Exchange. + # A sample we received had the headers X-MS-Has-Attach: and + # X-MS-Tnef-Correlator: and both had no value. + + my @tags = $mime->head->tags(); + return 1 if grep { /^X-MS-/ } @tags; + } + + return 0; # Doesn't look like MS email. +} sub DESTROY { my $self = shift; - File::Path::rmtree([@{$self->{'AttachmentDirs'}}],0,1); + File::Path::rmtree([@{$self->{'AttachmentDirs'}}],0,1) + if $self->{'AttachmentDirs'}; } -eval "require RT::EmailParser_Vendor"; -die $@ if ($@ && $@ !~ qr{^Can't locate RT/EmailParser_Vendor.pm}); -eval "require RT::EmailParser_Local"; -die $@ if ($@ && $@ !~ qr{^Can't locate RT/EmailParser_Local.pm}); +RT::Base->_ImportOverlays(); 1;