-# BEGIN LICENSE BLOCK
+# BEGIN BPS TAGGED BLOCK {{{
#
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
+# <jesse@bestpractical.com>
#
-# (Except where explictly superceded by other copyright notices)
+# (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
# 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.
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+#
+# 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 LICENSE BLOCK
+# END BPS TAGGED BLOCK }}}
package RT::EmailParser;
=head1 NAME
- RT::Interface::CLI - helper functions for creating a commandline RT interface
+ RT::EmailParser - helper functions for parsing parts from incoming
+ email messages
=head1 SYNOPSIS
=head2 new
+Returns a new RT::EmailParser object
=cut
}
+# {{{ sub SmartParseMIMEEntityFromScalar
-# {{{ 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;
-
- #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);
-}
+=head2 SmartParseMIMEEntityFromScalar { Message => SCALAR_REF, Decode => BOOL }
-# }}}
+Parse a message stored in a scalar from scalar_ref
-# {{{ sub CheckForSuspiciousSender
+=cut
-sub CheckForSuspiciousSender {
+sub SmartParseMIMEEntityFromScalar {
my $self = shift;
+ my %args = ( Message => undef, Decode => 1, @_ );
- #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 ( $fh, $temp_file );
+ eval {
- my ( $From, $junk ) = $self->ParseSenderAddressFromHead();
+ for ( 1 .. 10 ) {
- if ( ( $From =~ /^mailer-daemon/i ) or ( $From =~ /^postmaster/i ) ) {
- return (1);
+ # 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( undef, 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 =~ /(.*)/;
+ $self->ParseMIMEEntityFromFile( $1, $args{'Decode'} );
+ unlink($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 ) {
+ $self->ParseMIMEEntityFromScalar( $args{'Message'}, $args{'Decode'} );
}
- return (undef);
-
}
# }}}
-# {{{ sub CheckForAutoGenerated
-sub CheckForAutoGenerated {
- my $self = shift;
- my $head = $self->Head;
+# {{{ sub ParseMIMEEntityFromSTDIN
- 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);
+ my $postprocess = (@_ ? shift : 1);
+ return $self->ParseMIMEEntityFromFileHandle(\*STDIN, $postprocess);
}
# }}}
+# {{{ ParseMIMEEntityFromScalar
+
=head2 ParseMIMEEntityFromScalar $message
Takes either a scalar or a reference to a scalr which contains a stringified MIME message.
Returns true if it wins.
Returns false if it loses.
-
=cut
sub ParseMIMEEntityFromScalar {
my $self = shift;
my $message = shift;
-
- $self->_DoParse('parse_data', $message);
-
+ my $postprocess = (@_ ? shift : 1);
+ $self->_ParseMIMEEntity($message,'parse_data', $postprocess);
}
+# }}}
+
# {{{ ParseMIMEEntityFromFilehandle *FH
=head2 ParseMIMEEntityFromFilehandle *FH
sub ParseMIMEEntityFromFileHandle {
my $self = shift;
my $filehandle = shift;
-
- $self->_DoParse('parse', $filehandle);
-
+ my $postprocess = (@_ ? shift : 1);
+ $self->_ParseMIMEEntity($filehandle,'parse', $postprocess);
}
# }}}
sub ParseMIMEEntityFromFile {
my $self = shift;
-
my $file = shift;
- $self->_DoParse('parse_open', $file);
+ my $postprocess = (@_ ? shift : 1);
+ $self->_ParseMIMEEntity($file,'parse_open',$postprocess);
}
# }}}
-# {{{ _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 {
+# {{{ _ParseMIMEEntity
+sub _ParseMIMEEntity {
my $self = shift;
+ my $message = shift;
my $method = shift;
- my $file = shift;
-
+ my $postprocess = shift;
# Create a new parser object:
my $parser = MIME::Parser->new();
# 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);
+ if ($postprocess) {
+ $self->_PostProcessNewEntity() ;
+ }
+
}
# }}}
-
# {{{ _PostProcessNewEntity
=head2 _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;
-}
-
-# }}}
+ # try to convert text parts into utf-8 charset
+ RT::I18N::SetMIMEEntityToEncoding($self->{'entity'}, 'utf-8');
-# {{{ 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);
- }
}
# }}}
-# {{{ sub MailError
-
-=head2 MailError { }
-
-
-# TODO this doesn't belong here.
-# TODO doc this
-
-
-=cut
-
+# {{{ sub ParseTicketId
-sub MailError {
+sub ParseTicketId {
my $self = shift;
+ $RT::Logger->warnings("RT::EmailParser->ParseTicketId deprecated. You should be using RT::Interface::Email at (". join(":",caller).")");
- my %args = (
- To => $RT::OwnerEmail,
- Bcc => undef,
- From => $RT::CorrespondAddress,
- Subject => 'There has been an error',
- Explanation => 'Unexplained error',
- MIMEObj => undef,
- LogLevel => 'crit',
- @_
- );
-
- $RT::Logger->log(
- level => $args{'LogLevel'},
- message => $args{'Explanation'}
- );
- my $entity = MIME::Entity->build(
- Type => "multipart/mixed",
- From => $args{'From'},
- Bcc => $args{'Bcc'},
- To => $args{'To'},
- Subject => $args{'Subject'},
- 'X-RT-Loop-Prevention' => $RT::rtname,
- );
-
- $entity->attach( Data => $args{'Explanation'} . "\n" );
-
- my $mimeobj = $args{'MIMEObj'};
- $mimeobj->sync_headers();
- $entity->add_part($mimeobj);
-
- if ( $RT::MailCommand eq 'sendmailpipe' ) {
- open( MAIL, "|$RT::SendmailPath $RT::SendmailArguments" ) || return (0);
- print MAIL $entity->as_string;
- close(MAIL);
- }
- else {
- $entity->send( $RT::MailCommand, $RT::MailParams );
- }
+ require RT::Interface::Email;
+ RT::Interface::Email::ParseTicketId(@_);
}
# }}}
-# {{{ sub GetCurrentUser
-
-sub GetCurrentUser {
- my $self = shift;
- my $ErrorsTo = shift;
-
- my %UserInfo = ();
-
- #Suck the address of the sender out of the header
- my ( $Address, $Name ) = $self->ParseSenderAddressFromHead();
-
- my $tempuser = RT::User->new($RT::SystemUser);
-
- #This will apply local address canonicalization rules
- $Address = $tempuser->CanonicalizeEmailAddress($Address);
-
- #If desired, synchronize with an external database
- my $UserFoundInExternalDatabase = 0;
-
- # Username is the 'Name' attribute of the user that RT uses for things
- # like authentication
- my $Username = undef;
- ( $UserFoundInExternalDatabase, %UserInfo ) =
- $self->LookupExternalUserInfo( $Address, $Name );
-
- $Address = $UserInfo{'EmailAddress'};
- $Username = $UserInfo{'Name'};
-
- #Get us a currentuser object to work with.
- my $CurrentUser = RT::CurrentUser->new();
-
- # First try looking up by a username, if we got one from the external
- # db lookup. Next, try looking up by email address. Failing that,
- # try looking up by users who have this user's email address as their
- # username.
-
- if ($Username) {
- $CurrentUser->LoadByName($Username);
- }
-
- unless ( $CurrentUser->Id ) {
- $CurrentUser->LoadByEmail($Address);
- }
-
- #If we can't get it by email address, try by name.
- unless ( $CurrentUser->Id ) {
- $CurrentUser->LoadByName($Address);
- }
-
- unless ( $CurrentUser->Id ) {
-
- #If we couldn't load a user, determine whether to create a user
-
- # {{{ If we require an incoming address to be found in the external
- # user database, reject the incoming message appropriately
- if ( $RT::SenderMustExistInExternalDatabase
- && !$UserFoundInExternalDatabase ) {
-
- my $Message =
- "Sender's email address was not found in the user database.";
-
- # {{{ This code useful only if you've defined an AutoRejectRequest template
-
- require RT::Template;
- my $template = new RT::Template($RT::Nobody);
- $template->Load('AutoRejectRequest');
- $Message = $template->Content || $Message;
-
- # }}}
-
- MailError(
- To => $ErrorsTo,
- Subject => "Ticket Creation failed: user could not be created",
- Explanation => $Message,
- MIMEObj => $self->Entity,
- LogLevel => 'notice' );
-
- return ($CurrentUser);
-
- }
-
- # }}}
-
- else {
- my $NewUser = RT::User->new($RT::SystemUser);
-
- my ( $Val, $Message ) = $NewUser->Create(
- Name => ( $Username || $Address ),
- EmailAddress => $Address,
- RealName => "$Name",
- Password => undef,
- Privileged => 0,
- Comments => 'Autocreated on ticket submission'
- );
-
- unless ($Val) {
-
- # Deal with the race condition of two account creations at once
- #
- if ($Username) {
- $NewUser->LoadByName($Username);
- }
-
- unless ( $NewUser->Id ) {
- $NewUser->LoadByEmail($Address);
- }
-
- unless ( $NewUser->Id ) {
- MailError(To => $ErrorsTo,
- Subject => "User could not be created",
- Explanation =>
- "User creation failed in mailgateway: $Message",
- MIMEObj => $self->Entity,
- LogLevel => 'crit' );
- }
- }
- }
-
- #Load the new user object
- $CurrentUser->LoadByEmail($Address);
-
- unless ( $CurrentUser->id ) {
- $RT::Logger->warning(
- "Couldn't load user '$Address'." . "giving up" );
- MailError(
- To => $ErrorsTo,
- Subject => "User could not be loaded",
- Explanation =>
- "User '$Address' could not be loaded in the mail gateway",
- MIMEObj => $self->Entity,
- LogLevel => 'crit' );
-
- }
- }
-
- return ($CurrentUser);
-
-}
-
-# }}}
-
-
# {{{ ParseCcAddressesFromHead
=head2 ParseCcAddressesFromHead HASHREF
my $Address = $AddrObj->address;
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 ( lc $args{'QueueObj'}->CorrespondAddress eq lc $Address );
+ next if ( lc $args{'QueueObj'}->CommentAddress eq lc $Address );
+ next if ( $self->IsRTAddress($Address) );
push ( @Addresses, $Address );
}
my $self = shift;
my $Addr = shift;
+ # Perl 5.8.0 breaks when doing regex matches on utf8
+ Encode::_utf8_off($Addr) if $] == 5.008;
my @Addresses = Mail::Address->parse($Addr);
my $AddrObj = $Addresses[0];
# {{{ IsRTAddress
-=item IsRTaddress ADDRESS
+=head2 IsRTaddress ADDRESS
Takes a single parameter, an email address.
Returns true if that address matches the $RTAddressRegexp.
# Example: the following rule would tell RT not to Cc
# "tickets@noc.example.com"
if ( defined($RT::RTAddressRegexp) &&
- $address =~ /$RT::RTAddressRegexp/ ) {
+ $address =~ /$RT::RTAddressRegexp/i ) {
return(1);
} else {
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.
my @addrlist;
foreach my $addr( @addresses ) {
- push (@addrlist, $addr) unless IsRTAddress("", $addr);
+ # We use the class instead of the instance
+ # because sloppy code calls this method
+ # without a $self
+ push (@addrlist, $addr) unless RT::EmailParser->IsRTAddress($addr);
}
return (@addrlist);
}
# template for the rejection message.
-=item LookupExternalUserInfo
+=head2 LookupExternalUserInfo
LookupExternalUserInfo is a site-definable method for synchronizing
incoming users with an external data source.
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.
}
# }}}
+
# {{{ _SetupMIMEParser
=head2 _SetupMIMEParser $parser
## Over max size and return them
sub _SetupMIMEParser {
- my $self = shift;
+ my $self = shift;
my $parser = shift;
- my $AttachmentDir = File::Temp::tempdir( TMPDIR => 1, CLEANUP => 1 );
-
+
# Set up output directory for files:
- $parser->output_dir("$AttachmentDir");
- $parser->filer->ignore_filename(1);
+ my $tmpdir = File::Temp::tempdir( TMPDIR => 1, CLEANUP => 1 );
+ push ( @{ $self->{'AttachmentDirs'} }, $tmpdir );
+ $parser->output_dir($tmpdir);
+ $parser->filer->ignore_filename(1);
#If someone includes a message, extract it
$parser->extract_nested_messages(1);
- $parser->extract_uuencode(1); ### default is false
+ $parser->extract_uuencode(1); ### default is false
# Set up the prefix for files with auto-generated names:
$parser->output_prefix("part");
# do _not_ store each msg as in-core scalar;
$parser->output_to_core(0);
+
+ # 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);
+
}
+
# }}}
+sub DESTROY {
+ my $self = shift;
+ File::Path::rmtree([@{$self->{'AttachmentDirs'}}],0,1);
+}
+
+
+
eval "require RT::EmailParser_Vendor";
die $@ if ($@ && $@ !~ qr{^Can't locate RT/EmailParser_Vendor.pm});
eval "require RT::EmailParser_Local";