[Rt-commit] rt branch, 4.4/multiple-reply-to, repushed
? sunnavy
sunnavy at bestpractical.com
Mon Jul 6 12:47:08 EDT 2015
The branch 4.4/multiple-reply-to was deleted and repushed:
was 4bf016c7e53eaa56ca927a5d302606df2835ae7e
now c179f586d0d2c90590d4953fe307a255949fe598
1: e0497a2 ! 1: 2164243 RT::Interface::Email doesn't need to be an Exporter
@@ -8,8 +8,8 @@
--- a/docs/UPGRADING-4.4
+++ b/docs/UPGRADING-4.4
@@
- Estimated, and Time Left will no longer be copied. This simplifies time
- reporting.
+ Custom fields with categories will be split out into hierarchical custom
+ fields.
+=item *
+
@@ -34,7 +34,6 @@
- # as well as any optionally exported functions
- @EXPORT_OK = qw(
- &CreateUser
-- &GetMessageContent
- &CheckForLoops
- &CheckForSuspiciousSender
- &CheckForAutoGenerated
2: e3399f0 ! 2: 3205741 Reorder functions to more obviously split sending and receiving mail
@@ -42,9 +42,9 @@
- # to RT instance B which sends a mail to ...
- return undef;
-}
--
++=head2 Gateway ARGSREF
+
-=head2 CheckForSuspiciousSender HEAD
-+=head2 Gateway ARGSREF
-Takes a HEAD object of L<MIME::Head> class and returns true if sender
-is suspicious. Suspicious means mailer daemon.
@@ -615,24 +615,50 @@
- $RT::Logger->info( $msgid . " No recipients found. Not sending." );
- return -1;
- }
--
++ # if plugin returns AuthStat -2 we skip action
++ # NOTE: this is experimental API and it would be changed
++ my %skip_action = ();
+
- if ($args{'Entity'}->head->get('X-RT-Squelch')) {
- $RT::Logger->info( $msgid . " Squelch header found. Not sending." );
- return -1;
- }
--
++ # Since this needs loading, no matter what
++ foreach (@{ $args{MailPlugins} }) {
++ my ($Code, $NewAuthStat);
++ if ( ref($_) eq "CODE" ) {
++ $Code = $_;
++ } else {
++ no strict 'refs';
++ $Code = *{ $_ . "::GetCurrentUser" }{CODE};
++ }
+
- if (my $precedence = RT->Config->Get('DefaultMailPrecedence')
- and !$args{'Entity'}->head->get("Precedence")
- ) {
- $args{'Entity'}->head->replace( 'Precedence', Encode::encode("UTF-8",$precedence) );
- }
--
++ foreach my $action (@{ $args{Actions} }) {
++ ( $CurrentUser, $NewAuthStat ) = $Code->(
++ Message => $args{Message},
++ RawMessageRef => $args{RawMessageRef},
++ CurrentUser => $CurrentUser,
++ AuthLevel => $AuthStat,
++ Action => $action,
++ Ticket => $args{SystemTicket},
++ Queue => $args{SystemQueue},
++ );
+
- if ( $TransactionObj && !$TicketObj
- && $TransactionObj->ObjectType eq 'RT::Ticket' )
- {
- $TicketObj = $TransactionObj->Object;
- }
--
++# You get the highest level of authentication you were assigned, unless you get the magic -1
++# If a module returns a "-1" then we discard the ticket, so.
++ $AuthStat = $NewAuthStat
++ if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
+
- my $head = $args{'Entity'}->head;
- unless ( $head->get('Date') ) {
- require RT::Date;
@@ -648,7 +674,10 @@
- # fsck.com #5959: Since RT sends 8bit mail, we should say so.
- $head->replace( 'Content-Transfer-Encoding', '8bit' );
- }
--
++ last if $AuthStat == -1;
++ $skip_action{$action}++ if $AuthStat == -2;
++ }
+
- if ( RT->Config->Get('Crypt')->{'Enable'} ) {
- %args = WillSignEncrypt(
- %args,
@@ -657,30 +686,41 @@
- );
- my $res = SignEncrypt( %args );
- return $res unless $res > 0;
-- }
--
++ # strip actions we should skip
++ @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
++ if $AuthStat == -2;
++ last unless @{$args{Actions}};
++
++ last if $AuthStat == -1;
+ }
+
- my $mail_command = RT->Config->Get('MailCommand');
--
++ return $AuthStat if !wantarray;
+
- # if it is a sub routine, we just return it;
- return $mail_command->($args{'Entity'}) if UNIVERSAL::isa( $mail_command, 'CODE' );
--
++ return ($AuthStat, $CurrentUser, $error);
++}
+
- if ( $mail_command eq 'sendmailpipe' ) {
- my $path = RT->Config->Get('SendmailPath');
- my @args = shellwords(RT->Config->Get('SendmailArguments'));
- push @args, "-t" unless grep {$_ eq "-t"} @args;
--
+
- # 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') || {};
--
++=head2 _NoAuthorizedUserFound
+
- if ($TicketObj) {
- my $Queue = $TicketObj->QueueObj;
- my $QueueAddressOverride = $Overrides->{$Queue->id}
- || $Overrides->{$Queue->Name};
--
++Emails the RT Owner and the requestor when the auth plugins return "No auth user found"
+
- if ($QueueAddressOverride) {
- $OutgoingMailAddress = $QueueAddressOverride;
- } else {
@@ -691,11 +731,20 @@
- elsif ($Overrides->{'Default'}) {
- $OutgoingMailAddress = $Overrides->{'Default'};
- }
--
++=cut
+
- push @args, "-f", $OutgoingMailAddress
- if $OutgoingMailAddress;
- }
--
++sub _NoAuthorizedUserFound {
++ my %args = (
++ Right => undef,
++ Message => undef,
++ Requestor => undef,
++ Queue => undef,
++ @_
++ );
+
- # VERP
- if ( $TransactionObj and
- my $prefix = RT->Config->Get('VERPPrefix') and
@@ -706,22 +755,49 @@
- $from =~ s/\s//g;
- push @args, "-f", "$prefix$from\@$domain";
- }
--
++ # Notify the RT Admin of the failure.
++ MailError(
++ To => RT->Config->Get('OwnerEmail'),
++ Subject => "Could not load a valid user",
++ Explanation => <<EOT,
++RT could not load a valid user, and RT's configuration does not allow
++for the creation of a new user for this email (@{[$args{Requestor}]}).
+
- eval {
- # don't ignore CHLD signal to get proper exit code
- local $SIG{'CHLD'} = 'DEFAULT';
--
++You might need to grant 'Everyone' the right '@{[$args{Right}]}' for the
++queue @{[$args{'Queue'}]}.
+
- # if something wrong with $mail->print we will get PIPE signal, handle it
- local $SIG{'PIPE'} = sub { die "program unexpectedly closed pipe" };
--
++EOT
++ MIMEObj => $args{'Message'},
++ LogLevel => 'error'
++ );
+
- require IPC::Open2;
- my ($mail, $stdout);
- my $pid = IPC::Open2::open2( $stdout, $mail, $path, @args )
- or die "couldn't execute program: $!";
--
++ # Also notify the requestor that his request has been dropped.
++ if ($args{'Requestor'} ne RT->Config->Get('OwnerEmail')) {
++ MailError(
++ To => $args{'Requestor'},
++ Subject => "Could not load a valid user",
++ Explanation => <<EOT,
++RT could not load a valid user, and RT's configuration does not allow
++for the creation of a new user for your email.
+
- $args{'Entity'}->print($mail);
- close $mail or die "close pipe failed: $!";
--
++EOT
++ MIMEObj => $args{'Message'},
++ LogLevel => 'error'
++ );
++ }
++}
+
- waitpid($pid, 0);
- if ($?) {
- # sendmail exit statuses mostly errors with data not software
@@ -739,12 +815,47 @@
- }
- return 0;
- }
-- }
-- else {
+- } elsif ( $mail_command eq 'mbox' ) {
+- my $now = RT::Date->new(RT->SystemUser);
+- $now->SetToNow;
++sub CreateUser {
++ my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_;
+
+- state $logfile;
+- unless ($logfile) {
+- my $when = $now->ISO( Timezone => "server" );
+- $when =~ s/\s+/-/g;
+- $logfile = "$RT::VarPath/$when.mbox";
+- $RT::Logger->info("Storing outgoing emails in $logfile");
+- }
++ my $NewUser = RT::User->new( RT->SystemUser );
+
+- my $fh;
+- unless (open($fh, ">>", $logfile)) {
+- $RT::Logger->crit( "Can't open mbox file $logfile: $!" );
+- return 0;
++ 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);
+ }
+- my $content = $args{Entity}->stringify;
+- $content =~ s/^(>*From )/>$1/mg;
+- print $fh "From $ENV{USER}\@localhost ".localtime."\n";
+- print $fh $content, "\n";
+- close $fh;
+- } else {
- local ($ENV{'MAILADDRESS'}, $ENV{'PERL_MAILERS'});
-+ # if plugin returns AuthStat -2 we skip action
-+ # NOTE: this is experimental API and it would be changed
-+ my %skip_action = ();
- my @mailer_args = ($mail_command);
- if ( $mail_command eq 'sendmail' ) {
@@ -756,45 +867,78 @@
- $Mail::Mailer::testfile::config{outfile} = File::Temp->new;
- $RT::Logger->info("Storing outgoing emails in $Mail::Mailer::testfile::config{outfile}");
- }
-+ # Since this needs loading, no matter what
-+ foreach (@{ $args{MailPlugins} }) {
-+ my ($Code, $NewAuthStat);
-+ if ( ref($_) eq "CODE" ) {
-+ $Code = $_;
- } else {
+- } else {
- push @mailer_args, RT->Config->Get('MailParams');
-- }
--
++ unless ( $NewUser->Id ) {
++ $NewUser->LoadByEmail($Address);
+ }
+
- unless ( $args{'Entity'}->send( @mailer_args ) ) {
- $RT::Logger->crit( "$msgid: Could not send mail." );
- if ( $TicketObj ) {
- _RecordSendEmailFailure( $TicketObj );
- }
- return 0;
-+ no strict 'refs';
-+ $Code = *{ $_ . "::GetCurrentUser" }{CODE};
++ unless ( $NewUser->Id ) {
++ MailError(
++ To => $ErrorsTo,
++ Subject => "User could not be created",
++ Explanation =>
++ "User creation failed in mailgateway: $Message",
++ MIMEObj => $entity,
++ LogLevel => 'crit',
++ );
}
-- }
+ }
- return 1;
-}
--
+
-=head2 PrepareEmailUsingTemplate Template => '', Arguments => {}
--
++ #Load the new user object
++ my $CurrentUser = RT::CurrentUser->new;
++ $CurrentUser->LoadByEmail( $Address );
+
-Loads a template. Parses it using arguments if it's not empty.
-Returns a tuple (L<RT::Template> object, error message).
++ 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 => $entity,
++ LogLevel => 'crit'
++ );
++ }
-Note that even if a template object is returned MIMEObj method
-may return undef for empty templates.
--
--=cut
--
++ return $CurrentUser;
++}
++
++
++=head2 ParseCcAddressesFromHead HASH
++
++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
++email address and anything that the configuration sub RT::IsRTAddress matches.
+
+ =cut
+
-sub PrepareEmailUsingTemplate {
-- my %args = (
++sub ParseCcAddressesFromHead {
+ my %args = (
- Template => '',
- Arguments => {},
-- @_
-- );
--
++ Head => undef,
++ QueueObj => undef,
++ CurrentUser => undef,
+ @_
+ );
+
- my $template = RT::Template->new( RT->SystemUser );
- $template->LoadGlobalTemplate( $args{'Template'} );
- unless ( $template->id ) {
@@ -804,16 +948,25 @@
-
- my ($status, $msg) = $template->Parse( %{ $args{'Arguments'} } );
- return (undef, $msg) unless $status;
--
++ my $current_address = lc $args{'CurrentUser'}->EmailAddress;
++ my $user = $args{'CurrentUser'}->UserObj;
+
- return $template;
--}
--
++ return
++ grep $_ ne $current_address && !RT::EmailParser->IsRTAddress( $_ ),
++ map lc $user->CanonicalizeEmailAddress( $_->address ),
++ map RT::EmailParser->CleanupAddresses( Email::Address->parse(
++ Encode::decode( "UTF-8", $args{'Head'}->get( $_ ) ) ) ),
++ qw(To Cc);
+ }
+
-=head2 SendEmailUsingTemplate Template => '', Arguments => {}, From => CorrespondAddress, To => '', Cc => '', Bcc => ''
--
+
-Sends email using a template, takes name of template, arguments for it and recipients.
--
+
-=cut
--
++=head2 ParseSenderAddressFromHead HEAD
+
-sub SendEmailUsingTemplate {
- my %args = (
- Template => '',
@@ -826,58 +979,104 @@
- ExtraHeaders => {},
- @_
- );
--
++Takes a MIME::Header object. Returns (user at host, friendly name, errors)
++where the first two values are the From (evaluated in order of
++Reply-To:, From:, Sender).
+
- my ($template, $msg) = PrepareEmailUsingTemplate( %args );
- return (0, $msg) unless $template;
--
++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.
+
- my $mail = $template->MIMEObj;
- unless ( $mail ) {
- $RT::Logger->info("Message is not sent as template #". $template->id ." is empty");
- return -1;
- }
--
++=cut
+
- $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ $_ } ) )
- foreach grep defined $args{$_}, qw(To Cc Bcc From);
--
++sub ParseSenderAddressFromHead {
++ my $head = shift;
++ my @sender_headers = ('Reply-To', 'From', 'Sender');
++ my @errors; # Accumulate any errors
+
- $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
- foreach keys %{ $args{ExtraHeaders} };
--
++ #Figure out who's sending this message.
++ 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, @errors) if $addr;
+
- SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
--
++ chomp $addr_line;
++ push @errors, "$header: $addr_line";
++ }
+
- return SendEmail( Entity => $mail );
--}
--
++ return (undef, undef, @errors);
+ }
+
-=head2 GetForwardFrom Ticket => undef, Transaction => undef
--
++=head2 ParseErrorsToAddressFromHead HEAD
+
-Resolve the From field to use in forward mail
--
--=cut
--
++Takes a MIME::Header object. Return a single value : user at host
++of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
++From:, Sender)
+
+ =cut
+
-sub GetForwardFrom {
- my %args = ( Ticket => undef, Transaction => undef, @_ );
- my $txn = $args{Transaction};
- my $ticket = $args{Ticket} || $txn->Object;
--
++sub ParseErrorsToAddressFromHead {
++ my $head = shift;
+
- if ( RT->Config->Get('ForwardFromUser') ) {
- return ( $txn || $ticket )->CurrentUser->EmailAddress;
- }
- else {
- return $ticket->QueueObj->CorrespondAddress
- || RT->Config->Get('CorrespondAddress');
-- }
--}
--
++ #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 = Encode::decode( "UTF-8", $head->get($header) );
++ if ($headerobj) {
++ my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
++
++ # If it's got actual useful content...
++ return ($addr) if ($addr);
++ }
+ }
+ }
+
-=head2 GetForwardAttachments Ticket => undef, Transaction => undef
--
+
-Resolve the Attachments to forward
--
--=cut
--
++
++=head2 ParseAddressFromHeader ADDRESS
++
++Takes an address from C<$head->get('Line')> and returns a tuple: user at host, friendly name
+
+ =cut
+
-sub GetForwardAttachments {
- my %args = ( Ticket => undef, Transaction => undef, @_ );
- my $txn = $args{Transaction};
- my $ticket = $args{Ticket} || $txn->Object;
--
++sub ParseAddressFromHeader {
++ my $Addr = shift;
+
- my $attachments = RT::Attachments->new( $ticket->CurrentUser );
- if ($txn) {
- $attachments->Limit( FIELD => 'TransactionId', VALUE => $txn->id );
@@ -893,24 +1092,41 @@
- }
- return $attachments;
-}
--
--
++ # Some broken mailers send: ""Vincent, Jesse"" <jesse at fsck.com>. Hate
++ $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
++ my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
+
++ my ($AddrObj) = grep ref $_, @Addresses;
++ unless ( $AddrObj ) {
++ return ( undef, undef );
++ }
+
-=head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0
--
++ return ( $AddrObj->address, $AddrObj->phrase );
++}
+
-Signs and encrypts message using L<RT::Crypt>, but as well handle errors
-with users' keys.
--
++=head2 _HandleMachineGeneratedMail
+
-If a recipient has no key or has other problems with it, then the
-unction sends a error to him using 'Error: public key' template.
-Also, notifies RT's owner using template 'Error to RT owner: public key'
-to inform that there are problems with users' keys. Then we filter
-all bad recipients and retry.
--
++Takes named params:
++ Message
++ ErrorsTo
++ Subject
+
-Returns 1 on success, 0 on error and -1 if all recipients are bad and
-had been filtered out.
--
--=cut
--
++Checks the message to see if it's a bounce, if it looks like a loop, if it's autogenerated, etc.
++Returns a triple of ("Should we continue (boolean)", "New value for $ErrorsTo", "Status message",
++"This message appears to be a loop (boolean)" );
+
+ =cut
+
-sub SignEncrypt {
- my %args = (
- Entity => undef,
@@ -919,20 +1135,28 @@
- @_
- );
- return 1 unless $args{'Sign'} || $args{'Encrypt'};
--
++sub _HandleMachineGeneratedMail {
++ my %args = ( Message => undef, ErrorsTo => undef, Subject => undef, MessageId => undef, @_ );
++ my $head = $args{'Message'}->head;
++ my $ErrorsTo = $args{'ErrorsTo'};
+
- my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
- chomp $msgid;
--
++ my $IsBounce = CheckForBounce($head);
+
- $RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
- $RT::Logger->debug("$msgid Encrypting message") if $args{'Encrypt'};
--
++ my $IsAutoGenerated = CheckForAutoGenerated($head);
+
- my %res = RT::Crypt->SignEncrypt( %args );
- return 1 unless $res{'exit_code'};
--
++ my $IsSuspiciousSender = CheckForSuspiciousSender($head);
+
- my @status = RT::Crypt->ParseStatus(
- Protocol => $res{'Protocol'}, Status => $res{'status'},
- );
--
++ my $IsALoop = CheckForLoops($head);
+
- my @bad_recipients;
- foreach my $line ( @status ) {
- # if the passphrase fails, either you have a bad passphrase
@@ -949,23 +1173,11 @@
- push @bad_recipients, $line;
- }
- return 0 unless @bad_recipients;
-+ foreach my $action (@{ $args{Actions} }) {
-+ ( $CurrentUser, $NewAuthStat ) = $Code->(
-+ Message => $args{Message},
-+ RawMessageRef => $args{RawMessageRef},
-+ CurrentUser => $CurrentUser,
-+ AuthLevel => $AuthStat,
-+ Action => $action,
-+ Ticket => $args{SystemTicket},
-+ Queue => $args{SystemQueue},
-+ );
++ my $SquelchReplies = 0;
- $_->{'AddressObj'} = (Email::Address->parse( $_->{'Recipient'} ))[0]
- foreach @bad_recipients;
-+# You get the highest level of authentication you were assigned, unless you get the magic -1
-+# If a module returns a "-1" then we discard the ticket, so.
-+ $AuthStat = $NewAuthStat
-+ if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
++ my $owner_mail = RT->Config->Get('OwnerEmail');
- foreach my $recipient ( @bad_recipients ) {
- my $status = SendEmailUsingTemplate(
@@ -979,11 +1191,14 @@
- );
- unless ( $status ) {
- $RT::Logger->error("Couldn't send 'Error: public key'");
-+ last if $AuthStat == -1;
-+ $skip_action{$action}++ if $AuthStat == -2;
- }
-- }
--
+- }
++ #If the message is autogenerated, we need to know, so we can not
++ # send mail to the sender
++ if ( $IsBounce || $IsSuspiciousSender || $IsAutoGenerated || $IsALoop ) {
++ $SquelchReplies = 1;
++ $ErrorsTo = $owner_mail;
+ }
+
- my $status = SendEmailUsingTemplate(
- To => RT->Config->Get('OwnerEmail'),
- Template => 'Error to RT owner: public key',
@@ -996,15 +1211,23 @@
- unless ( $status ) {
- $RT::Logger->error("Couldn't send 'Error to RT owner: public key'");
- }
++ # Warn someone if it's a loop, before we drop it on the ground
++ if ($IsALoop) {
++ $RT::Logger->crit("RT Received mail (".$args{MessageId}.") from itself.");
- DeleteRecipientsFromHead(
- $args{'Entity'}->head,
- map $_->{'AddressObj'}->address, @bad_recipients
- );
-+ # strip actions we should skip
-+ @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
-+ if $AuthStat == -2;
-+ last unless @{$args{Actions}};
++ #Should we mail it to RTOwner?
++ if ( RT->Config->Get('LoopsToRTOwner') ) {
++ MailError(
++ To => $owner_mail,
++ Subject => "RT Bounce: ".$args{'Subject'},
++ Explanation => "RT thinks this message may be a bounce",
++ MIMEObj => $args{Message}
++ );
++ }
- unless ( $args{'Entity'}->head->get('To')
- || $args{'Entity'}->head->get('Cc')
@@ -1012,53 +1235,87 @@
- {
- $RT::Logger->debug("$msgid No recipients that have public key, not sending");
- return -1;
-+ last if $AuthStat == -1;
++ #Do we actually want to store it?
++ return ( 0, $ErrorsTo, "Message Bounced", $IsALoop )
++ unless RT->Config->Get('StoreLoops');
}
- # redo without broken recipients
- %res = RT::Crypt->SignEncrypt( %args );
- return 0 if $res{'exit_code'};
-+ return $AuthStat if !wantarray;
++ # Squelch replies if necessary
++ # Don't let the user stuff the RT-Squelch-Replies-To header.
++ if ( $head->get('RT-Squelch-Replies-To') ) {
++ $head->replace(
++ 'RT-Relocated-Squelch-Replies-To',
++ $head->get('RT-Squelch-Replies-To')
++ );
++ $head->delete('RT-Squelch-Replies-To');
++ }
- return 1;
-+ return ($AuthStat, $CurrentUser, $error);
++ if ($SquelchReplies) {
++
++ # Squelch replies to the sender, and also leave a clue to
++ # allow us to squelch ALL outbound messages. This way we
++ # can punt the logic of "what to do when we get a bounce"
++ # 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->replace( 'RT-Squelch-Replies-To', Encode::encode("UTF-8", $Sender ) );
++ $head->replace( 'RT-DetectedAutoGenerated', 'true' );
++ }
++ return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
}
-use MIME::Words ();
--
++=head2 CheckForLoops HEAD
+
-=head2 EncodeToMIME
--
++Takes a HEAD object of L<MIME::Head> class and returns true if the
++message's been sent by this RT instance. Uses "X-RT-Loop-Prevention"
++field of the head for test.
+
-Takes a hash with a String and a Charset. Returns the string encoded
-according to RFC2047, using B (base64 based) encoding.
--
++=cut
+
-String must be a perl string, octets are returned.
-+=head2 _NoAuthorizedUserFound
++sub CheckForLoops {
++ my $head = shift;
-If Charset is not provided then $EmailOutputEncoding config option
-is used, or "latin-1" if that is not set.
-+Emails the RT Owner and the requestor when the auth plugins return "No auth user found"
-
- =cut
++ # If this instance of RT sent it our, we don't want to take it in
++ 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;
++ }
+
+-=cut
++ # 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 EncodeToMIME {
-+sub _NoAuthorizedUserFound {
- my %args = (
+- my %args = (
- String => undef,
- Charset => undef,
-+ Right => undef,
-+ Message => undef,
-+ Requestor => undef,
-+ Queue => undef,
- @_
- );
+- @_
+- );
- my $value = $args{'String'};
- return $value unless $value; # 0 is perfect ascii
- my $charset = $args{'Charset'} || RT->Config->Get('EmailOutputEncoding');
- my $encoding = 'B';
--
++=head2 CheckForSuspiciousSender HEAD
+
- # using RFC2047 notation, sec 2.
- # encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
--
++Takes a HEAD object of L<MIME::Head> class and returns true if sender
++is suspicious. Suspicious means mailer daemon.
+
- # An 'encoded-word' may not be more than 75 characters long
- #
- # MIME encoding increases 4/3*(number of bytes), and always in multiples
@@ -1073,40 +1330,41 @@
- ) / 4
- );
- $max = int( $max / 3 ) * 3;
--
++See also L</ParseSenderAddressFromHead>.
+
- chomp $value;
++=cut
- if ( $max <= 0 ) {
-+ # Notify the RT Admin of the failure.
-+ MailError(
-+ To => RT->Config->Get('OwnerEmail'),
-+ Subject => "Could not load a valid user",
-+ Explanation => <<EOT,
-+RT could not load a valid user, and RT's configuration does not allow
-+for the creation of a new user for this email (@{[$args{Requestor}]}).
++sub CheckForSuspiciousSender {
++ my $head = shift;
- # gives an error...
- $RT::Logger->crit("Can't encode! Charset or encoding too big.");
- return ($value);
- }
-+You might need to grant 'Everyone' the right '@{[$args{Right}]}' for the
-+queue @{[$args{'Queue'}]}.
++ #if it's from a postmaster or mailer daemon, it's likely a bounce.
- return ($value) if $value =~ /^(?:[\t\x20-\x7e]|\x0D*\x0A[ \t])+$/s;
-+EOT
-+ MIMEObj => $args{'Message'},
-+ LogLevel => 'error'
-+ );
++ #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.
- $value =~ s/\s+$//;
-+ # Also notify the requestor that his request has been dropped.
-+ if ($args{'Requestor'} ne RT->Config->Get('OwnerEmail')) {
-+ MailError(
-+ To => $args{'Requestor'},
-+ Subject => "Could not load a valid user",
-+ Explanation => <<EOT,
-+RT could not load a valid user, and RT's configuration does not allow
-+for the creation of a new user for your email.
++ #TODO: search through the whole email and find the right Ticket ID.
++
++ 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 "" ))
++ {
++ return (1);
- my ( $tmp, @chunks ) = ( '', () );
- while ( length $value ) {
@@ -1117,356 +1375,94 @@
- $tmp = '';
- }
- $tmp .= $octets;
-+EOT
-+ MIMEObj => $args{'Message'},
-+ LogLevel => 'error'
-+ );
}
- push @chunks, $tmp if length $tmp;
--
+
- # encode an join chuncks
- $value = join "\n ",
- map MIME::Words::encode_mimeword( $_, $encoding, $charset ),
- @chunks;
- return ($value);
- }
-
- sub CreateUser {
-@@
- }
-
-
--
- =head2 ParseCcAddressesFromHead HASH
-
- Takes a hash containing QueueObj, Head and CurrentUser objects.
-@@
- push @errors, "$header: $addr_line";
- }
-
-- return (undef, undef, @errors);
-+ return (undef, undef, @errors);
-+}
-+
-+=head2 ParseErrorsToAddressFromHead HEAD
-+
-+Takes a MIME::Header object. Return a single value : user at host
-+of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
-+From:, Sender)
-+
-+=cut
-+
-+sub ParseErrorsToAddressFromHead {
-+ my $head = 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 = Encode::decode( "UTF-8", $head->get($header) );
-+ if ($headerobj) {
-+ my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
-+
-+ # If it's got actual useful content...
-+ return ($addr) if ($addr);
-+ }
-+ }
-+}
-+
-+
-+
-+=head2 ParseAddressFromHeader ADDRESS
-+
-+Takes an address from C<$head->get('Line')> and returns a tuple: user at host, friendly name
-+
-+=cut
-+
-+sub ParseAddressFromHeader {
-+ my $Addr = shift;
-+
-+ # Some broken mailers send: ""Vincent, Jesse"" <jesse at fsck.com>. Hate
-+ $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
-+ my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
-+
-+ my ($AddrObj) = grep ref $_, @Addresses;
-+ unless ( $AddrObj ) {
-+ return ( undef, undef );
-+ }
-+
-+ return ( $AddrObj->address, $AddrObj->phrase );
-+}
-+
-+=head2 _HandleMachineGeneratedMail
-+
-+Takes named params:
-+ Message
-+ ErrorsTo
-+ Subject
-+
-+Checks the message to see if it's a bounce, if it looks like a loop, if it's autogenerated, etc.
-+Returns a triple of ("Should we continue (boolean)", "New value for $ErrorsTo", "Status message",
-+"This message appears to be a loop (boolean)" );
-+
-+=cut
-+
-+sub _HandleMachineGeneratedMail {
-+ my %args = ( Message => undef, ErrorsTo => undef, Subject => undef, MessageId => undef, @_ );
-+ my $head = $args{'Message'}->head;
-+ my $ErrorsTo = $args{'ErrorsTo'};
-+
-+ my $IsBounce = CheckForBounce($head);
-+
-+ my $IsAutoGenerated = CheckForAutoGenerated($head);
-+
-+ my $IsSuspiciousSender = CheckForSuspiciousSender($head);
-+
-+ my $IsALoop = CheckForLoops($head);
-+
-+ my $SquelchReplies = 0;
-+
-+ my $owner_mail = RT->Config->Get('OwnerEmail');
-+
-+ #If the message is autogenerated, we need to know, so we can not
-+ # send mail to the sender
-+ if ( $IsBounce || $IsSuspiciousSender || $IsAutoGenerated || $IsALoop ) {
-+ $SquelchReplies = 1;
-+ $ErrorsTo = $owner_mail;
-+ }
-+
-+ # Warn someone if it's a loop, before we drop it on the ground
-+ if ($IsALoop) {
-+ $RT::Logger->crit("RT Received mail (".$args{MessageId}.") from itself.");
-+
-+ #Should we mail it to RTOwner?
-+ if ( RT->Config->Get('LoopsToRTOwner') ) {
-+ MailError(
-+ To => $owner_mail,
-+ Subject => "RT Bounce: ".$args{'Subject'},
-+ Explanation => "RT thinks this message may be a bounce",
-+ MIMEObj => $args{Message}
-+ );
-+ }
-+
-+ #Do we actually want to store it?
-+ return ( 0, $ErrorsTo, "Message Bounced", $IsALoop )
-+ unless RT->Config->Get('StoreLoops');
-+ }
-+
-+ # Squelch replies if necessary
-+ # Don't let the user stuff the RT-Squelch-Replies-To header.
-+ if ( $head->get('RT-Squelch-Replies-To') ) {
-+ $head->replace(
-+ 'RT-Relocated-Squelch-Replies-To',
-+ $head->get('RT-Squelch-Replies-To')
-+ );
-+ $head->delete('RT-Squelch-Replies-To');
-+ }
-+
-+ if ($SquelchReplies) {
-+
-+ # Squelch replies to the sender, and also leave a clue to
-+ # allow us to squelch ALL outbound messages. This way we
-+ # can punt the logic of "what to do when we get a bounce"
-+ # 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->replace( 'RT-Squelch-Replies-To', Encode::encode("UTF-8", $Sender ) );
-+ $head->replace( 'RT-DetectedAutoGenerated', 'true' );
-+ }
-+ return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
-+}
-+
-+=head2 CheckForLoops HEAD
-+
-+Takes a HEAD object of L<MIME::Head> class and returns true if the
-+message's been sent by this RT instance. Uses "X-RT-Loop-Prevention"
-+field of the head for test.
-+
-+=cut
-+
-+sub CheckForLoops {
-+ my $head = shift;
-+
-+ # If this instance of RT sent it our, we don't want to take it in
-+ 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;
-+ }
-+
-+ # 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 ParseErrorsToAddressFromHead HEAD
-+=head2 CheckForSuspiciousSender HEAD
-
--Takes a MIME::Header object. Return a single value : user at host
--of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
--From:, Sender)
-+Takes a HEAD object of L<MIME::Head> class and returns true if sender
-+is suspicious. Suspicious means mailer daemon.
-+
-+See also L</ParseSenderAddressFromHead>.
-
- =cut
-
--sub ParseErrorsToAddressFromHead {
-+sub CheckForSuspiciousSender {
- my $head = 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 = Encode::decode( "UTF-8", $head->get($header) );
-- if ($headerobj) {
-- my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
--
-- # If it's got actual useful content...
-- return ($addr) if ($addr);
-- }
-- }
--}
--
--
-+ #if it's from a postmaster or mailer daemon, it's likely a bounce.
-
--=head2 ParseAddressFromHeader ADDRESS
-+ #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.
-
--Takes an address from C<$head->get('Line')> and returns a tuple: user at host, friendly name
-+ #TODO: search through the whole email and find the right Ticket ID.
-
--=cut
-+ my ( $From, $junk ) = ParseSenderAddressFromHead($head);
-
--sub ParseAddressFromHeader {
-- my $Addr = shift;
-+ # If unparseable (non-ASCII), $From can come back undef
-+ return undef if not defined $From;
-
-- # Some broken mailers send: ""Vincent, Jesse"" <jesse at fsck.com>. Hate
-- $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
-- my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
-+ if ( ( $From =~ /^mailer-daemon\@/i )
-+ or ( $From =~ /^postmaster\@/i )
-+ or ( $From eq "" ))
-+ {
-+ return (1);
-
-- my ($AddrObj) = grep ref $_, @Addresses;
-- unless ( $AddrObj ) {
-- return ( undef, undef );
- }
-
-- return ( $AddrObj->address, $AddrObj->phrase );
-+ return undef;
- }
-
--=head2 DeleteRecipientsFromHead HEAD RECIPIENTS
+-sub CreateUser {
+- my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_;
+=head2 CheckForAutoGenerated HEAD
--Gets a head object and list of addresses.
--Deletes addresses from To, Cc or Bcc fields.
+- my $NewUser = RT::User->new( RT->SystemUser );
+Takes a HEAD object of L<MIME::Head> class and returns true if message is
+autogenerated. Checks C<Precedence>, C<Auto-Submitted>, and
+C<X-FC-Machinegenerated> fields of the head in tests.
- =cut
-
--sub DeleteRecipientsFromHead {
+- my ( $Val, $Message ) = $NewUser->Create(
+- Name => ( $Username || $Address ),
+- EmailAddress => $Address,
+- RealName => $Name,
+- Password => undef,
+- Privileged => 0,
+- Comments => 'Autocreated on ticket submission',
+- );
++=cut
+
+- unless ($Val) {
+sub CheckForAutoGenerated {
- my $head = shift;
-- my %skip = map { lc $_ => 1 } @_;
-
-- foreach my $field ( qw(To Cc Bcc) ) {
-- $head->replace( $field => Encode::encode( "UTF-8",
-- join ', ', map $_->format, grep !$skip{ lc $_->address },
-- Email::Address->parse( Encode::decode( "UTF-8", $head->get( $field ) ) ) )
-- );
++ my $head = shift;
+
+- # Deal with the race condition of two account creations at once
+- if ($Username) {
+- $NewUser->LoadByName($Username);
+- }
+ if (grep { /^(bulk|junk)/i } $head->get_all("Precedence")) {
+ return (1);
- }
--}
--
--sub GenMessageId {
-- my %args = (
-- Ticket => undef,
-- Scrip => undef,
-- ScripAction => undef,
-- @_
-- );
-- my $org = RT->Config->Get('Organization');
-- my $ticket_id = ( ref $args{'Ticket'}? $args{'Ticket'}->id : $args{'Ticket'} ) || 0;
-- my $scrip_id = ( ref $args{'Scrip'}? $args{'Scrip'}->id : $args{'Scrip'} ) || 0;
-- my $sent = ( ref $args{'ScripAction'}? $args{'ScripAction'}->{'_Message_ID'} : 0 ) || 0;
--
-- return "<rt-". $RT::VERSION ."-". $$ ."-". CORE::time() ."-". int(rand(2000)) .'.'
-- . $ticket_id ."-". $scrip_id ."-". $sent ."@". $org .">" ;
--}
--
--sub SetInReplyTo {
-- my %args = (
-- Message => undef,
-- InReplyTo => undef,
-- Ticket => undef,
-- @_
-- );
-- return unless $args{'Message'} && $args{'InReplyTo'};
--
-- my $get_header = sub {
-- my @res;
-- if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
-- @res = map {Encode::decode("UTF-8", $_)} $args{'InReplyTo'}->head->get( shift );
-- } else {
-- @res = $args{'InReplyTo'}->GetHeader( shift ) || '';
++ }
+
+- unless ( $NewUser->Id ) {
+- $NewUser->LoadByEmail($Address);
- }
-- return grep length, map { split /\s+/m, $_ } grep defined, @res;
-- };
-
-- my @id = $get_header->('Message-ID');
-- #XXX: custom header should begin with X- otherwise is violation of the standard
-- my @rtid = $get_header->('RT-Message-ID');
-- my @references = $get_header->('References');
-- unless ( @references ) {
-- @references = $get_header->('In-Reply-To');
+ # Per RFC3834, any Auto-Submitted header which is not "no" means
+ # it is auto-generated.
+ my $AutoSubmitted = $head->get("Auto-Submitted") || "";
+ if ( length $AutoSubmitted and $AutoSubmitted ne "no" ) {
+ return (1);
- }
-- push @references, @id, @rtid;
-- if ( $args{'Ticket'} ) {
-- my $pseudo_ref = PseudoReference( $args{'Ticket'} );
-- push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references;
-+
++ }
+
+- unless ( $NewUser->Id ) {
+- MailError(
+- To => $ErrorsTo,
+- Subject => "User could not be created",
+- Explanation =>
+- "User creation failed in mailgateway: $Message",
+- MIMEObj => $entity,
+- LogLevel => 'crit',
+- );
+- }
+ # First Class mailer uses this as a clue.
+ my $FCJunk = $head->get("X-FC-Machinegenerated") || "";
+ if ( $FCJunk =~ /^true/i ) {
+ return (1);
}
-- splice @references, 4, -6
-- if @references > 10;
-
-- my $mail = $args{'Message'};
-- $mail->head->replace( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
-- $mail->head->replace( 'References' => Encode::encode( "UTF-8", join ' ', @references) );
+
+- #Load the new user object
+- my $CurrentUser = RT::CurrentUser->new;
+- $CurrentUser->LoadByEmail( $Address );
+ return (0);
- }
-
--sub PseudoReference {
-- my $ticket = shift;
-- return '<RT-Ticket-'. $ticket->id .'@'. RT->Config->Get('Organization') .'>';
-+
++}
+
+- 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 => $entity,
+- LogLevel => 'crit'
+- );
+- }
+
+- return $CurrentUser;
+sub CheckForBounce {
+ my $head = shift;
+
@@ -1474,18 +1470,116 @@
+ return ( $ReturnPath =~ /<>/ );
}
- =head2 ExtractTicketId
-@@
- return $id;
+
++=head2 ExtractTicketId
+
+-=head2 ParseCcAddressesFromHead HASH
++Passed a MIME::Entity. Returns a ticket id or undef to signal 'new ticket'.
+
+-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
+-email address and anything that the configuration sub RT::IsRTAddress matches.
++This is a great entry point if you need to customize how ticket ids are
++handled for your site. RT-Extension-RepliesToResolved demonstrates one
++possible use for this extension.
+
+-=cut
++If the Subject of this ticket is modified, it will be reloaded by the
++mail gateway code before Ticket creation.
+
+-sub ParseCcAddressesFromHead {
+- my %args = (
+- Head => undef,
+- QueueObj => undef,
+- CurrentUser => undef,
+- @_
+- );
++=cut
+
+- my $current_address = lc $args{'CurrentUser'}->EmailAddress;
+- my $user = $args{'CurrentUser'}->UserObj;
++sub ExtractTicketId {
++ my $entity = shift;
+
+- return
+- grep $_ ne $current_address && !RT::EmailParser->IsRTAddress( $_ ),
+- map lc $user->CanonicalizeEmailAddress( $_->address ),
+- map RT::EmailParser->CleanupAddresses( Email::Address->parse(
+- Encode::decode( "UTF-8", $args{'Head'}->get( $_ ) ) ) ),
+- qw(To Cc);
++ my $subject = Encode::decode( "UTF-8", $entity->head->get('Subject') || '' );
++ chomp $subject;
++ return ParseTicketId( $subject );
}
--sub AddSubjectTag {
-- my $subject = shift;
-- my $ticket = shift;
-- unless ( ref $ticket ) {
-- my $tmp = RT::Ticket->new( RT->SystemUser );
-- $tmp->Load( $ticket );
-- $ticket = $tmp;
++=head2 ParseTicketId
+
++Takes a string and searches for [subjecttag #id]
+
+-=head2 ParseSenderAddressFromHead HEAD
+-
+-Takes a MIME::Header object. Returns (user at 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.
++Returns the id if a match is found. Otherwise returns undef.
+
+ =cut
+
+-sub ParseSenderAddressFromHead {
+- my $head = shift;
+- my @sender_headers = ('Reply-To', 'From', 'Sender');
+- my @errors; # Accumulate any errors
++sub ParseTicketId {
++ my $Subject = shift;
+
+- #Figure out who's sending this message.
+- 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, @errors) if $addr;
++ my $rtname = RT->Config->Get('rtname');
++ my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;
+
+- chomp $addr_line;
+- push @errors, "$header: $addr_line";
++ # We use @captures and pull out the last capture value to guard against
++ # someone using (...) instead of (?:...) in $EmailSubjectTagRegex.
++ my $id;
++ if ( my @captures = $Subject =~ /\[$test_name\s+\#(\d+)\s*\]/i ) {
++ $id = $captures[-1];
++ } else {
++ foreach my $tag ( RT->System->SubjectTag ) {
++ next unless my @captures = $Subject =~ /\[\Q$tag\E\s+\#(\d+)\s*\]/i;
++ $id = $captures[-1];
++ last;
++ }
+ }
++ return undef unless $id;
+
+- return (undef, undef, @errors);
++ $RT::Logger->debug("Found a ticket ID. It's $id");
++ return $id;
+ }
+
+-=head2 ParseErrorsToAddressFromHead HEAD
+-
+-Takes a MIME::Header object. Return a single value : user at host
+-of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
+-From:, Sender)
+-
+-=cut
+-
+-sub ParseErrorsToAddressFromHead {
+- my $head = shift;
+-
+- #Figure out who's sending this message.
+sub _RunUnsafeAction {
+ my %args = (
+ Action => undef,
@@ -1495,9 +1589,14 @@
+ CurrentUser => undef,
+ @_
+ );
-+
+
+- foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) {
+ my $From = Encode::decode( "UTF-8", $args{Message}->head->get("From") );
-+
+
+- # If there's a header of that name
+- my $headerobj = Encode::decode( "UTF-8", $head->get($header) );
+- if ($headerobj) {
+- my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
+ if ( $args{'Action'} =~ /^take$/i ) {
+ my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
+ unless ($status) {
@@ -1514,7 +1613,9 @@
+ if ($new_status) {
+ my ( $status, $msg ) = $args{'Ticket'}->SetStatus($new_status);
+ unless ($status) {
-+
+
+- # If it's got actual useful content...
+- return ($addr) if ($addr);
+ #Warn the sender that we couldn't actually submit the comment.
+ MailError(
+ To => $args{'ErrorsTo'},
@@ -1524,51 +1625,76 @@
+ );
+ return ( 0, "Ticket not resolved, by email From: $From" );
+ }
-+ }
+ }
+ } else {
+ return ( 0, "Not supported unsafe action $args{'Action'}, by email From: $From", $args{'Ticket'} );
}
-- my $id = $ticket->id;
-- my $queue_tag = $ticket->QueueObj->SubjectTag;
+ return ( 1, "Success" );
-+}
-
-- my $tag_re = RT->Config->Get('EmailSubjectTagRegex');
-- unless ( $tag_re ) {
-- my $tag = $queue_tag || RT->Config->Get('rtname');
-- $tag_re = qr/\Q$tag\E/;
-- } elsif ( $queue_tag ) {
-- $tag_re = qr/$tag_re|\Q$queue_tag\E/;
+ }
+
+=head2 MailError PARAM HASH
-+
+
+Sends an error message. Takes a param hash:
-+
+
+-=head2 ParseAddressFromHeader ADDRESS
+-
+-Takes an address from C<$head->get('Line')> and returns a tuple: user at host, friendly name
+=over 4
-+
+
+-=cut
+=item From - sender's address, by default is 'CorrespondAddress';
-+
+
+-sub ParseAddressFromHeader {
+- my $Addr = shift;
+=item To - recipient, by default is 'OwnerEmail';
-+
+
+- # Some broken mailers send: ""Vincent, Jesse"" <jesse at fsck.com>. Hate
+- $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
+- my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
+=item Bcc - optional Bcc recipients;
-+
+
+- my ($AddrObj) = grep ref $_, @Addresses;
+- unless ( $AddrObj ) {
+- return ( undef, undef );
+- }
+=item Subject - subject of the message, default is 'There has been an error';
-+
+
+- return ( $AddrObj->address, $AddrObj->phrase );
+-}
+=item Explanation - main content of the error, default value is 'Unexplained error';
-+
+
+-=head2 DeleteRecipientsFromHead HEAD RECIPIENTS
+=item MIMEObj - optional MIME entity that's attached to the error mail, as well we
+add 'In-Reply-To' field to the error that points to this message.
-+
+
+-Gets a head object and list of addresses.
+-Deletes addresses from To, Cc or Bcc fields.
+=item Attach - optional text that attached to the error as 'message/rfc822' part.
-+
+
+-=cut
+=item LogLevel - log level under which we should write the subject and
+explanation message into the log, by default we log it as critical.
-+
+
+-sub DeleteRecipientsFromHead {
+- my $head = shift;
+- my %skip = map { lc $_ => 1 } @_;
+=back
-+
+
+- foreach my $field ( qw(To Cc Bcc) ) {
+- $head->replace( $field => Encode::encode( "UTF-8",
+- join ', ', map $_->format, grep !$skip{ lc $_->address },
+- Email::Address->parse( Encode::decode( "UTF-8", $head->get( $field ) ) ) )
+- );
+- }
+-}
+=cut
-+
+
+-sub GenMessageId {
+sub MailError {
-+ my %args = (
+ my %args = (
+- Ticket => undef,
+- Scrip => undef,
+- ScripAction => undef,
+ To => RT->Config->Get('OwnerEmail'),
+ Bcc => undef,
+ From => RT->Config->Get('CorrespondAddress'),
@@ -1577,14 +1703,27 @@
+ MIMEObj => undef,
+ Attach => undef,
+ LogLevel => 'crit',
-+ @_
-+ );
-+
+ @_
+ );
+- my $org = RT->Config->Get('Organization');
+- my $ticket_id = ( ref $args{'Ticket'}? $args{'Ticket'}->id : $args{'Ticket'} ) || 0;
+- my $scrip_id = ( ref $args{'Scrip'}? $args{'Scrip'}->id : $args{'Scrip'} ) || 0;
+- my $sent = ( ref $args{'ScripAction'}? $args{'ScripAction'}->{'_Message_ID'} : 0 ) || 0;
+
+- return "<rt-". $RT::VERSION ."-". $$ ."-". CORE::time() ."-". int(rand(2000)) .'.'
+- . $ticket_id ."-". $scrip_id ."-". $sent ."@". $org .">" ;
+-}
+ $RT::Logger->log(
+ level => $args{'LogLevel'},
+ message => "$args{Subject}: $args{'Explanation'}",
+ ) if $args{'LogLevel'};
-+
+
+-sub SetInReplyTo {
+- my %args = (
+- Message => undef,
+- InReplyTo => undef,
+- Ticket => undef,
+- @_
+ # the colons are necessary to make ->build include non-standard headers
+ my %entity_args = (
+ Type => "multipart/mixed",
@@ -1593,99 +1732,166 @@
+ To => Encode::encode( "UTF-8", $args{'To'} ),
+ Subject => EncodeToMIME( String => $args{'Subject'} ),
+ 'X-RT-Loop-Prevention:' => Encode::encode( "UTF-8", RT->Config->Get('rtname') ),
-+ );
-+
+ );
+- return unless $args{'Message'} && $args{'InReplyTo'};
+-
+- my $get_header = sub {
+- my @res;
+- if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
+- @res = map {Encode::decode("UTF-8", $_)} $args{'InReplyTo'}->head->get( shift );
+- } else {
+- @res = $args{'InReplyTo'}->GetHeader( shift ) || '';
+- }
+- return grep length, map { split /\s+/m, $_ } grep defined, @res;
+- };
+
+- my @id = $get_header->('Message-ID');
+- #XXX: custom header should begin with X- otherwise is violation of the standard
+- my @rtid = $get_header->('RT-Message-ID');
+- my @references = $get_header->('References');
+- unless ( @references ) {
+- @references = $get_header->('In-Reply-To');
+- }
+- push @references, @id, @rtid;
+- if ( $args{'Ticket'} ) {
+- my $pseudo_ref = PseudoReference( $args{'Ticket'} );
+- push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references;
+ # 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') );
}
-- return $subject if $subject =~ /\[$tag_re\s+#$id\]/;
-
-- $subject =~ s/(\r\n|\n|\s)/ /g;
-- chomp $subject;
-- return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject";
+- splice @references, 4, -6
+- if @references > 10;
+
+- my $mail = $args{'Message'};
+- $mail->head->replace( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
+- $mail->head->replace( 'References' => Encode::encode( "UTF-8", join ' ', @references) );
-}
+ my $entity = MIME::Entity->build(%entity_args);
+ SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} );
+-sub PseudoReference {
+- my $ticket = shift;
+- return '<RT-Ticket-'. $ticket->id .'@'. RT->Config->Get('Organization') .'>';
+-}
+ $entity->attach(
+ Type => "text/plain",
+ Charset => "UTF-8",
+ Data => Encode::encode( "UTF-8", $args{'Explanation'} . "\n" ),
+ );
--=head2 Gateway ARGSREF
+-=head2 ExtractTicketId
+ if ( $args{'MIMEObj'} ) {
+ $args{'MIMEObj'}->sync_headers;
+ $entity->add_part( $args{'MIMEObj'} );
+ }
+-Passed a MIME::Entity. Returns a ticket id or undef to signal 'new ticket'.
+ if ( $args{'Attach'} ) {
+ $entity->attach( Data => Encode::encode( "UTF-8", $args{'Attach'} ), Type => 'message/rfc822' );
--Takes parameters:
-+ }
-
-- action
-- queue
-- message
+-This is a great entry point if you need to customize how ticket ids are
+-handled for your site. RT-Extension-RepliesToResolved demonstrates one
+-possible use for this extension.
++ }
+
+-If the Subject of this ticket is modified, it will be reloaded by the
+-mail gateway code before Ticket creation.
+ SendEmail( Entity => $entity, Bounce => 1 );
+}
-
--This performs all the "guts" of the mail rt-mailgate program, and is
--designed to be called from the web interface with a message, user
--object, and so on.
+-=cut
+
+-sub ExtractTicketId {
+- my $entity = shift;
+=head2 SendEmail Entity => undef, [ Bounce => 0, Ticket => undef, Transaction => undef ]
--Can also take an optional 'ticket' parameter; this ticket id overrides
--any ticket id found in the subject.
+- my $subject = Encode::decode( "UTF-8", $entity->head->get('Subject') || '' );
+- chomp $subject;
+- return ParseTicketId( $subject );
+-}
+Sends an email (passed as a L<MIME::Entity> object C<ENTITY>) using
+RT's outgoing mail configuration. If C<BOUNCE> 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.
--Returns:
+-=head2 ParseTicketId
+If the C<X-RT-Squelch> header is set to any true value, the mail will
+not be sent. One use is to let extensions easily cancel outgoing mail.
-- An array of:
+-Takes a string and searches for [subjecttag #id]
+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.
-- (status code, message, optional ticket object)
+-Returns the id if a match is found. Otherwise returns undef.
+Returns 1 on success, 0 on error or -1 if message has no recipients
+and hasn't been sent.
-- status code is a numeric value.
+-=cut
+=head3 Signing and Encrypting
-- for temporary failures, the status code should be -75
+-sub ParseTicketId {
+- my $Subject = shift;
+This function as well signs and/or encrypts the message according to
+headers of a transaction's attachment or properties of a ticket's queue.
+To get full access to the configuration Ticket and/or Transaction
+arguments must be provided, but you can force behaviour using Sign
+and/or Encrypt arguments.
-- for permanent failures which are handled by RT, the status code
-- should be 0
+- my $rtname = RT->Config->Get('rtname');
+- my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;
+The following precedence of arguments are used to figure out if
+the message should be encrypted and/or signed:
-- for succces, the status code should be 1
+- # We use @captures and pull out the last capture value to guard against
+- # someone using (...) instead of (?:...) in $EmailSubjectTagRegex.
+- my $id;
+- if ( my @captures = $Subject =~ /\[$test_name\s+\#(\d+)\s*\]/i ) {
+- $id = $captures[-1];
+- } else {
+- foreach my $tag ( RT->System->SubjectTag ) {
+- next unless my @captures = $Subject =~ /\[\Q$tag\E\s+\#(\d+)\s*\]/i;
+- $id = $captures[-1];
+- last;
+- }
+- }
+- return undef unless $id;
+* if Sign or Encrypt argument is defined then its value is used
+- $RT::Logger->debug("Found a ticket ID. It's $id");
+- return $id;
+-}
+* else if Transaction's first attachment has X-RT-Sign or X-RT-Encrypt
+header field then it's value is used
+-sub AddSubjectTag {
+- my $subject = shift;
+- my $ticket = shift;
+- unless ( ref $ticket ) {
+- my $tmp = RT::Ticket->new( RT->SystemUser );
+- $tmp->Load( $ticket );
+- $ticket = $tmp;
+- }
+- my $id = $ticket->id;
+- my $queue_tag = $ticket->QueueObj->SubjectTag;
+* else properties of a queue of the Ticket are used.
- =cut
-
--sub _LoadPlugins {
-- my @mail_plugins = @_;
+- my $tag_re = RT->Config->Get('EmailSubjectTagRegex');
+- unless ( $tag_re ) {
+- my $tag = $queue_tag || RT->Config->Get('rtname');
+- $tag_re = qr/\Q$tag\E/;
+- } elsif ( $queue_tag ) {
+- $tag_re = qr/$tag_re|\Q$queue_tag\E/;
+- }
+- return $subject if $subject =~ /\[$tag_re\s+#$id\]/;
++=cut
+
+- $subject =~ s/(\r\n|\n|\s)/ /g;
+- chomp $subject;
+- return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject";
+-}
+sub SendEmail {
+ my (%args) = (
+ Entity => undef,
@@ -1694,6 +1900,150 @@
+ Transaction => undef,
+ @_,
+ );
+
++ my $TicketObj = $args{'Ticket'};
++ my $TransactionObj = $args{'Transaction'};
+
+-=head2 Gateway ARGSREF
++ unless ( $args{'Entity'} ) {
++ $RT::Logger->crit( "Could not send mail without 'Entity' object" );
++ return 0;
++ }
+
++ 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;
++ unless ( $args{'Entity'}->head->get('To')
++ || $args{'Entity'}->head->get('Cc')
++ || $args{'Entity'}->head->get('Bcc') )
++ {
++ $RT::Logger->info( $msgid . " No recipients found. Not sending." );
++ return -1;
++ }
+
+-Takes parameters:
++ if ($args{'Entity'}->head->get('X-RT-Squelch')) {
++ $RT::Logger->info( $msgid . " Squelch header found. Not sending." );
++ return -1;
++ }
+
+- action
+- queue
+- message
++ if (my $precedence = RT->Config->Get('DefaultMailPrecedence')
++ and !$args{'Entity'}->head->get("Precedence")
++ ) {
++ $args{'Entity'}->head->replace( 'Precedence', Encode::encode("UTF-8",$precedence) );
++ }
+
++ if ( $TransactionObj && !$TicketObj
++ && $TransactionObj->ObjectType eq 'RT::Ticket' )
++ {
++ $TicketObj = $TransactionObj->Object;
++ }
+
+-This performs all the "guts" of the mail rt-mailgate program, and is
+-designed to be called from the web interface with a message, user
+-object, and so on.
++ my $head = $args{'Entity'}->head;
++ unless ( $head->get('Date') ) {
++ require RT::Date;
++ my $date = RT::Date->new( RT->SystemUser );
++ $date->SetToNow;
++ $head->replace( 'Date', Encode::encode("UTF-8",$date->RFC2822( Timezone => 'server' ) ) );
++ }
++ unless ( $head->get('MIME-Version') ) {
++ # We should never have to set the MIME-Version header
++ $head->replace( 'MIME-Version', '1.0' );
++ }
++ unless ( $head->get('Content-Transfer-Encoding') ) {
++ # fsck.com #5959: Since RT sends 8bit mail, we should say so.
++ $head->replace( 'Content-Transfer-Encoding', '8bit' );
++ }
+
+-Can also take an optional 'ticket' parameter; this ticket id overrides
+-any ticket id found in the subject.
++ if ( RT->Config->Get('Crypt')->{'Enable'} ) {
++ %args = WillSignEncrypt(
++ %args,
++ Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef,
++ Ticket => $TicketObj,
++ );
++ my $res = SignEncrypt( %args );
++ return $res unless $res > 0;
++ }
+
+-Returns:
++ my $mail_command = RT->Config->Get('MailCommand');
+
+- An array of:
++ # if it is a sub routine, we just return it;
++ return $mail_command->($args{'Entity'}) if UNIVERSAL::isa( $mail_command, 'CODE' );
+
+- (status code, message, optional ticket object)
++ if ( $mail_command eq 'sendmailpipe' ) {
++ my $path = RT->Config->Get('SendmailPath');
++ my @args = shellwords(RT->Config->Get('SendmailArguments'));
++ push @args, "-t" unless grep {$_ eq "-t"} @args;
+
+- status code is a numeric value.
++ # 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') || {};
+
+- for temporary failures, the status code should be -75
++ if ($TicketObj) {
++ my $Queue = $TicketObj->QueueObj;
++ my $QueueAddressOverride = $Overrides->{$Queue->id}
++ || $Overrides->{$Queue->Name};
+
+- for permanent failures which are handled by RT, the status code
+- should be 0
++ if ($QueueAddressOverride) {
++ $OutgoingMailAddress = $QueueAddressOverride;
++ } else {
++ $OutgoingMailAddress ||= $Queue->CorrespondAddress
++ || RT->Config->Get('CorrespondAddress');
++ }
++ }
++ elsif ($Overrides->{'Default'}) {
++ $OutgoingMailAddress = $Overrides->{'Default'};
++ }
+
+- for succces, the status code should be 1
++ push @args, "-f", $OutgoingMailAddress
++ if $OutgoingMailAddress;
++ }
+
++ # VERP
++ if ( $TransactionObj and
++ my $prefix = RT->Config->Get('VERPPrefix') and
++ my $domain = RT->Config->Get('VERPDomain') )
++ {
++ my $from = $TransactionObj->CreatorObj->EmailAddress;
++ $from =~ s/@/=/g;
++ $from =~ s/\s//g;
++ push @args, "-f", "$prefix$from\@$domain";
++ }
+
++ eval {
++ # don't ignore CHLD signal to get proper exit code
++ local $SIG{'CHLD'} = 'DEFAULT';
+
+-=cut
++ # if something wrong with $mail->print we will get PIPE signal, handle it
++ local $SIG{'PIPE'} = sub { die "program unexpectedly closed pipe" };
+
+-sub _LoadPlugins {
+- my @mail_plugins = @_;
++ require IPC::Open2;
++ my ($mail, $stdout);
++ my $pid = IPC::Open2::open2( $stdout, $mail, $path, @args )
++ or die "couldn't execute program: $!";
- my @res;
- foreach my $plugin (@mail_plugins) {
@@ -1705,25 +2055,37 @@
- unless $Class =~ /^RT::/;
- $Class->require or
- do { $RT::Logger->error("Couldn't load $Class: $@"); next };
-+ my $TicketObj = $args{'Ticket'};
-+ my $TransactionObj = $args{'Transaction'};
++ $args{'Entity'}->print($mail);
++ close $mail or die "close pipe failed: $!";
- no strict 'refs';
- unless ( defined *{ $Class . "::GetCurrentUser" }{CODE} ) {
- $RT::Logger->crit( "No GetCurrentUser code found in $Class module");
- next;
-- }
++ 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` exited with code ". ($?>>8);
++ $msg = ", interrupted by signal ". ($?&127) if $?&127;
++ $RT::Logger->error( $msg );
++ die $msg;
+ }
- push @res, $Class;
- } else {
- $RT::Logger->crit( "$plugin - is not class name or code reference");
-- }
-+ unless ( $args{'Entity'} ) {
-+ $RT::Logger->crit( "Could not send mail without 'Entity' object" );
-+ return 0;
- }
++ };
++ if ( $@ ) {
++ $RT::Logger->crit( "$msgid: Could not send mail with command `$path @args`: " . $@ );
++ if ( $TicketObj ) {
++ _RecordSendEmailFailure( $TicketObj );
++ }
++ return 0;
+ }
+- }
- return @res;
-}
-
+-
-sub Gateway {
- my $argsref = shift;
- my %args = (
@@ -1733,25 +2095,13 @@
- message => undef,
- %$argsref
- );
-+ 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;
-+ unless ( $args{'Entity'}->head->get('To')
-+ || $args{'Entity'}->head->get('Cc')
-+ || $args{'Entity'}->head->get('Bcc') )
-+ {
-+ $RT::Logger->info( $msgid . " No recipients found. Not sending." );
-+ return -1;
-+ }
++ } elsif ( $mail_command eq 'mbox' ) {
++ my $now = RT::Date->new(RT->SystemUser);
++ $now->SetToNow;
- my $SystemTicket;
- my $Right;
-+ if ($args{'Entity'}->head->get('X-RT-Squelch')) {
-+ $RT::Logger->info( $msgid . " Squelch header found. Not sending." );
-+ return -1;
-+ }
-
+-
- # Validate the action
- my ( $status, @actions ) = IsCorrectAction( $args{'action'} );
- unless ($status) {
@@ -1763,24 +2113,15 @@
- . $args{'queue'},
- undef
- );
-+ if (my $precedence = RT->Config->Get('DefaultMailPrecedence')
-+ and !$args{'Entity'}->head->get("Precedence")
-+ ) {
-+ $args{'Entity'}->head->replace( 'Precedence', Encode::encode("UTF-8",$precedence) );
- }
-
+- }
+-
- my $parser = RT::EmailParser->new();
- $parser->SmartParseMIMEEntityFromScalar(
- Message => $args{'message'},
- Decode => 0,
- Exact => 1,
- );
-+ if ( $TransactionObj && !$TicketObj
-+ && $TransactionObj->ObjectType eq 'RT::Ticket' )
-+ {
-+ $TicketObj = $TransactionObj->Object;
-+ }
-
+-
- my $Message = $parser->Entity();
- unless ($Message) {
- MailError(
@@ -1788,45 +2129,20 @@
- Explanation => "RT couldn't process the message below",
- Attach => $args{'message'}
- );
-+ my $head = $args{'Entity'}->head;
-+ unless ( $head->get('Date') ) {
-+ require RT::Date;
-+ my $date = RT::Date->new( RT->SystemUser );
-+ $date->SetToNow;
-+ $head->replace( 'Date', Encode::encode("UTF-8",$date->RFC2822( Timezone => 'server' ) ) );
-+ }
-+ unless ( $head->get('MIME-Version') ) {
-+ # We should never have to set the MIME-Version header
-+ $head->replace( 'MIME-Version', '1.0' );
-+ }
-+ unless ( $head->get('Content-Transfer-Encoding') ) {
-+ # fsck.com #5959: Since RT sends 8bit mail, we should say so.
-+ $head->replace( 'Content-Transfer-Encoding', '8bit' );
-+ }
-
+-
- return ( 0,
- "Failed to parse this message. Something is likely badly wrong with the message"
-+ if ( RT->Config->Get('Crypt')->{'Enable'} ) {
-+ %args = WillSignEncrypt(
-+ %args,
-+ Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef,
-+ Ticket => $TicketObj,
- );
-+ my $res = SignEncrypt( %args );
-+ return $res unless $res > 0;
- }
-
+- );
+- }
+-
- my @mail_plugins = grep $_, RT->Config->Get('MailPlugins');
- push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
- @mail_plugins = _LoadPlugins( @mail_plugins );
-+ my $mail_command = RT->Config->Get('MailCommand');
-
+-
- #Set up a queue object
- my $SystemQueueObj = RT::Queue->new( RT->SystemUser );
- $SystemQueueObj->Load( $args{'queue'} );
-+ # if it is a sub routine, we just return it;
-+ return $mail_command->($args{'Entity'}) if UNIVERSAL::isa( $mail_command, 'CODE' );
-
+-
- my %skip_plugin;
- foreach my $class( grep !ref, @mail_plugins ) {
- # check if we should apply filter before decoding
@@ -1841,78 +2157,13 @@
- Queue => $SystemQueueObj,
- Actions => \@actions,
- );
-+ if ( $mail_command eq 'sendmailpipe' ) {
-+ my $path = RT->Config->Get('SendmailPath');
-+ my @args = shellwords(RT->Config->Get('SendmailArguments'));
-+ push @args, "-t" unless grep {$_ eq "-t"} @args;
-+
-+ # 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 $Queue = $TicketObj->QueueObj;
-+ my $QueueAddressOverride = $Overrides->{$Queue->id}
-+ || $Overrides->{$Queue->Name};
-+
-+ if ($QueueAddressOverride) {
-+ $OutgoingMailAddress = $QueueAddressOverride;
-+ } else {
-+ $OutgoingMailAddress ||= $Queue->CorrespondAddress
-+ || RT->Config->Get('CorrespondAddress');
-+ }
-+ }
-+ elsif ($Overrides->{'Default'}) {
-+ $OutgoingMailAddress = $Overrides->{'Default'};
-+ }
-+
-+ push @args, "-f", $OutgoingMailAddress
-+ if $OutgoingMailAddress;
-+ }
-+
-+ # VERP
-+ if ( $TransactionObj and
-+ my $prefix = RT->Config->Get('VERPPrefix') and
-+ my $domain = RT->Config->Get('VERPDomain') )
-+ {
-+ my $from = $TransactionObj->CreatorObj->EmailAddress;
-+ $from =~ s/@/=/g;
-+ $from =~ s/\s//g;
-+ push @args, "-f", "$prefix$from\@$domain";
-+ }
-
+-
- $skip_plugin{ $class }++;
-+ eval {
-+ # don't ignore CHLD signal to get proper exit code
-+ local $SIG{'CHLD'} = 'DEFAULT';
-
+-
- my $Code = do {
- no strict 'refs';
- *{ $class . "::GetCurrentUser" }{CODE};
-+ # 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: $!";
-+
-+ 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` exited with code ". ($?>>8);
-+ $msg = ", interrupted by signal ". ($?&127) if $?&127;
-+ $RT::Logger->error( $msg );
-+ die $msg;
-+ }
- };
+- };
- my ($status, $msg) = $Code->(
- Message => $Message,
- RawMessageRef => \$args{'message'},
@@ -1920,21 +2171,105 @@
- Actions => \@actions,
- );
- next if $status > 0;
-+ if ( $@ ) {
-+ $RT::Logger->crit( "$msgid: Could not send mail with command `$path @args`: " . $@ );
-+ if ( $TicketObj ) {
-+ _RecordSendEmailFailure( $TicketObj );
-+ }
-+ return 0;
-+ }
-+ }
-+ else {
-+ local ($ENV{'MAILADDRESS'}, $ENV{'PERL_MAILERS'});
-
+-
- if ( $status == -2 ) {
- return (1, $msg, undef);
- } elsif ( $status == -1 ) {
- return (0, $msg, undef);
++ state $logfile;
++ unless ($logfile) {
++ my $when = $now->ISO( Timezone => "server" );
++ $when =~ s/\s+/-/g;
++ $logfile = "$RT::VarPath/$when.mbox";
++ $RT::Logger->info("Storing outgoing emails in $logfile");
+ }
+- }
+- @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 = Encode::decode( "UTF-8", $head->get('Message-ID') )
+- || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
+-
+- #Pull apart the subject line
+- my $Subject = Encode::decode( "UTF-8", $head->get('Subject') || '');
+- chomp $Subject;
+-
+- # 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(
+- Message => $Message,
+- ErrorsTo => $ErrorsTo,
+- Subject => $Subject,
+- MessageId => $MessageId
+- );
+-
+- # Do not pass loop messages to MailPlugins, to make sure the loop
+- # is broken, unless $RT::StoreLoops is set.
+- if ($IsALoop && !$should_store_machine_generated_message) {
+- return ( 0, $result, undef );
+- }
+- # }}}
+-
+- $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->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
+- if ( $SystemTicket->id ) {
+- $Right = 'ReplyToTicket';
++ my $fh;
++ unless (open($fh, ">>", $logfile)) {
++ $RT::Logger->crit( "Can't open mbox file $logfile: $!" );
++ return 0;
++ }
++ my $content = $args{Entity}->stringify;
++ $content =~ s/^(>*From )/>$1/mg;
++ print $fh "From $ENV{USER}\@localhost ".localtime."\n";
++ print $fh $content, "\n";
++ close $fh;
+ } else {
+- $Right = 'CreateTicket';
+- }
+-
+- # We can safely have no queue of we have a known-good ticket
+- unless ( $SystemTicket->id || $SystemQueueObj->id ) {
+- return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
+- }
+-
+- my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel(
+- MailPlugins => \@mail_plugins,
+- Actions => \@actions,
+- Message => $Message,
+- RawMessageRef => \$args{message},
+- SystemTicket => $SystemTicket,
+- SystemQueue => $SystemQueueObj,
+- );
+-
+- # 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.
+- unless ( $AuthStat == -1 ) {
+- _NoAuthorizedUserFound(
+- Right => $Right,
+- Message => $Message,
+- Requestor => $ErrorsTo,
+- Queue => $args{'queue'}
+- );
++ local ($ENV{'MAILADDRESS'}, $ENV{'PERL_MAILERS'});
+
+ my @mailer_args = ($mail_command);
+ if ( $mail_command eq 'sendmail' ) {
+ $ENV{'PERL_MAILERS'} = RT->Config->Get('SendmailPath');
@@ -1947,139 +2282,9 @@
+ }
+ } else {
+ push @mailer_args, RT->Config->Get('MailParams');
-+ }
-+
-+ unless ( $args{'Entity'}->send( @mailer_args ) ) {
-+ $RT::Logger->crit( "$msgid: Could not send mail." );
-+ if ( $TicketObj ) {
-+ _RecordSendEmailFailure( $TicketObj );
-+ }
-+ return 0;
}
- }
-- @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins;
-- $parser->_DecodeBodies;
-- $parser->RescueOutlook;
-- $parser->_PostProcessNewEntity;
-+ return 1;
-+}
-
-- 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;
-+=head2 PrepareEmailUsingTemplate Template => '', Arguments => {}
-
-- my $MessageId = Encode::decode( "UTF-8", $head->get('Message-ID') )
-- || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
-+Loads a template. Parses it using arguments if it's not empty.
-+Returns a tuple (L<RT::Template> object, error message).
-
-- #Pull apart the subject line
-- my $Subject = Encode::decode( "UTF-8", $head->get('Subject') || '');
-- chomp $Subject;
--
-- # 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(
-- Message => $Message,
-- ErrorsTo => $ErrorsTo,
-- Subject => $Subject,
-- MessageId => $MessageId
-+Note that even if a template object is returned MIMEObj method
-+may return undef for empty templates.
-+
-+=cut
-+
-+sub PrepareEmailUsingTemplate {
-+ my %args = (
-+ Template => '',
-+ Arguments => {},
-+ @_
- );
-
-- # Do not pass loop messages to MailPlugins, to make sure the loop
-- # is broken, unless $RT::StoreLoops is set.
-- if ($IsALoop && !$should_store_machine_generated_message) {
-- return ( 0, $result, undef );
-+ my $template = RT::Template->new( RT->SystemUser );
-+ $template->LoadGlobalTemplate( $args{'Template'} );
-+ unless ( $template->id ) {
-+ return (undef, "Couldn't load template '". $args{'Template'} ."'");
- }
-- # }}}
-+ return $template if $template->IsEmpty;
-
-- $args{'ticket'} ||= ExtractTicketId( $Message );
-+ my ($status, $msg) = $template->Parse( %{ $args{'Arguments'} } );
-+ return (undef, $msg) unless $status;
-
-- # ExtractTicketId may have been overridden, and edited the Subject
-- my $NewSubject = Encode::decode( "UTF-8", $Message->head->get('Subject') );
-- chomp $NewSubject;
-+ return $template;
-+}
-
-- $SystemTicket = RT::Ticket->new( RT->SystemUser );
-- $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
-- if ( $SystemTicket->id ) {
-- $Right = 'ReplyToTicket';
-- } else {
-- $Right = 'CreateTicket';
-- }
-+=head2 SendEmailUsingTemplate Template => '', Arguments => {}, From => CorrespondAddress, To => '', Cc => '', Bcc => ''
-
-- # We can safely have no queue of we have a known-good ticket
-- unless ( $SystemTicket->id || $SystemQueueObj->id ) {
-- return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
-- }
-+Sends email using a template, takes name of template, arguments for it and recipients.
-
-- my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel(
-- MailPlugins => \@mail_plugins,
-- Actions => \@actions,
-- Message => $Message,
-- RawMessageRef => \$args{message},
-- SystemTicket => $SystemTicket,
-- SystemQueue => $SystemQueueObj,
-- );
-+=cut
-
-- # If authentication fails and no new user was created, get out.
-- if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) {
-+sub SendEmailUsingTemplate {
-+ my %args = (
-+ Template => '',
-+ Arguments => {},
-+ To => undef,
-+ Cc => undef,
-+ Bcc => undef,
-+ From => RT->Config->Get('CorrespondAddress'),
-+ InReplyTo => undef,
-+ ExtraHeaders => {},
-+ @_
-+ );
-
-- # If the plugins refused to create one, they lose.
-- unless ( $AuthStat == -1 ) {
-- _NoAuthorizedUserFound(
-- Right => $Right,
-- Message => $Message,
-- Requestor => $ErrorsTo,
-- Queue => $args{'queue'}
-- );
-+ my ($template, $msg) = PrepareEmailUsingTemplate( %args );
-+ return (0, $msg) unless $template;
-
-- }
- return ( 0, "Could not load a valid user", undef );
-+ my $mail = $template->MIMEObj;
-+ unless ( $mail ) {
-+ $RT::Logger->info("Message is not sent as template #". $template->id ." is empty");
-+ return -1;
- }
+- }
- # If we got a user, but they don't have the right to say things
- if ( $AuthStat == 0 ) {
@@ -2099,38 +2304,36 @@
- undef
- );
- }
-+ $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ $_ } ) )
-+ foreach grep defined $args{$_}, qw(To Cc Bcc From);
-
-+ $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
-+ foreach keys %{ $args{ExtraHeaders} };
-
+-
+-
- unless ($should_store_machine_generated_message) {
- return ( 0, $result, undef );
-- }
-+ SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
++ unless ( $args{'Entity'}->send( @mailer_args ) ) {
++ $RT::Logger->crit( "$msgid: Could not send mail." );
++ if ( $TicketObj ) {
++ _RecordSendEmailFailure( $TicketObj );
++ }
++ return 0;
++ }
+ }
++ return 1;
++}
- $head->replace('X-RT-Interface' => 'Email');
-+ return SendEmail( Entity => $mail );
-+}
-
+-
- # if plugin's updated SystemTicket then update arguments
- $args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id;
-+=head2 GetForwardFrom Ticket => undef, Transaction => undef
-
+-
- my $Ticket = RT::Ticket->new($CurrentUser);
-+Resolve the From field to use in forward mail
-
+-
- if ( !$args{'ticket'} && grep /^(comment|correspond)$/, @actions )
- {
-+=cut
++=head2 PrepareEmailUsingTemplate Template => '', Arguments => {}
- my @Cc;
- my @Requestors = ( $CurrentUser->id );
-+sub GetForwardFrom {
-+ my %args = ( Ticket => undef, Transaction => undef, @_ );
-+ my $txn = $args{Transaction};
-+ my $ticket = $args{Ticket} || $txn->Object;
++Loads a template. Parses it using arguments if it's not empty.
++Returns a tuple (L<RT::Template> object, error message).
- if (RT->Config->Get('ParseNewMessageForTicketCcs')) {
- @Cc = ParseCcAddressesFromHead(
@@ -2139,14 +2342,8 @@
- QueueObj => $SystemQueueObj
- );
- }
-+ if ( RT->Config->Get('ForwardFromUser') ) {
-+ return ( $txn || $ticket )->CurrentUser->EmailAddress;
-+ }
-+ else {
-+ return $ticket->QueueObj->CorrespondAddress
-+ || RT->Config->Get('CorrespondAddress');
-+ }
-+}
++Note that even if a template object is returned MIMEObj method
++may return undef for empty templates.
- my ( $id, $Transaction, $ErrStr ) = $Ticket->Create(
- Queue => $SystemQueueObj->Id,
@@ -2164,16 +2361,26 @@
- );
- return ( 0, "Ticket creation From: $From failed: $ErrStr", $Ticket );
- }
-+=head2 GetForwardAttachments Ticket => undef, Transaction => undef
++=cut
- # strip comments&corresponds from the actions we don't need
- # to record them if we've created the ticket just now
- @actions = grep !/^(comment|correspond)$/, @actions;
- $args{'ticket'} = $id;
-+Resolve the Attachments to forward
++sub PrepareEmailUsingTemplate {
++ my %args = (
++ Template => '',
++ Arguments => {},
++ @_
++ );
- } elsif ( $args{'ticket'} ) {
-+=cut
++ my $template = RT::Template->new( RT->SystemUser );
++ $template->LoadGlobalTemplate( $args{'Template'} );
++ unless ( $template->id ) {
++ return (undef, "Couldn't load template '". $args{'Template'} ."'");
++ }
++ return $template if $template->IsEmpty;
- $Ticket->Load( $args{'ticket'} );
- unless ( $Ticket->Id ) {
@@ -2184,52 +2391,31 @@
- Explanation => $error,
- MIMEObj => $Message
- );
-+sub GetForwardAttachments {
-+ my %args = ( Ticket => undef, Transaction => undef, @_ );
-+ my $txn = $args{Transaction};
-+ my $ticket = $args{Ticket} || $txn->Object;
++ my ($status, $msg) = $template->Parse( %{ $args{'Arguments'} } );
++ return (undef, $msg) unless $status;
- return ( 0, $error );
- }
- $args{'ticket'} = $Ticket->id;
- } else {
- return ( 1, "Success", $Ticket );
-+ my $attachments = RT::Attachments->new( $ticket->CurrentUser );
-+ if ($txn) {
-+ $attachments->Limit( FIELD => 'TransactionId', VALUE => $txn->id );
-+ }
-+ else {
-+ $attachments->LimitByTicket( $ticket->id );
-+ $attachments->Limit(
-+ ALIAS => $attachments->TransactionAlias,
-+ FIELD => 'Type',
-+ OPERATOR => 'IN',
-+ VALUE => [ qw(Create Correspond) ],
-+ );
- }
-+ return $attachments;
+- }
++ return $template;
+}
- # }}}
-+sub WillSignEncrypt {
-+ my %args = @_;
-+ my $attachment = delete $args{Attachment};
-+ my $ticket = delete $args{Ticket};
++=head2 SendEmailUsingTemplate Template => '', Arguments => {}, From => CorrespondAddress, To => '', Cc => '', Bcc => ''
- my $unsafe_actions = RT->Config->Get('UnsafeEmailCommands');
- foreach my $action (@actions) {
-+ if ( not RT->Config->Get('Crypt')->{'Enable'} ) {
-+ $args{Sign} = $args{Encrypt} = 0;
-+ return wantarray ? %args : 0;
-+ }
++Sends email using a template, takes name of template, arguments for it and recipients.
- # If the action is comment, add a comment.
- if ( $action =~ /^(?:comment|correspond)$/i ) {
- my $method = ucfirst lc $action;
- my ( $status, $msg ) = $Ticket->$method( MIMEObj => $Message );
- unless ($status) {
-+ for my $argument ( qw(Sign Encrypt) ) {
-+ next if defined $args{ $argument };
++=cut
- #Warn the sender that we couldn't actually submit the comment.
- MailError(
@@ -2249,6 +2435,157 @@
- CurrentUser => $CurrentUser,
- );
- return ($status, $msg, $Ticket) unless $status == 1;
+- }
++sub SendEmailUsingTemplate {
++ my %args = (
++ Template => '',
++ Arguments => {},
++ To => undef,
++ Cc => undef,
++ Bcc => undef,
++ From => RT->Config->Get('CorrespondAddress'),
++ InReplyTo => undef,
++ ExtraHeaders => {},
++ @_
++ );
++
++ my ($template, $msg) = PrepareEmailUsingTemplate( %args );
++ return (0, $msg) unless $template;
++
++ my $mail = $template->MIMEObj;
++ unless ( $mail ) {
++ $RT::Logger->info("Message is not sent as template #". $template->id ." is empty");
++ return -1;
+ }
+- return ( 1, "Success", $Ticket );
++
++ $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ $_ } ) )
++ foreach grep defined $args{$_}, qw(To Cc Bcc From);
++
++ $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
++ foreach keys %{ $args{ExtraHeaders} };
++
++ SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
++
++ return SendEmail( Entity => $mail );
+ }
+
+-=head2 GetAuthenticationLevel
++=head2 GetForwardFrom Ticket => undef, Transaction => undef
+
+- # Authentication Level
+- # -1 - Get out. this user has been explicitly declined
+- # 0 - User may not do anything (Not used at the moment)
+- # 1 - Normal user
+- # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate
++Resolve the From field to use in forward mail
+
+ =cut
+
+-sub GetAuthenticationLevel {
+- my %args = (
+- MailPlugins => [],
+- Actions => [],
+- Message => undef,
+- RawMessageRef => undef,
+- SystemTicket => undef,
+- SystemQueue => undef,
+- @_,
+- );
++sub GetForwardFrom {
++ my %args = ( Ticket => undef, Transaction => undef, @_ );
++ my $txn = $args{Transaction};
++ my $ticket = $args{Ticket} || $txn->Object;
+
+- my ( $CurrentUser, $AuthStat, $error );
++ if ( RT->Config->Get('ForwardFromUser') ) {
++ return ( $txn || $ticket )->CurrentUser->EmailAddress;
++ }
++ else {
++ return $ticket->QueueObj->CorrespondAddress
++ || RT->Config->Get('CorrespondAddress');
++ }
++}
+
+- # Initalize AuthStat so comparisons work correctly
+- $AuthStat = -9999999;
++=head2 GetForwardAttachments Ticket => undef, Transaction => undef
+
+- # if plugin returns AuthStat -2 we skip action
+- # NOTE: this is experimental API and it would be changed
+- my %skip_action = ();
++Resolve the Attachments to forward
+
+- # Since this needs loading, no matter what
+- foreach (@{ $args{MailPlugins} }) {
+- my ($Code, $NewAuthStat);
+- if ( ref($_) eq "CODE" ) {
+- $Code = $_;
+- } else {
+- no strict 'refs';
+- $Code = *{ $_ . "::GetCurrentUser" }{CODE};
+- }
++=cut
+
+- foreach my $action (@{ $args{Actions} }) {
+- ( $CurrentUser, $NewAuthStat ) = $Code->(
+- Message => $args{Message},
+- RawMessageRef => $args{RawMessageRef},
+- CurrentUser => $CurrentUser,
+- AuthLevel => $AuthStat,
+- Action => $action,
+- Ticket => $args{SystemTicket},
+- Queue => $args{SystemQueue},
+- );
++sub GetForwardAttachments {
++ my %args = ( Ticket => undef, Transaction => undef, @_ );
++ my $txn = $args{Transaction};
++ my $ticket = $args{Ticket} || $txn->Object;
+
+-# You get the highest level of authentication you were assigned, unless you get the magic -1
+-# If a module returns a "-1" then we discard the ticket, so.
+- $AuthStat = $NewAuthStat
+- if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
++ my $attachments = RT::Attachments->new( $ticket->CurrentUser );
++ if ($txn) {
++ $attachments->Limit( FIELD => 'TransactionId', VALUE => $txn->id );
++ }
++ else {
++ $attachments->LimitByTicket( $ticket->id );
++ $attachments->Limit(
++ ALIAS => $attachments->TransactionAlias,
++ FIELD => 'Type',
++ OPERATOR => 'IN',
++ VALUE => [ qw(Create Correspond) ],
++ );
++ }
++ return $attachments;
++}
+
+- last if $AuthStat == -1;
+- $skip_action{$action}++ if $AuthStat == -2;
+- }
+
+- # strip actions we should skip
+- @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
+- if $AuthStat == -2;
+- last unless @{$args{Actions}};
++sub WillSignEncrypt {
++ my %args = @_;
++ my $attachment = delete $args{Attachment};
++ my $ticket = delete $args{Ticket};
+
+- last if $AuthStat == -1;
++ if ( not RT->Config->Get('Crypt')->{'Enable'} ) {
++ $args{Sign} = $args{Encrypt} = 0;
++ return wantarray ? %args : 0;
+ }
+
+- return $AuthStat if !wantarray;
++ for my $argument ( qw(Sign Encrypt) ) {
++ next if defined $args{ $argument };
+
+- return ($AuthStat, $CurrentUser, $error);
+ if ( $attachment and defined $attachment->GetHeader("X-RT-$argument") ) {
+ $args{$argument} = $attachment->GetHeader("X-RT-$argument");
+ } elsif ( $ticket and $argument eq "Encrypt" ) {
@@ -2259,21 +2596,15 @@
+ # to 0 or 1; thus this path is only taken for messages
+ # generated _not_ via the web UI.
+ $args{Sign} = $ticket->QueueObj->SignAuto();
- }
- }
-- return ( 1, "Success", $Ticket );
++ }
++ }
+
+ return wantarray ? %args : ($args{Sign} || $args{Encrypt});
}
--=head2 GetAuthenticationLevel
+-sub _RunUnsafeAction {
+=head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0
-
-- # Authentication Level
-- # -1 - Get out. this user has been explicitly declined
-- # 0 - User may not do anything (Not used at the moment)
-- # 1 - Normal user
-- # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate
++
+Signs and encrypts message using L<RT::Crypt>, but as well handle errors
+with users' keys.
+
@@ -2285,72 +2616,47 @@
+
+Returns 1 on success, 0 on error and -1 if all recipients are bad and
+had been filtered out.
-
- =cut
-
--sub GetAuthenticationLevel {
++
++=cut
++
+sub SignEncrypt {
my %args = (
-- MailPlugins => [],
-- Actions => [],
-- Message => undef,
-- RawMessageRef => undef,
-- SystemTicket => undef,
-- SystemQueue => undef,
-- @_,
+- Action => undef,
+- ErrorsTo => undef,
+- Message => undef,
+- Ticket => undef,
+- CurrentUser => undef,
+ Entity => undef,
+ Sign => 0,
+ Encrypt => 0,
-+ @_
+ @_
);
+ return 1 unless $args{'Sign'} || $args{'Encrypt'};
-- my ( $CurrentUser, $AuthStat, $error );
--
-- # Initalize AuthStat so comparisons work correctly
-- $AuthStat = -9999999;
--
-- # if plugin returns AuthStat -2 we skip action
-- # NOTE: this is experimental API and it would be changed
-- my %skip_action = ();
+- my $From = Encode::decode( "UTF-8", $args{Message}->head->get("From") );
+ my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
+ chomp $msgid;
-- # Since this needs loading, no matter what
-- foreach (@{ $args{MailPlugins} }) {
-- my ($Code, $NewAuthStat);
-- if ( ref($_) eq "CODE" ) {
-- $Code = $_;
-- } else {
-- no strict 'refs';
-- $Code = *{ $_ . "::GetCurrentUser" }{CODE};
-- }
+- if ( $args{'Action'} =~ /^take$/i ) {
+- my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
+- unless ($status) {
+- MailError(
+- To => $args{'ErrorsTo'},
+- Subject => "Ticket not taken",
+- Explanation => $msg,
+- MIMEObj => $args{'Message'}
+- );
+- return ( 0, "Ticket not taken, by email From: $From" );
+ $RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
+ $RT::Logger->debug("$msgid Encrypting message") if $args{'Encrypt'};
-
-- foreach my $action (@{ $args{Actions} }) {
-- ( $CurrentUser, $NewAuthStat ) = $Code->(
-- Message => $args{Message},
-- RawMessageRef => $args{RawMessageRef},
-- CurrentUser => $CurrentUser,
-- AuthLevel => $AuthStat,
-- Action => $action,
-- Ticket => $args{SystemTicket},
-- Queue => $args{SystemQueue},
-- );
++
+ my %res = RT::Crypt->SignEncrypt( %args );
+ return 1 unless $res{'exit_code'};
-
--# You get the highest level of authentication you were assigned, unless you get the magic -1
--# If a module returns a "-1" then we discard the ticket, so.
-- $AuthStat = $NewAuthStat
-- if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
++
+ my @status = RT::Crypt->ParseStatus(
+ Protocol => $res{'Protocol'}, Status => $res{'status'},
+ );
-
-- last if $AuthStat == -1;
-- $skip_action{$action}++ if $AuthStat == -2;
++
+ my @bad_recipients;
+ foreach my $line ( @status ) {
+ # if the passphrase fails, either you have a bad passphrase
@@ -2361,26 +2667,30 @@
+ $RT::Logger->error( "$line->{'Status'} PASSPHRASE: $line->{'Message'}" );
+ return 0;
}
--
-- # strip actions we should skip
-- @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
-- if $AuthStat == -2;
-- last unless @{$args{Actions}};
--
-- last if $AuthStat == -1;
+- } elsif ( $args{'Action'} =~ /^resolve$/i ) {
+- my $new_status = $args{'Ticket'}->FirstInactiveStatus;
+- if ($new_status) {
+- my ( $status, $msg ) = $args{'Ticket'}->SetStatus($new_status);
+- unless ($status) {
+ next unless ($line->{'Operation'}||'') eq 'RecipientsCheck';
+ next if $line->{'Status'} eq 'DONE';
+ $RT::Logger->error( $line->{'Message'} );
+ push @bad_recipients, $line;
- }
++ }
+ return 0 unless @bad_recipients;
-- return $AuthStat if !wantarray;
+- #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" );
+- }
+ $_->{'AddressObj'} = (Email::Address->parse( $_->{'Recipient'} ))[0]
+ foreach @bad_recipients;
-
-- return ($AuthStat, $CurrentUser, $error);
--}
++
+ foreach my $recipient ( @bad_recipients ) {
+ my $status = SendEmailUsingTemplate(
+ To => $recipient->{'AddressObj'}->address,
@@ -2393,17 +2703,14 @@
+ );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't send 'Error: public key'");
-+ }
-+ }
-
--sub _RunUnsafeAction {
-- my %args = (
-- Action => undef,
-- ErrorsTo => undef,
-- Message => undef,
-- Ticket => undef,
-- CurrentUser => undef,
-- @_
+ }
+- } else {
+- return ( 0, "Not supported unsafe action $args{'Action'}, by email From: $From", $args{'Ticket'} );
+ }
+- return ( 1, "Success" );
+-}
+
+-=head2 _NoAuthorizedUserFound
+ my $status = SendEmailUsingTemplate(
+ To => RT->Config->Get('OwnerEmail'),
+ Template => 'Error to RT owner: public key',
@@ -2412,64 +2719,32 @@
+ TicketObj => $args{'Ticket'},
+ TransactionObj => $args{'Transaction'},
+ },
- );
++ );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't send 'Error to RT owner: public key'");
+ }
-
-- 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) {
-- MailError(
-- To => $args{'ErrorsTo'},
-- Subject => "Ticket not taken",
-- Explanation => $msg,
-- MIMEObj => $args{'Message'}
-- );
-- return ( 0, "Ticket not taken, by email From: $From" );
-- }
-- } elsif ( $args{'Action'} =~ /^resolve$/i ) {
-- my $new_status = $args{'Ticket'}->FirstInactiveStatus;
-- if ($new_status) {
-- my ( $status, $msg ) = $args{'Ticket'}->SetStatus($new_status);
-- unless ($status) {
++
+ DeleteRecipientsFromHead(
+ $args{'Entity'}->head,
+ map $_->{'AddressObj'}->address, @bad_recipients
+ );
-
-- #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'}, by email From: $From", $args{'Ticket'} );
++
+ unless ( $args{'Entity'}->head->get('To')
+ || $args{'Entity'}->head->get('Cc')
+ || $args{'Entity'}->head->get('Bcc') )
+ {
+ $RT::Logger->debug("$msgid No recipients that have public key, not sending");
+ return -1;
- }
-- return ( 1, "Success" );
--}
--
--=head2 _NoAuthorizedUserFound
++ }
-Emails the RT Owner and the requestor when the auth plugins return "No auth user found"
--
--=cut
+ # redo without broken recipients
+ %res = RT::Crypt->SignEncrypt( %args );
+ return 0 if $res{'exit_code'};
+
+-=cut
++ return 1;
++}
-sub _NoAuthorizedUserFound {
- my %args = (
@@ -2479,8 +2754,6 @@
- Queue => undef,
- @_
- );
-+ return 1;
-+}
- # Notify the RT Admin of the failure.
- MailError(
@@ -2699,6 +2972,7 @@
+ @references = $get_header->('In-Reply-To');
}
- return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
+-}
+ push @references, @id, @rtid;
+ if ( $args{'Ticket'} ) {
+ my $pseudo_ref = PseudoReference( $args{'Ticket'} );
@@ -2706,19 +2980,18 @@
+ }
+ splice @references, 4, -6
+ if @references > 10;
-+
+
+-=head2 IsCorrectAction
+ my $mail = $args{'Message'};
+ $mail->head->replace( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
+ $mail->head->replace( 'References' => Encode::encode( "UTF-8", join ' ', @references) );
- }
-
--=head2 IsCorrectAction
++}
+
+-Returns a list of valid actions we've found for this message
+sub PseudoReference {
+ my $ticket = shift;
+ return '<RT-Ticket-'. $ticket->id .'@'. RT->Config->Get('Organization') .'>';
+}
-
--Returns a list of valid actions we've found for this message
-=cut
+sub AddSubjectTag {
@@ -2753,5 +3026,9 @@
+ return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject";
}
++
++
sub _RecordSendEmailFailure {
+ my $ticket = shift;
+ if ($ticket) {
3: 2a14662 ! 3: 3820f12 Adjust POD headers to reflect split
@@ -14,8 +14,8 @@
+
+=head3 Gateway ARGSREF
+
Takes parameters:
-
@@
return ( 1, "Success", $Ticket );
}
@@ -35,8 +35,8 @@
# Authentication Level
# -1 - Get out. this user has been explicitly declined
@@
- return ($AuthStat, $CurrentUser, $error);
- }
+ }
+
-=head2 _NoAuthorizedUserFound
+=head3 _NoAuthorizedUserFound
@@ -181,8 +181,8 @@
Signs and encrypts message using L<RT::Crypt>, but as well handle errors
with users' keys.
@@
- return 1;
- }
+ }
+
-=head2 DeleteRecipientsFromHead HEAD RECIPIENTS
+=head3 DeleteRecipientsFromHead HEAD RECIPIENTS
4: 99624a2 = 4: cdd7fc6 Update MailPlugins to only reference existant modules
5: 00d0850 = 5: 28d4d8d Move MailPlugins documentation out of rt-mailgate, where is is mostly irrelevant
6: 8400ff9 = 6: 4fb779c Remove duplicate ParseCcAddressesFromHead in RT::EmailParser
7: 7b68589 = 7: 7cf851d Use Scope::Upper to allow returns from Gateway from nested subs
8: d358013 = 8: 6e71490 Add a base email plugin role, which provides TMPFAIL/FAILURE/SUCCESS
9: aee7869 = 9: 783cfae Check if each named mail plugin DOES the mail plugin role
10: 049f74d = 10: 7f449ab Return values of ApplyBeforeDecode plugins' GetCurrentUser are irrelevant
11: cae6a89 = 11: 4f3ecec Push bounce short-circuiting down into _HandleMachineGeneratedMail
12: afe6659 = 12: 12dd661 $IsALoop is now unused in Gateway
13: dc1f8fe = 13: 0a620d9 Stop moving RT-Squelch-Replies-To aside
14: 03979a3 = 14: e02dbd8 Simplify _HandleMachineGeneratedMail logic
15: 1a1d88f = 15: dbe2a63 Use different method names rather than an ApplyBeforeDecode method
16: 202a69f = 16: 3f2ed6f Use FAILURE to abort from GetCurrentUser, rather than a magic -1 value
17: 19f7ec5 ! 17: 2d5fa31 Remove the unused $error variable
@@ -22,5 +22,5 @@
+ return ($AuthStat, $CurrentUser);
}
- =head3 _NoAuthorizedUserFound
+
18: 332f4d2 = 18: 0454c42 Move $Right to where it is used
19: a418722 = 19: d34399b Move SystemTicket definition to where it is first used
20: 8ae942f = 20: 771cfe1 Move NewSubject to where it is used
21: 24bf62a = 21: 6eed8e2 Remove CreateUser, merging to form a more featureful LoadOrCreateByEmail
22: 6dd0a29 = 22: 9bf127f Always create the user; this simplifies ACL checking greatly
23: 39d8b60 ! 23: 9c1e527 Split authentication from authorization
@@ -189,7 +189,7 @@
+ return $AuthStat;
}
- =head3 _NoAuthorizedUserFound
+
diff --git a/lib/RT/Interface/Email/Auth/MailFrom.pm b/lib/RT/Interface/Email/Auth/MailFrom.pm
--- a/lib/RT/Interface/Email/Auth/MailFrom.pm
24: d6b673a ! 24: 26bf806 Remove no-longer-used _NoAuthorizedUserFound
@@ -10,8 +10,8 @@
--- a/lib/RT/Interface/Email.pm
+++ b/lib/RT/Interface/Email.pm
@@
- return $AuthStat;
}
+
-=head3 _NoAuthorizedUserFound
-
25: 7408373 = 25: 5db4d3b Remove now-unused $Right variable, previously used by _NoAuthorizedUserFound
26: ddd1c03 ! 26: 05271ff Fail if the first action is unauthenticated
@@ -129,7 +129,7 @@
+ FAILURE( "You have no permission to $args{Action}" );
}
- =head3 ParseCcAddressesFromHead HASH
+
diff --git a/lib/RT/Interface/Email/Auth/MailFrom.pm b/lib/RT/Interface/Email/Auth/MailFrom.pm
--- a/lib/RT/Interface/Email/Auth/MailFrom.pm
27: 03d0af9 = 27: 019e5fa Notify the owner on common mis-configurations
28: 12115ef = 28: efd311e Remove extra error in mail-gateway
29: 096152b = 29: 1269b22 Add back a warning that is now lacking
30: 3079bf8 = 30: 4a83780 Stop passing mail plugins around; load them lazily in one Plugins() method
31: 5ccf5a1 ! 31: 9edfe8c Split action handling into plugin classes
@@ -245,8 +245,8 @@
foreach my $plugin (@mail_plugins) {
if ( ref($plugin) eq "CODE" ) {
@@
- FAILURE( "You have no permission to $args{Action}" );
}
+
-=head3 ParseCcAddressesFromHead HASH
-
32: 2f24bb09 = 32: 442f33d Plugins may alter @actions; ensure action is valid prior to calling
33: a6838f4 = 33: e1d4d7e Split default authentication from default authorization
34: 4651652 = 34: dcf1351 Local'ize MailErrors to avoid having to pass $ErrorsTo everywhere
35: 3539367 = 35: 1175342 Remove the warning about the deprecated Auth::GnuPG/Auth::SMIME plugins
36: 384caa6 = 36: 940073d Allow lazy adding of Auth::MailFrom if no other GetCurrentUser plugins exist
37: b33b8a4 = 37: 5126611 There is no reason to not always enable Auth::Crypt
38: 59f1c3c = 38: 770b585 Make Crypt not an Auth:: plugin, but hardcoded
39: 9af3360 = 39: 43a1ac6 Move RejectOnUnencrypted to being a mail plugin
40: 65d32d4 = 40: 8da28bf Merge ParseAddressFromHeader and RT::EmailParser->ParseEmailAddress
41: 8c71c0f = 41: 4d174b7 Fix callsites of ParseSenderAddressFromHead to be slightly less incomprehensible
42: b735284 = 42: 5172953 $MessageId is only used in IsMachineGeneratedMail; move it in there
43: f71f257 ! 43: 1d1320b Merge CheckForSuspiciousSender, CheckForAutoGenerated, and CheckForBounce
@@ -124,5 +124,5 @@
+ return 0;
}
- =head2 ExtractTicketId
+
44: b1ea355 ! 44: 2f047f2 Reduce repetition by making MailError handle throwing the FAILURE, as well
@@ -38,7 +38,7 @@
- FAILURE( "You have no permission to $args{Action}" );
}
- sub HandleAction {
+
@@
MIMEObj => undef,
Attach => undef,
45: a008d0a = 45: 2a1a9b5 Move ACL checking for Take and Resolve into their own plugins
46: 140d5c8 ! 46: 1e19855 Update POD for new methods and functionality
@@ -429,8 +429,8 @@
=cut
@@
- );
}
+
+=head3 HandleAction Action => C<action>, Message => C<message>, Ticket => C<ticket>, Queue => C<queue>
+
@@ -508,8 +508,8 @@
=cut
@@
- return 0;
}
+
-=head2 ExtractTicketId
+=head3 ExtractTicketId
47: ac37116 = 47: 044fe16 simple test for multiple reply-to addresses
48: b78d166 = 48: b4b94b3 take into account multiple sender's addresses
49: 4bf016c = 49: c179f58 Email::Address 1.900+ has a fix of uninitialized warning
More information about the rt-commit
mailing list