#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
use File::Temp;
use UNIVERSAL::require;
use Mail::Mailer ();
+use Text::ParseWords qw/shellwords/;
BEGIN {
use base 'Exporter';
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 "" ))
=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
$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
=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,
}
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' ) );
}
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
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 >/dev/null" )
- 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 );
}
@_
);
- 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'} ."'");
Bcc => undef,
From => RT->Config->Get('CorrespondAddress'),
InReplyTo => undef,
+ ExtraHeaders => {},
@_
);
$mail->head->set( $_ => Encode::encode_utf8( $args{ $_ } ) )
foreach grep defined $args{$_}, qw(To Cc Bcc From);
+ $mail->head->set( $_ => $args{ExtraHeaders}{$_} )
+ foreach keys %{ $args{ExtraHeaders} };
+
SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
return SendEmail( Entity => $mail );
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 => ''
) 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 => ''
$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
return ($value);
}
- return ($value) unless $value =~ /[^\x20-\x7e]/;
+ return ($value) if $value =~ /^(?:[\t\x20-\x7e]|\x0D*\x0A[ \t])+$/s;
$value =~ s/\s+$//;
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 ),
}
#Load the new user object
- my $CurrentUser = new RT::CurrentUser;
+ my $CurrentUser = RT::CurrentUser->new;
$CurrentUser->LoadByEmail( $Address );
unless ( $CurrentUser->id ) {
Takes a hash containing QueueObj, Head and CurrentUser objects.
Returns a list of all email addresses in the To and Cc
-headers b<except> the current Queue\'s email addresses, the CurrentUser\'s
+headers b<except> the current Queue's email addresses, the CurrentUser's
email address and anything that the configuration sub RT::IsRTAddress matches.
=cut
&& !IgnoreCcAddress( $_ )
}
map lc $user->CanonicalizeEmailAddress( $_->address ),
- map Email::Address->parse( $args{'Head'}->get( $_ ) ),
+ map RT::EmailParser->CleanupAddresses( Email::Address->parse( $args{'Head'}->get( $_ ) ) ),
qw(To Cc);
}
=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') {
+ foreach my $header ( @sender_headers ) {
my $addr_line = $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
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
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_utf8(join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
+ $mail->head->set( 'References' => Encode::encode_utf8(join ' ', @references) );
+}
+
+sub ExtractTicketId {
+ my $entity = shift;
+
+ my $subject = $entity->head->get('Subject') || '';
+ chomp $subject;
+ return ParseTicketId( $subject );
}
sub ParseTicketId {
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;
}
}
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";
}
} 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 };
}
@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 = $head->get("From");
+ chomp $From if defined $From;
my $MessageId = $head->get('Message-ID')
|| "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
my $Subject = $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(
}
# }}}
- $args{'ticket'} ||= ParseTicketId( $Subject );
+ $args{'ticket'} ||= ExtractTicketId( $Message );
- $SystemTicket = RT::Ticket->new( $RT::SystemUser );
+ # ExtractTicketId may have been overridden, and edited the Subject
+ my $NewSubject = $Message->head->get('Subject');
+ chomp $NewSubject;
+
+ $SystemTicket = RT::Ticket->new( RT->SystemUser );
$SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
if ( $SystemTicket->id ) {
$Right = 'ReplyToTicket';
}
#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
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.
);
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
);
}
+ $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
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
#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(
@_
);
+ my $From = $args{Message}->head->get("From");
+
if ( $args{'Action'} =~ /^take$/i ) {
my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
unless ($status) {
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" );
}
# 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')
);
# 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', $Sender );
+ $head->replace( 'RT-DetectedAutoGenerated', 'true' );
}
return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
}