X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FInterface%2FEmail.pm;h=f860461e0e185fbb13d210b3b3918c51d85320bf;hb=31f3763747b82764bb019cfab5b2a2945fc9a99d;hp=b669b5b2f831ef0b7321f5fd558a71e4f6b2fe2d;hpb=b4b0c7e72d7eaee2fbfc7022022c9698323203dd;p=freeside.git diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm index b669b5b2f..f860461e0 100755 --- a/rt/lib/RT/Interface/Email.pm +++ b/rt/lib/RT/Interface/Email.pm @@ -1,40 +1,40 @@ # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# -# +# +# 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. -# +# # 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 @@ -43,7 +43,7 @@ # 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::Interface::Email; @@ -57,6 +57,7 @@ use RT::EmailParser; use File::Temp; use UNIVERSAL::require; use Mail::Mailer (); +use Text::ParseWords qw/shellwords/; BEGIN { use base 'Exporter'; @@ -113,7 +114,7 @@ sub CheckForLoops { my $head = shift; # If this instance of RT sent it our, we don't want to take it in - my $RTLoop = $head->get("X-RT-Loop-Prevention") || ""; + my $RTLoop = Encode::decode( "UTF-8", $head->get("X-RT-Loop-Prevention") || "" ); chomp ($RTLoop); # remove that newline if ( $RTLoop eq RT->Config->Get('rtname') ) { return 1; @@ -148,6 +149,9 @@ sub CheckForSuspiciousSender { my ( $From, $junk ) = ParseSenderAddressFromHead($head); + # If unparseable (non-ASCII), $From can come back undef + return undef if not defined $From; + if ( ( $From =~ /^mailer-daemon\@/i ) or ( $From =~ /^postmaster\@/i ) or ( $From eq "" )) @@ -221,8 +225,8 @@ add 'In-Reply-To' field to the error that points to this message. =item Attach - optional text that attached to the error as 'message/rfc822' part. -=item LogLevel - log level under which we should write explanation message into the -log, by default we log it as critical. +=item LogLevel - log level under which we should write the subject and +explanation message into the log, by default we log it as critical. =back @@ -243,21 +247,33 @@ sub MailError { $RT::Logger->log( level => $args{'LogLevel'}, - message => $args{'Explanation'} + message => "$args{Subject}: $args{'Explanation'}", ) if $args{'LogLevel'}; + # the colons are necessary to make ->build include non-standard headers - my $entity = MIME::Entity->build( - Type => "multipart/mixed", - From => $args{'From'}, - Bcc => $args{'Bcc'}, - To => $args{'To'}, - Subject => $args{'Subject'}, - 'Precedence:' => 'bulk', - 'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'), + my %entity_args = ( + Type => "multipart/mixed", + From => Encode::encode( "UTF-8", $args{'From'} ), + Bcc => Encode::encode( "UTF-8", $args{'Bcc'} ), + To => Encode::encode( "UTF-8", $args{'To'} ), + Subject => EncodeToMIME( String => $args{'Subject'} ), + 'X-RT-Loop-Prevention:' => Encode::encode( "UTF-8", RT->Config->Get('rtname') ), ); + + # only set precedence if the sysadmin wants us to + if (defined(RT->Config->Get('DefaultErrorMailPrecedence'))) { + $entity_args{'Precedence:'} = + Encode::encode( "UTF-8", RT->Config->Get('DefaultErrorMailPrecedence') ); + } + + my $entity = MIME::Entity->build(%entity_args); SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} ); - $entity->attach( Data => $args{'Explanation'} . "\n" ); + $entity->attach( + Type => "text/plain", + Charset => "UTF-8", + Data => Encode::encode( "UTF-8", $args{'Explanation'} . "\n" ), + ); if ( $args{'MIMEObj'} ) { $args{'MIMEObj'}->sync_headers; @@ -265,7 +281,7 @@ sub MailError { } if ( $args{'Attach'} ) { - $entity->attach( Data => $args{'Attach'}, Type => 'message/rfc822' ); + $entity->attach( Data => Encode::encode( "UTF-8", $args{'Attach'} ), Type => 'message/rfc822' ); } @@ -280,6 +296,9 @@ RT's outgoing mail configuration. If C is passed, and is a true value, the message will be marked as an autogenerated error, if possible. Sets Date field of the head to now if it's not set. +If the C header is set to any true value, the mail will +not be sent. One use is to let extensions easily cancel outgoing mail. + Ticket and Transaction arguments are optional. If Transaction is specified and Ticket is not then ticket of the transaction is used, but only if the transaction belongs to a ticket. @@ -307,6 +326,35 @@ header field then it's value is used =cut +sub WillSignEncrypt { + my %args = @_; + my $attachment = delete $args{Attachment}; + my $ticket = delete $args{Ticket}; + + if ( not RT->Config->Get('GnuPG')->{'Enable'} ) { + $args{Sign} = $args{Encrypt} = 0; + return wantarray ? %args : 0; + } + + for my $argument ( qw(Sign Encrypt) ) { + next if defined $args{ $argument }; + + if ( $attachment and defined $attachment->GetHeader("X-RT-$argument") ) { + $args{$argument} = $attachment->GetHeader("X-RT-$argument"); + } elsif ( $ticket and $argument eq "Encrypt" ) { + $args{Encrypt} = $ticket->QueueObj->Encrypt(); + } elsif ( $ticket and $argument eq "Sign" ) { + # Note that $queue->Sign is UI-only, and that all + # UI-generated messages explicitly set the X-RT-Crypt header + # to 0 or 1; thus this path is only taken for messages + # generated _not_ via the web UI. + $args{Sign} = $ticket->QueueObj->SignAuto(); + } + } + + return wantarray ? %args : ($args{Sign} || $args{Encrypt}); +} + sub SendEmail { my (%args) = ( Entity => undef, @@ -331,7 +379,7 @@ sub SendEmail { return 0; } - my $msgid = $args{'Entity'}->head->get('Message-ID') || ''; + my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' ); chomp $msgid; # If we don't have any recipients to send to, don't send a message; @@ -343,6 +391,11 @@ sub SendEmail { return -1; } + if ($args{'Entity'}->head->get('X-RT-Squelch')) { + $RT::Logger->info( $msgid . " Squelch header found. Not sending." ); + return -1; + } + if ( $TransactionObj && !$TicketObj && $TransactionObj->ObjectType eq 'RT::Ticket' ) { @@ -350,37 +403,27 @@ sub SendEmail { } if ( RT->Config->Get('GnuPG')->{'Enable'} ) { - my %crypt; - - my $attachment; - $attachment = $TransactionObj->Attachments->First - if $TransactionObj; - - foreach my $argument ( qw(Sign Encrypt) ) { - next if defined $args{ $argument }; - - if ( $attachment && defined $attachment->GetHeader("X-RT-$argument") ) { - $crypt{$argument} = $attachment->GetHeader("X-RT-$argument"); - } elsif ( $TicketObj ) { - $crypt{$argument} = $TicketObj->QueueObj->$argument(); - } - } - - my $res = SignEncrypt( %args, %crypt ); + %args = WillSignEncrypt( + %args, + Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef, + Ticket => $TicketObj, + ); + my $res = SignEncrypt( %args ); return $res unless $res > 0; } unless ( $args{'Entity'}->head->get('Date') ) { require RT::Date; - my $date = RT::Date->new( $RT::SystemUser ); + my $date = RT::Date->new( RT->SystemUser ); $date->SetToNow; - $args{'Entity'}->head->set( 'Date', $date->RFC2822( Timezone => 'server' ) ); + $args{'Entity'}->head->set( 'Date', Encode::encode( "UTF-8", $date->RFC2822( Timezone => 'server' ) ) ); } my $mail_command = RT->Config->Get('MailCommand'); - if ($mail_command eq 'testfile') { + if ($mail_command eq 'testfile' and not $Mail::Mailer::testfile::config{outfile}) { $Mail::Mailer::testfile::config{outfile} = File::Temp->new; + $RT::Logger->info("Storing outgoing emails in $Mail::Mailer::testfile::config{outfile}"); } # if it is a sub routine, we just return it; @@ -388,32 +431,34 @@ sub SendEmail { if ( $mail_command eq 'sendmailpipe' ) { my $path = RT->Config->Get('SendmailPath'); - my $args = RT->Config->Get('SendmailArguments'); + my @args = shellwords(RT->Config->Get('SendmailArguments')); - # SetOutgoingMailFrom - if ( RT->Config->Get('SetOutgoingMailFrom') ) { - my $OutgoingMailAddress; + # SetOutgoingMailFrom and bounces conflict, since they both want -f + if ( $args{'Bounce'} ) { + push @args, shellwords(RT->Config->Get('SendmailBounceArguments')); + } elsif ( my $MailFrom = RT->Config->Get('SetOutgoingMailFrom') ) { + my $OutgoingMailAddress = $MailFrom =~ /\@/ ? $MailFrom : undef; + my $Overrides = RT->Config->Get('OverrideOutgoingMailFrom') || {}; if ($TicketObj) { my $QueueName = $TicketObj->QueueObj->Name; - my $QueueAddressOverride = RT->Config->Get('OverrideOutgoingMailFrom')->{$QueueName}; + my $QueueAddressOverride = $Overrides->{$QueueName}; if ($QueueAddressOverride) { $OutgoingMailAddress = $QueueAddressOverride; } else { - $OutgoingMailAddress = $TicketObj->QueueObj->CorrespondAddress; + $OutgoingMailAddress ||= $TicketObj->QueueObj->CorrespondAddress + || RT->Config->Get('CorrespondAddress'); } } + elsif ($Overrides->{'Default'}) { + $OutgoingMailAddress = $Overrides->{'Default'}; + } - $OutgoingMailAddress ||= RT->Config->Get('OverrideOutgoingMailFrom')->{'Default'}; - - $args .= " -f $OutgoingMailAddress" + push @args, "-f", $OutgoingMailAddress if $OutgoingMailAddress; } - # Set Bounce Arguments - $args .= ' '. RT->Config->Get('SendmailBounceArguments') if $args{'Bounce'}; - # VERP if ( $TransactionObj and my $prefix = RT->Config->Get('VERPPrefix') and @@ -422,30 +467,39 @@ sub SendEmail { my $from = $TransactionObj->CreatorObj->EmailAddress; $from =~ s/@/=/g; $from =~ s/\s//g; - $args .= " -f $prefix$from\@$domain"; + push @args, "-f", "$prefix$from\@$domain"; } eval { # don't ignore CHLD signal to get proper exit code local $SIG{'CHLD'} = 'DEFAULT'; - open my $mail, "|$path $args" or die "couldn't execute program: $!"; - # if something wrong with $mail->print we will get PIPE signal, handle it local $SIG{'PIPE'} = sub { die "program unexpectedly closed pipe" }; + + require IPC::Open2; + my ($mail, $stdout); + my $pid = IPC::Open2::open2( $stdout, $mail, $path, @args ) + or die "couldn't execute program: $!"; + $args{'Entity'}->print($mail); + close $mail or die "close pipe failed: $!"; - unless ( close $mail ) { - die "close pipe failed: $!" if $!; # system error + waitpid($pid, 0); + if ($?) { # sendmail exit statuses mostly errors with data not software # TODO: status parsing: core dump, exit on signal or EX_* - my $msg = "$msgid: `$path $args` exitted with code ". ($?>>8); + my $msg = "$msgid: `$path @args` exited with code ". ($?>>8); $msg = ", interrupted by signal ". ($?&127) if $?&127; $RT::Logger->error( $msg ); + die $msg; } }; if ( $@ ) { - $RT::Logger->crit( "$msgid: Could not send mail with command `$path $args`: " . $@ ); + $RT::Logger->crit( "$msgid: Could not send mail with command `$path @args`: " . $@ ); + if ( $TicketObj ) { + _RecordSendEmailFailure( $TicketObj ); + } return 0; } } @@ -457,17 +511,21 @@ sub SendEmail { ) } }; unless ( $smtp ) { $RT::Logger->crit( "Could not connect to SMTP server."); + if ($TicketObj) { + _RecordSendEmailFailure( $TicketObj ); + } return 0; } # duplicate head as we want drop Bcc field my $head = $args{'Entity'}->head->dup; - my @recipients = map $_->address, map - Email::Address->parse($head->get($_)), qw(To Cc Bcc); + my @recipients = map $_->address, map + Email::Address->parse(Encode::decode("UTF-8", $head->get($_))), + qw(To Cc Bcc); $head->delete('Bcc'); my $sender = RT->Config->Get('SMTPFrom') - || $args{'Entity'}->head->get('From'); + || Encode::decode( "UTF-8", $args{'Entity'}->head->get('From') ); chomp $sender; my $status = $smtp->mail( $sender ) @@ -485,6 +543,9 @@ sub SendEmail { unless ( $status ) { $RT::Logger->crit( "$msgid: Could not send mail via SMTP." ); + if ( $TicketObj ) { + _RecordSendEmailFailure( $TicketObj ); + } return 0; } } @@ -502,6 +563,9 @@ sub SendEmail { unless ( $args{'Entity'}->send( @mailer_args ) ) { $RT::Logger->crit( "$msgid: Could not send mail." ); + if ( $TicketObj ) { + _RecordSendEmailFailure( $TicketObj ); + } return 0; } } @@ -525,7 +589,7 @@ sub PrepareEmailUsingTemplate { @_ ); - my $template = RT::Template->new( $RT::SystemUser ); + my $template = RT::Template->new( RT->SystemUser ); $template->LoadGlobalTemplate( $args{'Template'} ); unless ( $template->id ) { return (undef, "Couldn't load template '". $args{'Template'} ."'"); @@ -553,6 +617,7 @@ sub SendEmailUsingTemplate { Bcc => undef, From => RT->Config->Get('CorrespondAddress'), InReplyTo => undef, + ExtraHeaders => {}, @_ ); @@ -565,9 +630,12 @@ sub SendEmailUsingTemplate { return -1; } - $mail->head->set( $_ => $args{ $_ } ) + $mail->head->set( $_ => Encode::encode( "UTF-8", $args{ $_ } ) ) foreach grep defined $args{$_}, qw(To Cc Bcc From); + $mail->head->set( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) ) + foreach keys %{ $args{ExtraHeaders} }; + SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} ); return SendEmail( Entity => $mail ); @@ -585,7 +653,19 @@ sub ForwardTransaction { my $entity = $txn->ContentAsMIME; - return SendForward( %args, Entity => $entity, Transaction => $txn ); + my ( $ret, $msg ) = SendForward( %args, Entity => $entity, Transaction => $txn ); + if ($ret) { + my $ticket = $txn->TicketObj; + my ( $ret, $msg ) = $ticket->_NewTransaction( + Type => 'Forward Transaction', + Field => $txn->id, + Data => join ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc}, + ); + unless ($ret) { + $RT::Logger->error("Failed to create transaction: $msg"); + } + } + return ( $ret, $msg ); } =head2 ForwardTicket TICKET, To => '', Cc => '', Bcc => '' @@ -605,13 +685,33 @@ sub ForwardTicket { ) for qw(Create Correspond); my $entity = MIME::Entity->build( - Type => 'multipart/mixed', + Type => 'multipart/mixed', + Description => 'forwarded ticket', ); $entity->add_part( $_ ) foreach map $_->ContentAsMIME, @{ $txns->ItemsArrayRef }; - return SendForward( %args, Entity => $entity, Ticket => $ticket, Template => 'Forward Ticket' ); + my ( $ret, $msg ) = SendForward( + %args, + Entity => $entity, + Ticket => $ticket, + Template => 'Forward Ticket', + ); + + if ($ret) { + my ( $ret, $msg ) = $ticket->_NewTransaction( + Type => 'Forward Ticket', + Field => $ticket->id, + Data => join ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc}, + ); + unless ($ret) { + $RT::Logger->error("Failed to create transaction: $msg"); + } + } + + return ( $ret, $msg ); + } =head2 SendForward Entity => undef, Ticket => undef, Transaction => undef, Template => undef, To => '', Cc => '', Bcc => '' @@ -666,42 +766,64 @@ sub SendForward { . $txn->id ." of a ticket #". $txn->ObjectId; } $mail = MIME::Entity->build( - Type => 'text/plain', - Data => $description, + Type => 'text/plain', + Charset => "UTF-8", + Data => Encode::encode( "UTF-8", $description ), ); } $mail->head->set( $_ => EncodeToMIME( String => $args{$_} ) ) foreach grep defined $args{$_}, qw(To Cc Bcc); - $mail->attach( - Type => 'message/rfc822', - Disposition => 'attachment', - Description => 'forwarded message', - Data => $entity->as_string, - ); + $mail->make_multipart unless $mail->is_multipart; + $mail->add_part( $entity ); my $from; - my $subject = ''; - $subject = $txn->Subject if $txn; - $subject ||= $ticket->Subject if $ticket; - if ( RT->Config->Get('ForwardFromUser') ) { - $from = ($txn || $ticket)->CurrentUser->UserObj->EmailAddress; - } else { - # XXX: what if want to forward txn of other object than ticket? - $subject = AddSubjectTag( $subject, $ticket ); - $from = $ticket->QueueObj->CorrespondAddress - || RT->Config->Get('CorrespondAddress'); + unless (defined $mail->head->get('Subject')) { + my $subject = ''; + $subject = $txn->Subject if $txn; + $subject ||= $ticket->Subject if $ticket; + + unless ( RT->Config->Get('ForwardFromUser') ) { + # XXX: what if want to forward txn of other object than ticket? + $subject = AddSubjectTag( $subject, $ticket ); + } + + $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) ); } - $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) ); - $mail->head->set( From => EncodeToMIME( String => $from ) ); + + $mail->head->set( + From => EncodeToMIME( + String => GetForwardFrom( Transaction => $txn, Ticket => $ticket ) + ) + ); my $status = RT->Config->Get('ForwardFromUser') # never sign if we forward from User ? SendEmail( %args, Entity => $mail, Sign => 0 ) : SendEmail( %args, Entity => $mail ); return (0, $ticket->loc("Couldn't send email")) unless $status; - return (1, $ticket->loc("Send email successfully")); + return (1, $ticket->loc("Sent email successfully")); +} + +=head2 GetForwardFrom Ticket => undef, Transaction => undef + +Resolve the From field to use in forward mail + +=cut + +sub GetForwardFrom { + my %args = ( Ticket => undef, Transaction => undef, @_ ); + my $txn = $args{Transaction}; + my $ticket = $args{Ticket} || $txn->Object; + + if ( RT->Config->Get('ForwardFromUser') ) { + return ( $txn || $ticket )->CurrentUser->EmailAddress; + } + else { + return $ticket->QueueObj->CorrespondAddress + || RT->Config->Get('CorrespondAddress'); + } } =head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0 @@ -729,7 +851,7 @@ sub SignEncrypt { ); return 1 unless $args{'Sign'} || $args{'Encrypt'}; - my $msgid = $args{'Entity'}->head->get('Message-ID') || ''; + my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' ); chomp $msgid; $RT::Logger->debug("$msgid Signing message") if $args{'Sign'}; @@ -861,13 +983,10 @@ sub EncodeToMIME { return ($value); } - return ($value) unless $value =~ /[^\x20-\x7e]/; + return ($value) if $value =~ /^(?:[\t\x20-\x7e]|\x0D*\x0A[ \t])+$/s; $value =~ s/\s+$//; - # we need perl string to split thing char by char - Encode::_utf8_on($value) unless Encode::is_utf8($value); - my ( $tmp, @chunks ) = ( '', () ); while ( length $value ) { my $char = substr( $value, 0, 1, '' ); @@ -890,7 +1009,7 @@ sub EncodeToMIME { sub CreateUser { my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_; - my $NewUser = RT::User->new( $RT::SystemUser ); + my $NewUser = RT::User->new( RT->SystemUser ); my ( $Val, $Message ) = $NewUser->Create( Name => ( $Username || $Address ), @@ -925,7 +1044,7 @@ sub CreateUser { } #Load the new user object - my $CurrentUser = new RT::CurrentUser; + my $CurrentUser = RT::CurrentUser->new; $CurrentUser->LoadByEmail( $Address ); unless ( $CurrentUser->id ) { @@ -950,7 +1069,7 @@ sub CreateUser { Takes a hash 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 +headers b the current Queue's email addresses, the CurrentUser's email address and anything that the configuration sub RT::IsRTAddress matches. =cut @@ -963,45 +1082,64 @@ sub ParseCcAddressesFromHead { @_ ); - my @recipients = - map lc $_->address, - map Email::Address->parse( $args{'Head'}->get( $_ ) ), + my $current_address = lc $args{'CurrentUser'}->EmailAddress; + my $user = $args{'CurrentUser'}->UserObj; + + return + grep { $_ ne $current_address + && !RT::EmailParser->IsRTAddress( $_ ) + && !IgnoreCcAddress( $_ ) + } + map lc $user->CanonicalizeEmailAddress( $_->address ), + map RT::EmailParser->CleanupAddresses( Email::Address->parse( + Encode::decode( "UTF-8", $args{'Head'}->get( $_ ) ) ) ), qw(To Cc); +} - my @res; - foreach my $address ( @recipients ) { - $address = $args{'CurrentUser'}->UserObj->CanonicalizeEmailAddress( $address ); - next if lc $args{'CurrentUser'}->EmailAddress eq $address; - next if lc $args{'QueueObj'}->CorrespondAddress eq $address; - next if lc $args{'QueueObj'}->CommentAddress eq $address; - next if RT::EmailParser->IsRTAddress( $address ); +=head2 IgnoreCcAddress ADDRESS - push @res, $address; - } - return @res; -} +Returns true if ADDRESS matches the $IgnoreCcRegexp config variable. +=cut +sub IgnoreCcAddress { + my $address = shift; + if ( my $address_re = RT->Config->Get('IgnoreCcRegexp') ) { + return 1 if $address =~ /$address_re/i; + } + return undef; +} =head2 ParseSenderAddressFromHead HEAD -Takes a MIME::Header object. Returns a tuple: (user@host, friendly name) -of the From (evaluated in order of Reply-To:, From:, Sender) +Takes a MIME::Header object. Returns (user@host, friendly name, errors) +where the first two values are the From (evaluated in order of +Reply-To:, From:, Sender). + +A list of error messages may be returned even when a Sender value is +found, since it could be a parse error for another (checked earlier) +sender field. In this case, the errors aren't fatal, but may be useful +to investigate the parse failure. =cut sub ParseSenderAddressFromHead { my $head = shift; + my @sender_headers = ('Reply-To', 'From', 'Sender'); + my @errors; # Accumulate any errors #Figure out who's sending this message. - foreach my $header ('Reply-To', 'From', 'Sender') { - my $addr_line = $head->get($header) || next; + foreach my $header ( @sender_headers ) { + my $addr_line = Encode::decode( "UTF-8", $head->get($header) ) || next; my ($addr, $name) = ParseAddressFromHeader( $addr_line ); # only return if the address is not empty - return ($addr, $name) if $addr; + return ($addr, $name, @errors) if $addr; + + chomp $addr_line; + push @errors, "$header: $addr_line"; } - return (undef, undef); + return (undef, undef, @errors); } =head2 ParseErrorsToAddressFromHead HEAD @@ -1020,7 +1158,7 @@ sub ParseErrorsToAddressFromHead { foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) { # If there's a header of that name - my $headerobj = $head->get($header); + my $headerobj = Encode::decode( "UTF-8", $head->get($header) ); if ($headerobj) { my ( $addr, $name ) = ParseAddressFromHeader($headerobj); @@ -1050,12 +1188,7 @@ sub ParseAddressFromHeader { return ( undef, undef ); } - my $Name = ( $AddrObj->name || $AddrObj->phrase || $AddrObj->comment || $AddrObj->address ); - - #Lets take the from and load a user object. - my $Address = $AddrObj->address; - - return ( $Address, $Name ); + return ( $AddrObj->address, $AddrObj->phrase ); } =head2 DeleteRecipientsFromHead HEAD RECIPIENTS @@ -1070,9 +1203,9 @@ sub DeleteRecipientsFromHead { my %skip = map { lc $_ => 1 } @_; foreach my $field ( qw(To Cc Bcc) ) { - $head->set( $field => + $head->set( $field => Encode::encode( "UTF-8", join ', ', map $_->format, grep !$skip{ lc $_->address }, - Email::Address->parse( $head->get( $field ) ) + Email::Address->parse( Encode::decode( "UTF-8", $head->get( $field ) ) ) ) ); } } @@ -1105,7 +1238,7 @@ sub SetInReplyTo { my $get_header = sub { my @res; if ( $args{'InReplyTo'}->isa('MIME::Entity') ) { - @res = $args{'InReplyTo'}->head->get( shift ); + @res = map {Encode::decode("UTF-8", $_)} $args{'InReplyTo'}->head->get( shift ); } else { @res = $args{'InReplyTo'}->GetHeader( shift ) || ''; } @@ -1128,8 +1261,16 @@ sub SetInReplyTo { if @references > 10; my $mail = $args{'Message'}; - $mail->head->set( 'In-Reply-To' => join ' ', @rtid? (@rtid) : (@id) ) if @id || @rtid; - $mail->head->set( 'References' => join ' ', @references ); + $mail->head->set( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid; + $mail->head->set( 'References' => Encode::encode( "UTF-8", join ' ', @references) ); +} + +sub ExtractTicketId { + my $entity = shift; + + my $subject = Encode::decode( "UTF-8", $entity->head->get('Subject') || '' ); + chomp $subject; + return ParseTicketId( $subject ); } sub ParseTicketId { @@ -1158,7 +1299,7 @@ sub AddSubjectTag { my $subject = shift; my $ticket = shift; unless ( ref $ticket ) { - my $tmp = RT::Ticket->new( $RT::SystemUser ); + my $tmp = RT::Ticket->new( RT->SystemUser ); $tmp->Load( $ticket ); $ticket = $tmp; } @@ -1174,7 +1315,7 @@ sub AddSubjectTag { } return $subject if $subject =~ /\[$tag_re\s+#$id\]/; - $subject =~ s/(\r\n|\n|\s)/ /gi; + $subject =~ s/(\r\n|\n|\s)/ /g; chomp $subject; return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject"; } @@ -1226,7 +1367,7 @@ sub _LoadPlugins { } elsif ( !ref $plugin ) { my $Class = $plugin; $Class = "RT::Interface::Email::" . $Class - unless $Class =~ /^RT::Interface::Email::/; + unless $Class =~ /^RT::/; $Class->require or do { $RT::Logger->error("Couldn't load $Class: $@"); next }; @@ -1326,19 +1467,23 @@ sub Gateway { } @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins; $parser->_DecodeBodies; + $parser->RescueOutlook; $parser->_PostProcessNewEntity; my $head = $Message->head; my $ErrorsTo = ParseErrorsToAddressFromHead( $head ); + my $Sender = (ParseSenderAddressFromHead( $head ))[0]; + my $From = Encode::decode( "UTF-8", $head->get("From") ); + chomp $From if defined $From; - my $MessageId = $head->get('Message-ID') + my $MessageId = Encode::decode( "UTF-8", $head->get('Message-ID') ) || "Config->Get('Organization') .'>'; #Pull apart the subject line - my $Subject = $head->get('Subject') || ''; + my $Subject = Encode::decode( "UTF-8", $head->get('Subject') || ''); chomp $Subject; - # {{{ Lets check for mail loops of various sorts. + # Lets check for mail loops of various sorts. my ($should_store_machine_generated_message, $IsALoop, $result); ( $should_store_machine_generated_message, $ErrorsTo, $result, $IsALoop ) = _HandleMachineGeneratedMail( @@ -1355,9 +1500,13 @@ sub Gateway { } # }}} - $args{'ticket'} ||= ParseTicketId( $Subject ); + $args{'ticket'} ||= ExtractTicketId( $Message ); + + # ExtractTicketId may have been overridden, and edited the Subject + my $NewSubject = Encode::decode( "UTF-8", $Message->head->get('Subject') ); + chomp $NewSubject; - $SystemTicket = RT::Ticket->new( $RT::SystemUser ); + $SystemTicket = RT::Ticket->new( RT->SystemUser ); $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ; if ( $SystemTicket->id ) { $Right = 'ReplyToTicket'; @@ -1366,7 +1515,7 @@ sub Gateway { } #Set up a queue object - my $SystemQueueObj = RT::Queue->new( $RT::SystemUser ); + my $SystemQueueObj = RT::Queue->new( RT->SystemUser ); $SystemQueueObj->Load( $args{'queue'} ); # We can safely have no queue of we have a known-good ticket @@ -1383,7 +1532,7 @@ sub Gateway { SystemQueue => $SystemQueueObj, ); - # {{{ If authentication fails and no new user was created, get out. + # If authentication fails and no new user was created, get out. if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) { # If the plugins refused to create one, they lose. @@ -1410,7 +1559,8 @@ sub Gateway { ); return ( 0, - "$ErrorsTo tried to submit a message to " + ($CurrentUser->EmailAddress || $CurrentUser->Name) + . " ($Sender) tried to submit a message to " . $args{'Queue'} . " without permission.", undef @@ -1441,9 +1591,11 @@ sub Gateway { ); } + $head->replace('X-RT-Interface' => 'Email'); + my ( $id, $Transaction, $ErrStr ) = $Ticket->Create( Queue => $SystemQueueObj->Id, - Subject => $Subject, + Subject => $NewSubject, Requestor => \@Requestors, Cc => \@Cc, MIMEObj => $Message @@ -1455,7 +1607,7 @@ sub Gateway { Explanation => $ErrStr, MIMEObj => $Message ); - return ( 0, "Ticket creation failed: $ErrStr", $Ticket ); + return ( 0, "Ticket creation From: $From failed: $ErrStr", $Ticket ); } # strip comments&corresponds from the actions we don't need @@ -1496,11 +1648,11 @@ sub Gateway { #Warn the sender that we couldn't actually submit the comment. MailError( To => $ErrorsTo, - Subject => "Message not recorded: $Subject", + Subject => "Message not recorded ($method): $Subject", Explanation => $msg, MIMEObj => $Message ); - return ( 0, "Message not recorded: $msg", $Ticket ); + return ( 0, "Message From: $From not recorded: $msg", $Ticket ); } } elsif ($unsafe_actions) { my ( $status, $msg ) = _RunUnsafeAction( @@ -1599,6 +1751,8 @@ sub _RunUnsafeAction { @_ ); + my $From = Encode::decode( "UTF-8", $args{Message}->head->get("From") ); + if ( $args{'Action'} =~ /^take$/i ) { my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id ); unless ($status) { @@ -1608,23 +1762,26 @@ sub _RunUnsafeAction { Explanation => $msg, MIMEObj => $args{'Message'} ); - return ( 0, "Ticket not taken" ); + return ( 0, "Ticket not taken, by email From: $From" ); } } elsif ( $args{'Action'} =~ /^resolve$/i ) { - my ( $status, $msg ) = $args{'Ticket'}->SetStatus('resolved'); - unless ($status) { + my $new_status = $args{'Ticket'}->FirstInactiveStatus; + if ($new_status) { + my ( $status, $msg ) = $args{'Ticket'}->SetStatus($new_status); + unless ($status) { - #Warn the sender that we couldn't actually submit the comment. - MailError( - To => $args{'ErrorsTo'}, - Subject => "Ticket not resolved", - Explanation => $msg, - MIMEObj => $args{'Message'} - ); - return ( 0, "Ticket not resolved" ); + #Warn the sender that we couldn't actually submit the comment. + MailError( + To => $args{'ErrorsTo'}, + Subject => "Ticket not resolved", + Explanation => $msg, + MIMEObj => $args{'Message'} + ); + return ( 0, "Ticket not resolved, by email From: $From" ); + } } } else { - return ( 0, "Not supported unsafe action $args{'Action'}", $args{'Ticket'} ); + return ( 0, "Not supported unsafe action $args{'Action'}, by email From: $From", $args{'Ticket'} ); } return ( 1, "Success" ); } @@ -1735,7 +1892,7 @@ sub _HandleMachineGeneratedMail { # Squelch replies if necessary # Don't let the user stuff the RT-Squelch-Replies-To header. if ( $head->get('RT-Squelch-Replies-To') ) { - $head->add( + $head->replace( 'RT-Relocated-Squelch-Replies-To', $head->get('RT-Squelch-Replies-To') ); @@ -1750,8 +1907,8 @@ sub _HandleMachineGeneratedMail { # to the scrip. We might want to notify nobody. Or just # the RT Owner. Or maybe all Privileged watchers. my ( $Sender, $junk ) = ParseSenderAddressFromHead($head); - $head->add( 'RT-Squelch-Replies-To', $Sender ); - $head->add( 'RT-DetectedAutoGenerated', 'true' ); + $head->replace( 'RT-Squelch-Replies-To', Encode::encode("UTF-8", $Sender ) ); + $head->replace( 'RT-DetectedAutoGenerated', 'true' ); } return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop ); } @@ -1772,9 +1929,21 @@ sub IsCorrectAction { return ( 1, @actions ); } -eval "require RT::Interface::Email_Vendor"; -die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Email_Vendor.pm} ); -eval "require RT::Interface::Email_Local"; -die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Email_Local.pm} ); +sub _RecordSendEmailFailure { + my $ticket = shift; + if ($ticket) { + $ticket->_RecordNote( + NoteType => 'SystemError', + Content => "Sending the previous mail has failed. Please contact your admin, they can find more details in the logs.", + ); + return 1; + } + else { + $RT::Logger->error( "Can't record send email failure as ticket is missing" ); + return; + } +} + +RT::Base->_ImportOverlays(); 1;