1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
49 package RT::Interface::Email;
58 use UNIVERSAL::require;
60 use Text::ParseWords qw/shellwords/;
64 use vars qw ( @EXPORT_OK);
66 # set the version for version checking
69 # your exported package globals go here,
70 # as well as any optionally exported functions
75 &CheckForSuspiciousSender
76 &CheckForAutoGenerated
79 &ParseCcAddressesFromHead
80 &ParseSenderAddressFromHead
81 &ParseErrorsToAddressFromHead
82 &ParseAddressFromHeader
89 RT::Interface::Email - helper functions for parsing email sent to RT
93 use lib "!!RT_LIB_PATH!!";
94 use lib "!!RT_ETC_PATH!!";
96 use RT::Interface::Email qw(Gateway CreateUser);
105 =head2 CheckForLoops HEAD
107 Takes a HEAD object of L<MIME::Head> class and returns true if the
108 message's been sent by this RT instance. Uses "X-RT-Loop-Prevention"
109 field of the head for test.
116 # If this instance of RT sent it our, we don't want to take it in
117 my $RTLoop = $head->get("X-RT-Loop-Prevention") || "";
118 chomp ($RTLoop); # remove that newline
119 if ( $RTLoop eq RT->Config->Get('rtname') ) {
123 # TODO: We might not trap the case where RT instance A sends a mail
124 # to RT instance B which sends a mail to ...
128 =head2 CheckForSuspiciousSender HEAD
130 Takes a HEAD object of L<MIME::Head> class and returns true if sender
131 is suspicious. Suspicious means mailer daemon.
133 See also L</ParseSenderAddressFromHead>.
137 sub CheckForSuspiciousSender {
140 #if it's from a postmaster or mailer daemon, it's likely a bounce.
142 #TODO: better algorithms needed here - there is no standards for
143 #bounces, so it's very difficult to separate them from anything
144 #else. At the other hand, the Return-To address is only ment to be
145 #used as an error channel, we might want to put up a separate
146 #Return-To address which is treated differently.
148 #TODO: search through the whole email and find the right Ticket ID.
150 my ( $From, $junk ) = ParseSenderAddressFromHead($head);
152 if ( ( $From =~ /^mailer-daemon\@/i )
153 or ( $From =~ /^postmaster\@/i )
163 =head2 CheckForAutoGenerated HEAD
165 Takes a HEAD object of L<MIME::Head> class and returns true if message
166 is autogenerated. Checks 'Precedence' and 'X-FC-Machinegenerated'
167 fields of the head in tests.
171 sub CheckForAutoGenerated {
174 my $Precedence = $head->get("Precedence") || "";
175 if ( $Precedence =~ /^(bulk|junk)/i ) {
179 # Per RFC3834, any Auto-Submitted header which is not "no" means
180 # it is auto-generated.
181 my $AutoSubmitted = $head->get("Auto-Submitted") || "";
182 if ( length $AutoSubmitted and $AutoSubmitted ne "no" ) {
186 # First Class mailer uses this as a clue.
187 my $FCJunk = $head->get("X-FC-Machinegenerated") || "";
188 if ( $FCJunk =~ /^true/i ) {
199 my $ReturnPath = $head->get("Return-path") || "";
200 return ( $ReturnPath =~ /<>/ );
204 =head2 MailError PARAM HASH
206 Sends an error message. Takes a param hash:
210 =item From - sender's address, by default is 'CorrespondAddress';
212 =item To - recipient, by default is 'OwnerEmail';
214 =item Bcc - optional Bcc recipients;
216 =item Subject - subject of the message, default is 'There has been an error';
218 =item Explanation - main content of the error, default value is 'Unexplained error';
220 =item MIMEObj - optional MIME entity that's attached to the error mail, as well we
221 add 'In-Reply-To' field to the error that points to this message.
223 =item Attach - optional text that attached to the error as 'message/rfc822' part.
225 =item LogLevel - log level under which we should write explanation message into the
226 log, by default we log it as critical.
234 To => RT->Config->Get('OwnerEmail'),
236 From => RT->Config->Get('CorrespondAddress'),
237 Subject => 'There has been an error',
238 Explanation => 'Unexplained error',
246 level => $args{'LogLevel'},
247 message => $args{'Explanation'}
248 ) if $args{'LogLevel'};
250 # the colons are necessary to make ->build include non-standard headers
252 Type => "multipart/mixed",
253 From => $args{'From'},
256 Subject => $args{'Subject'},
257 'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'),
260 # only set precedence if the sysadmin wants us to
261 if (defined(RT->Config->Get('DefaultErrorMailPrecedence'))) {
262 $entity_args{'Precedence:'} = RT->Config->Get('DefaultErrorMailPrecedence');
265 my $entity = MIME::Entity->build(%entity_args);
266 SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} );
268 $entity->attach( Data => $args{'Explanation'} . "\n" );
270 if ( $args{'MIMEObj'} ) {
271 $args{'MIMEObj'}->sync_headers;
272 $entity->add_part( $args{'MIMEObj'} );
275 if ( $args{'Attach'} ) {
276 $entity->attach( Data => $args{'Attach'}, Type => 'message/rfc822' );
280 SendEmail( Entity => $entity, Bounce => 1 );
284 =head2 SendEmail Entity => undef, [ Bounce => 0, Ticket => undef, Transaction => undef ]
286 Sends an email (passed as a L<MIME::Entity> object C<ENTITY>) using
287 RT's outgoing mail configuration. If C<BOUNCE> is passed, and is a
288 true value, the message will be marked as an autogenerated error, if
289 possible. Sets Date field of the head to now if it's not set.
291 If the C<X-RT-Squelch> header is set to any true value, the mail will
292 not be sent. One use is to let extensions easily cancel outgoing mail.
294 Ticket and Transaction arguments are optional. If Transaction is
295 specified and Ticket is not then ticket of the transaction is
296 used, but only if the transaction belongs to a ticket.
298 Returns 1 on success, 0 on error or -1 if message has no recipients
299 and hasn't been sent.
301 =head3 Signing and Encrypting
303 This function as well signs and/or encrypts the message according to
304 headers of a transaction's attachment or properties of a ticket's queue.
305 To get full access to the configuration Ticket and/or Transaction
306 arguments must be provided, but you can force behaviour using Sign
307 and/or Encrypt arguments.
309 The following precedence of arguments are used to figure out if
310 the message should be encrypted and/or signed:
312 * if Sign or Encrypt argument is defined then its value is used
314 * else if Transaction's first attachment has X-RT-Sign or X-RT-Encrypt
315 header field then it's value is used
317 * else properties of a queue of the Ticket are used.
326 Transaction => undef,
330 my $TicketObj = $args{'Ticket'};
331 my $TransactionObj = $args{'Transaction'};
333 foreach my $arg( qw(Entity Bounce) ) {
334 next unless defined $args{ lc $arg };
336 $RT::Logger->warning("'". lc($arg) ."' argument is deprecated, use '$arg' instead");
337 $args{ $arg } = delete $args{ lc $arg };
340 unless ( $args{'Entity'} ) {
341 $RT::Logger->crit( "Could not send mail without 'Entity' object" );
345 my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
348 # If we don't have any recipients to send to, don't send a message;
349 unless ( $args{'Entity'}->head->get('To')
350 || $args{'Entity'}->head->get('Cc')
351 || $args{'Entity'}->head->get('Bcc') )
353 $RT::Logger->info( $msgid . " No recipients found. Not sending." );
357 if ($args{'Entity'}->head->get('X-RT-Squelch')) {
358 $RT::Logger->info( $msgid . " Squelch header found. Not sending." );
362 if ( $TransactionObj && !$TicketObj
363 && $TransactionObj->ObjectType eq 'RT::Ticket' )
365 $TicketObj = $TransactionObj->Object;
368 if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
372 $attachment = $TransactionObj->Attachments->First
375 foreach my $argument ( qw(Sign Encrypt) ) {
376 next if defined $args{ $argument };
378 if ( $attachment && defined $attachment->GetHeader("X-RT-$argument") ) {
379 $crypt{$argument} = $attachment->GetHeader("X-RT-$argument");
380 } elsif ( $TicketObj ) {
381 $crypt{$argument} = $TicketObj->QueueObj->$argument();
385 my $res = SignEncrypt( %args, %crypt );
386 return $res unless $res > 0;
389 unless ( $args{'Entity'}->head->get('Date') ) {
391 my $date = RT::Date->new( $RT::SystemUser );
393 $args{'Entity'}->head->set( 'Date', $date->RFC2822( Timezone => 'server' ) );
396 my $mail_command = RT->Config->Get('MailCommand');
398 if ($mail_command eq 'testfile' and not $Mail::Mailer::testfile::config{outfile}) {
399 $Mail::Mailer::testfile::config{outfile} = File::Temp->new;
400 $RT::Logger->info("Storing outgoing emails in $Mail::Mailer::testfile::config{outfile}");
403 # if it is a sub routine, we just return it;
404 return $mail_command->($args{'Entity'}) if UNIVERSAL::isa( $mail_command, 'CODE' );
406 if ( $mail_command eq 'sendmailpipe' ) {
407 my $path = RT->Config->Get('SendmailPath');
408 my @args = shellwords(RT->Config->Get('SendmailArguments'));
410 # SetOutgoingMailFrom
411 if ( RT->Config->Get('SetOutgoingMailFrom') ) {
412 my $OutgoingMailAddress;
415 my $QueueName = $TicketObj->QueueObj->Name;
416 my $QueueAddressOverride = RT->Config->Get('OverrideOutgoingMailFrom')->{$QueueName};
418 if ($QueueAddressOverride) {
419 $OutgoingMailAddress = $QueueAddressOverride;
421 $OutgoingMailAddress = $TicketObj->QueueObj->CorrespondAddress;
425 $OutgoingMailAddress ||= RT->Config->Get('OverrideOutgoingMailFrom')->{'Default'};
427 push @args, "-f", $OutgoingMailAddress
428 if $OutgoingMailAddress;
431 # Set Bounce Arguments
432 push @args, shellwords(RT->Config->Get('SendmailBounceArguments'))
436 if ( $TransactionObj and
437 my $prefix = RT->Config->Get('VERPPrefix') and
438 my $domain = RT->Config->Get('VERPDomain') )
440 my $from = $TransactionObj->CreatorObj->EmailAddress;
443 push @args, "-f", "$prefix$from\@$domain";
447 # don't ignore CHLD signal to get proper exit code
448 local $SIG{'CHLD'} = 'DEFAULT';
450 # if something wrong with $mail->print we will get PIPE signal, handle it
451 local $SIG{'PIPE'} = sub { die "program unexpectedly closed pipe" };
453 # Make it look to open2 like STDIN is on FD 0, like it
454 # should be; this is necessary because under mod_perl with
455 # the perl-script handler, it's not. This causes our
456 # child's "STDIN" (FD 10-ish) to be set to the pipe we want,
457 # but FD 0 (which the exec'd sendmail assumes is STDIN) is
458 # still open to /dev/null; this ends disasterously.
459 local *STDIN = IO::Handle->new_from_fd( 0, "r" );
463 my $pid = IPC::Open2::open2( $stdout, $mail, $path, @args )
464 or die "couldn't execute program: $!";
466 $args{'Entity'}->print($mail);
467 close $mail or die "close pipe failed: $!";
471 # sendmail exit statuses mostly errors with data not software
472 # TODO: status parsing: core dump, exit on signal or EX_*
473 my $msg = "$msgid: `$path @args` exited with code ". ($?>>8);
474 $msg = ", interrupted by signal ". ($?&127) if $?&127;
475 $RT::Logger->error( $msg );
480 $RT::Logger->crit( "$msgid: Could not send mail with command `$path @args`: " . $@ );
482 _RecordSendEmailFailure( $TicketObj );
487 elsif ( $mail_command eq 'smtp' ) {
489 my $smtp = do { local $@; eval { Net::SMTP->new(
490 Host => RT->Config->Get('SMTPServer'),
491 Debug => RT->Config->Get('SMTPDebug'),
494 $RT::Logger->crit( "Could not connect to SMTP server.");
496 _RecordSendEmailFailure( $TicketObj );
501 # duplicate head as we want drop Bcc field
502 my $head = $args{'Entity'}->head->dup;
503 my @recipients = map $_->address, map
504 Email::Address->parse($head->get($_)), qw(To Cc Bcc);
505 $head->delete('Bcc');
507 my $sender = RT->Config->Get('SMTPFrom')
508 || $args{'Entity'}->head->get('From');
511 my $status = $smtp->mail( $sender )
512 && $smtp->recipient( @recipients );
516 my $fh = $smtp->tied_fh;
519 $args{'Entity'}->print_body( $fh );
525 $RT::Logger->crit( "$msgid: Could not send mail via SMTP." );
527 _RecordSendEmailFailure( $TicketObj );
533 local ($ENV{'MAILADDRESS'}, $ENV{'PERL_MAILERS'});
535 my @mailer_args = ($mail_command);
536 if ( $mail_command eq 'sendmail' ) {
537 $ENV{'PERL_MAILERS'} = RT->Config->Get('SendmailPath');
538 push @mailer_args, split(/\s+/, RT->Config->Get('SendmailArguments'));
541 push @mailer_args, RT->Config->Get('MailParams');
544 unless ( $args{'Entity'}->send( @mailer_args ) ) {
545 $RT::Logger->crit( "$msgid: Could not send mail." );
547 _RecordSendEmailFailure( $TicketObj );
555 =head2 PrepareEmailUsingTemplate Template => '', Arguments => {}
557 Loads a template. Parses it using arguments if it's not empty.
558 Returns a tuple (L<RT::Template> object, error message).
560 Note that even if a template object is returned MIMEObj method
561 may return undef for empty templates.
565 sub PrepareEmailUsingTemplate {
572 my $template = RT::Template->new( $RT::SystemUser );
573 $template->LoadGlobalTemplate( $args{'Template'} );
574 unless ( $template->id ) {
575 return (undef, "Couldn't load template '". $args{'Template'} ."'");
577 return $template if $template->IsEmpty;
579 my ($status, $msg) = $template->Parse( %{ $args{'Arguments'} } );
580 return (undef, $msg) unless $status;
585 =head2 SendEmailUsingTemplate Template => '', Arguments => {}, From => CorrespondAddress, To => '', Cc => '', Bcc => ''
587 Sends email using a template, takes name of template, arguments for it and recipients.
591 sub SendEmailUsingTemplate {
598 From => RT->Config->Get('CorrespondAddress'),
603 my ($template, $msg) = PrepareEmailUsingTemplate( %args );
604 return (0, $msg) unless $template;
606 my $mail = $template->MIMEObj;
608 $RT::Logger->info("Message is not sent as template #". $template->id ." is empty");
612 $mail->head->set( $_ => Encode::encode_utf8( $args{ $_ } ) )
613 foreach grep defined $args{$_}, qw(To Cc Bcc From);
615 SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
617 return SendEmail( Entity => $mail );
620 =head2 ForwardTransaction TRANSACTION, To => '', Cc => '', Bcc => ''
622 Forwards transaction with all attachments as 'message/rfc822'.
626 sub ForwardTransaction {
628 my %args = ( To => '', Cc => '', Bcc => '', @_ );
630 my $entity = $txn->ContentAsMIME;
632 return SendForward( %args, Entity => $entity, Transaction => $txn );
635 =head2 ForwardTicket TICKET, To => '', Cc => '', Bcc => ''
637 Forwards a ticket's Create and Correspond Transactions and their Attachments as 'message/rfc822'.
643 my %args = ( To => '', Cc => '', Bcc => '', @_ );
645 my $txns = $ticket->Transactions;
649 ) for qw(Create Correspond);
651 my $entity = MIME::Entity->build(
652 Type => 'multipart/mixed',
654 $entity->add_part( $_ ) foreach
655 map $_->ContentAsMIME,
656 @{ $txns->ItemsArrayRef };
658 return SendForward( %args, Entity => $entity, Ticket => $ticket, Template => 'Forward Ticket' );
661 =head2 SendForward Entity => undef, Ticket => undef, Transaction => undef, Template => undef, To => '', Cc => '', Bcc => ''
663 Forwards an Entity representing Ticket or Transaction as 'message/rfc822'. Entity is wrapped into Template.
671 Transaction => undef,
672 Template => 'Forward',
673 To => '', Cc => '', Bcc => '',
677 my $txn = $args{'Transaction'};
678 my $ticket = $args{'Ticket'};
679 $ticket ||= $txn->Object if $txn;
681 my $entity = $args{'Entity'};
684 $RT::Logger->error(Carp::longmess("No entity provided"));
685 return (0, $ticket->loc("Couldn't send email"));
688 my ($template, $msg) = PrepareEmailUsingTemplate(
689 Template => $args{'Template'},
698 $mail = $template->MIMEObj;
700 $RT::Logger->warning($msg);
703 $RT::Logger->warning("Couldn't generate email using template '$args{Template}'");
706 unless ( $args{'Transaction'} ) {
707 $description = 'This is forward of ticket #'. $ticket->id;
709 $description = 'This is forward of transaction #'
710 . $txn->id ." of a ticket #". $txn->ObjectId;
712 $mail = MIME::Entity->build(
713 Type => 'text/plain',
714 Data => $description,
718 $mail->head->set( $_ => EncodeToMIME( String => $args{$_} ) )
719 foreach grep defined $args{$_}, qw(To Cc Bcc);
722 Type => 'message/rfc822',
723 Disposition => 'attachment',
724 Description => 'forwarded message',
725 Data => $entity->as_string,
730 $subject = $txn->Subject if $txn;
731 $subject ||= $ticket->Subject if $ticket;
732 if ( RT->Config->Get('ForwardFromUser') ) {
733 $from = ($txn || $ticket)->CurrentUser->UserObj->EmailAddress;
735 # XXX: what if want to forward txn of other object than ticket?
736 $subject = AddSubjectTag( $subject, $ticket );
737 $from = $ticket->QueueObj->CorrespondAddress
738 || RT->Config->Get('CorrespondAddress');
740 $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) );
741 $mail->head->set( From => EncodeToMIME( String => $from ) );
743 my $status = RT->Config->Get('ForwardFromUser')
744 # never sign if we forward from User
745 ? SendEmail( %args, Entity => $mail, Sign => 0 )
746 : SendEmail( %args, Entity => $mail );
747 return (0, $ticket->loc("Couldn't send email")) unless $status;
748 return (1, $ticket->loc("Send email successfully"));
751 =head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0
753 Signs and encrypts message using L<RT::Crypt::GnuPG>, but as well
754 handle errors with users' keys.
756 If a recipient has no key or has other problems with it, then the
757 unction sends a error to him using 'Error: public key' template.
758 Also, notifies RT's owner using template 'Error to RT owner: public key'
759 to inform that there are problems with users' keys. Then we filter
760 all bad recipients and retry.
762 Returns 1 on success, 0 on error and -1 if all recipients are bad and
763 had been filtered out.
774 return 1 unless $args{'Sign'} || $args{'Encrypt'};
776 my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
779 $RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
780 $RT::Logger->debug("$msgid Encrypting message") if $args{'Encrypt'};
782 require RT::Crypt::GnuPG;
783 my %res = RT::Crypt::GnuPG::SignEncrypt( %args );
784 return 1 unless $res{'exit_code'};
786 my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
789 foreach my $line ( @status ) {
790 # if the passphrase fails, either you have a bad passphrase
791 # or gpg-agent has died. That should get caught in Create and
792 # Update, but at least throw an error here
793 if (($line->{'Operation'}||'') eq 'PassphraseCheck'
794 && $line->{'Status'} =~ /^(?:BAD|MISSING)$/ ) {
795 $RT::Logger->error( "$line->{'Status'} PASSPHRASE: $line->{'Message'}" );
798 next unless ($line->{'Operation'}||'') eq 'RecipientsCheck';
799 next if $line->{'Status'} eq 'DONE';
800 $RT::Logger->error( $line->{'Message'} );
801 push @bad_recipients, $line;
803 return 0 unless @bad_recipients;
805 $_->{'AddressObj'} = (Email::Address->parse( $_->{'Recipient'} ))[0]
806 foreach @bad_recipients;
808 foreach my $recipient ( @bad_recipients ) {
809 my $status = SendEmailUsingTemplate(
810 To => $recipient->{'AddressObj'}->address,
811 Template => 'Error: public key',
814 TicketObj => $args{'Ticket'},
815 TransactionObj => $args{'Transaction'},
819 $RT::Logger->error("Couldn't send 'Error: public key'");
823 my $status = SendEmailUsingTemplate(
824 To => RT->Config->Get('OwnerEmail'),
825 Template => 'Error to RT owner: public key',
827 BadRecipients => \@bad_recipients,
828 TicketObj => $args{'Ticket'},
829 TransactionObj => $args{'Transaction'},
833 $RT::Logger->error("Couldn't send 'Error to RT owner: public key'");
836 DeleteRecipientsFromHead(
837 $args{'Entity'}->head,
838 map $_->{'AddressObj'}->address, @bad_recipients
841 unless ( $args{'Entity'}->head->get('To')
842 || $args{'Entity'}->head->get('Cc')
843 || $args{'Entity'}->head->get('Bcc') )
845 $RT::Logger->debug("$msgid No recipients that have public key, not sending");
849 # redo without broken recipients
850 %res = RT::Crypt::GnuPG::SignEncrypt( %args );
851 return 0 if $res{'exit_code'};
860 Takes a hash with a String and a Charset. Returns the string encoded
861 according to RFC2047, using B (base64 based) encoding.
863 String must be a perl string, octets are returned.
865 If Charset is not provided then $EmailOutputEncoding config option
866 is used, or "latin-1" if that is not set.
876 my $value = $args{'String'};
877 return $value unless $value; # 0 is perfect ascii
878 my $charset = $args{'Charset'} || RT->Config->Get('EmailOutputEncoding');
881 # using RFC2047 notation, sec 2.
882 # encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
884 # An 'encoded-word' may not be more than 75 characters long
886 # MIME encoding increases 4/3*(number of bytes), and always in multiples
887 # of 4. Thus we have to find the best available value of bytes available
890 # First we get the integer max which max*4/3 would fit on space.
891 # Then we find the greater multiple of 3 lower or equal than $max.
893 ( ( 75 - length( '=?' . $charset . '?' . $encoding . '?' . '?=' ) )
897 $max = int( $max / 3 ) * 3;
904 $RT::Logger->crit("Can't encode! Charset or encoding too big.");
908 return ($value) unless $value =~ /[^\x20-\x7e]/;
912 # we need perl string to split thing char by char
913 Encode::_utf8_on($value) unless Encode::is_utf8($value);
915 my ( $tmp, @chunks ) = ( '', () );
916 while ( length $value ) {
917 my $char = substr( $value, 0, 1, '' );
918 my $octets = Encode::encode( $charset, $char );
919 if ( length($tmp) + length($octets) > $max ) {
925 push @chunks, $tmp if length $tmp;
927 # encode an join chuncks
929 map MIME::Words::encode_mimeword( $_, $encoding, $charset ),
935 my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_;
937 my $NewUser = RT::User->new( $RT::SystemUser );
939 my ( $Val, $Message ) = $NewUser->Create(
940 Name => ( $Username || $Address ),
941 EmailAddress => $Address,
945 Comments => 'Autocreated on ticket submission',
950 # Deal with the race condition of two account creations at once
952 $NewUser->LoadByName($Username);
955 unless ( $NewUser->Id ) {
956 $NewUser->LoadByEmail($Address);
959 unless ( $NewUser->Id ) {
962 Subject => "User could not be created",
964 "User creation failed in mailgateway: $Message",
971 #Load the new user object
972 my $CurrentUser = new RT::CurrentUser;
973 $CurrentUser->LoadByEmail( $Address );
975 unless ( $CurrentUser->id ) {
976 $RT::Logger->warning(
977 "Couldn't load user '$Address'." . "giving up" );
980 Subject => "User could not be loaded",
982 "User '$Address' could not be loaded in the mail gateway",
993 =head2 ParseCcAddressesFromHead HASH
995 Takes a hash containing QueueObj, Head and CurrentUser objects.
996 Returns a list of all email addresses in the To and Cc
997 headers b<except> the current Queue\'s email addresses, the CurrentUser\'s
998 email address and anything that the configuration sub RT::IsRTAddress matches.
1002 sub ParseCcAddressesFromHead {
1006 CurrentUser => undef,
1010 my $current_address = lc $args{'CurrentUser'}->EmailAddress;
1011 my $user = $args{'CurrentUser'}->UserObj;
1014 grep { $_ ne $current_address
1015 && !RT::EmailParser->IsRTAddress( $_ )
1016 && !IgnoreCcAddress( $_ )
1018 map lc $user->CanonicalizeEmailAddress( $_->address ),
1019 map Email::Address->parse( $args{'Head'}->get( $_ ) ),
1023 =head2 IgnoreCcAddress ADDRESS
1025 Returns true if ADDRESS matches the $IgnoreCcRegexp config variable.
1029 sub IgnoreCcAddress {
1030 my $address = shift;
1031 if ( my $address_re = RT->Config->Get('IgnoreCcRegexp') ) {
1032 return 1 if $address =~ /$address_re/i;
1037 =head2 ParseSenderAddressFromHead HEAD
1039 Takes a MIME::Header object. Returns a tuple: (user@host, friendly name)
1040 of the From (evaluated in order of Reply-To:, From:, Sender)
1044 sub ParseSenderAddressFromHead {
1047 #Figure out who's sending this message.
1048 foreach my $header ('Reply-To', 'From', 'Sender') {
1049 my $addr_line = $head->get($header) || next;
1050 my ($addr, $name) = ParseAddressFromHeader( $addr_line );
1051 # only return if the address is not empty
1052 return ($addr, $name) if $addr;
1055 return (undef, undef);
1058 =head2 ParseErrorsToAddressFromHead HEAD
1060 Takes a MIME::Header object. Return a single value : user@host
1061 of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
1066 sub ParseErrorsToAddressFromHead {
1069 #Figure out who's sending this message.
1071 foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) {
1073 # If there's a header of that name
1074 my $headerobj = $head->get($header);
1076 my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
1078 # If it's got actual useful content...
1079 return ($addr) if ($addr);
1086 =head2 ParseAddressFromHeader ADDRESS
1088 Takes an address from C<$head->get('Line')> and returns a tuple: user@host, friendly name
1092 sub ParseAddressFromHeader {
1095 # Some broken mailers send: ""Vincent, Jesse"" <jesse@fsck.com>. Hate
1096 $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
1097 my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
1099 my ($AddrObj) = grep ref $_, @Addresses;
1100 unless ( $AddrObj ) {
1101 return ( undef, undef );
1104 my $Name = ( $AddrObj->name || $AddrObj->phrase || $AddrObj->comment || $AddrObj->address );
1106 #Lets take the from and load a user object.
1107 my $Address = $AddrObj->address;
1109 return ( $Address, $Name );
1112 =head2 DeleteRecipientsFromHead HEAD RECIPIENTS
1114 Gets a head object and list of addresses.
1115 Deletes addresses from To, Cc or Bcc fields.
1119 sub DeleteRecipientsFromHead {
1121 my %skip = map { lc $_ => 1 } @_;
1123 foreach my $field ( qw(To Cc Bcc) ) {
1124 $head->set( $field =>
1125 join ', ', map $_->format, grep !$skip{ lc $_->address },
1126 Email::Address->parse( $head->get( $field ) )
1135 ScripAction => undef,
1138 my $org = RT->Config->Get('Organization');
1139 my $ticket_id = ( ref $args{'Ticket'}? $args{'Ticket'}->id : $args{'Ticket'} ) || 0;
1140 my $scrip_id = ( ref $args{'Scrip'}? $args{'Scrip'}->id : $args{'Scrip'} ) || 0;
1141 my $sent = ( ref $args{'ScripAction'}? $args{'ScripAction'}->{'_Message_ID'} : 0 ) || 0;
1143 return "<rt-". $RT::VERSION ."-". $$ ."-". CORE::time() ."-". int(rand(2000)) .'.'
1144 . $ticket_id ."-". $scrip_id ."-". $sent ."@". $org .">" ;
1154 return unless $args{'Message'} && $args{'InReplyTo'};
1156 my $get_header = sub {
1158 if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
1159 @res = $args{'InReplyTo'}->head->get( shift );
1161 @res = $args{'InReplyTo'}->GetHeader( shift ) || '';
1163 return grep length, map { split /\s+/m, $_ } grep defined, @res;
1166 my @id = $get_header->('Message-ID');
1167 #XXX: custom header should begin with X- otherwise is violation of the standard
1168 my @rtid = $get_header->('RT-Message-ID');
1169 my @references = $get_header->('References');
1170 unless ( @references ) {
1171 @references = $get_header->('In-Reply-To');
1173 push @references, @id, @rtid;
1174 if ( $args{'Ticket'} ) {
1175 my $pseudo_ref = '<RT-Ticket-'. $args{'Ticket'}->id .'@'. RT->Config->Get('Organization') .'>';
1176 push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references;
1178 @references = splice @references, 4, -6
1179 if @references > 10;
1181 my $mail = $args{'Message'};
1182 $mail->head->set( 'In-Reply-To' => join ' ', @rtid? (@rtid) : (@id) ) if @id || @rtid;
1183 $mail->head->set( 'References' => join ' ', @references );
1187 my $Subject = shift;
1189 my $rtname = RT->Config->Get('rtname');
1190 my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;
1193 if ( $Subject =~ s/\[$test_name\s+\#(\d+)\s*\]//i ) {
1196 foreach my $tag ( RT->System->SubjectTag ) {
1197 next unless $Subject =~ s/\[\Q$tag\E\s+\#(\d+)\s*\]//i;
1202 return undef unless $id;
1204 $RT::Logger->debug("Found a ticket ID. It's $id");
1209 my $subject = shift;
1211 unless ( ref $ticket ) {
1212 my $tmp = RT::Ticket->new( $RT::SystemUser );
1213 $tmp->Load( $ticket );
1216 my $id = $ticket->id;
1217 my $queue_tag = $ticket->QueueObj->SubjectTag;
1219 my $tag_re = RT->Config->Get('EmailSubjectTagRegex');
1220 unless ( $tag_re ) {
1221 my $tag = $queue_tag || RT->Config->Get('rtname');
1222 $tag_re = qr/\Q$tag\E/;
1223 } elsif ( $queue_tag ) {
1224 $tag_re = qr/$tag_re|\Q$queue_tag\E/;
1226 return $subject if $subject =~ /\[$tag_re\s+#$id\]/;
1228 $subject =~ s/(\r\n|\n|\s)/ /gi;
1230 return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject";
1234 =head2 Gateway ARGSREF
1244 This performs all the "guts" of the mail rt-mailgate program, and is
1245 designed to be called from the web interface with a message, user
1248 Can also take an optional 'ticket' parameter; this ticket id overrides
1249 any ticket id found in the subject.
1255 (status code, message, optional ticket object)
1257 status code is a numeric value.
1259 for temporary failures, the status code should be -75
1261 for permanent failures which are handled by RT, the status code
1264 for succces, the status code should be 1
1271 my @mail_plugins = @_;
1274 foreach my $plugin (@mail_plugins) {
1275 if ( ref($plugin) eq "CODE" ) {
1277 } elsif ( !ref $plugin ) {
1278 my $Class = $plugin;
1279 $Class = "RT::Interface::Email::" . $Class
1280 unless $Class =~ /^RT::Interface::Email::/;
1282 do { $RT::Logger->error("Couldn't load $Class: $@"); next };
1285 unless ( defined *{ $Class . "::GetCurrentUser" }{CODE} ) {
1286 $RT::Logger->crit( "No GetCurrentUser code found in $Class module");
1291 $RT::Logger->crit( "$plugin - is not class name or code reference");
1298 my $argsref = shift;
1300 action => 'correspond',
1310 # Validate the action
1311 my ( $status, @actions ) = IsCorrectAction( $args{'action'} );
1315 "Invalid 'action' parameter "
1323 my $parser = RT::EmailParser->new();
1324 $parser->SmartParseMIMEEntityFromScalar(
1325 Message => $args{'message'},
1330 my $Message = $parser->Entity();
1333 Subject => "RT Bounce: Unparseable message",
1334 Explanation => "RT couldn't process the message below",
1335 Attach => $args{'message'}
1339 "Failed to parse this message. Something is likely badly wrong with the message"
1343 my @mail_plugins = grep $_, RT->Config->Get('MailPlugins');
1344 push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
1345 @mail_plugins = _LoadPlugins( @mail_plugins );
1348 foreach my $class( grep !ref, @mail_plugins ) {
1349 # check if we should apply filter before decoding
1352 *{ $class . "::ApplyBeforeDecode" }{CODE};
1354 next unless defined $check_cb;
1355 next unless $check_cb->(
1356 Message => $Message,
1357 RawMessageRef => \$args{'message'},
1360 $skip_plugin{ $class }++;
1364 *{ $class . "::GetCurrentUser" }{CODE};
1366 my ($status, $msg) = $Code->(
1367 Message => $Message,
1368 RawMessageRef => \$args{'message'},
1370 next if $status > 0;
1372 if ( $status == -2 ) {
1373 return (1, $msg, undef);
1374 } elsif ( $status == -1 ) {
1375 return (0, $msg, undef);
1378 @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins;
1379 $parser->_DecodeBodies;
1380 $parser->_PostProcessNewEntity;
1382 my $head = $Message->head;
1383 my $ErrorsTo = ParseErrorsToAddressFromHead( $head );
1385 my $MessageId = $head->get('Message-ID')
1386 || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
1388 #Pull apart the subject line
1389 my $Subject = $head->get('Subject') || '';
1392 # {{{ Lets check for mail loops of various sorts.
1393 my ($should_store_machine_generated_message, $IsALoop, $result);
1394 ( $should_store_machine_generated_message, $ErrorsTo, $result, $IsALoop ) =
1395 _HandleMachineGeneratedMail(
1396 Message => $Message,
1397 ErrorsTo => $ErrorsTo,
1398 Subject => $Subject,
1399 MessageId => $MessageId
1402 # Do not pass loop messages to MailPlugins, to make sure the loop
1403 # is broken, unless $RT::StoreLoops is set.
1404 if ($IsALoop && !$should_store_machine_generated_message) {
1405 return ( 0, $result, undef );
1409 $args{'ticket'} ||= ParseTicketId( $Subject );
1411 $SystemTicket = RT::Ticket->new( $RT::SystemUser );
1412 $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
1413 if ( $SystemTicket->id ) {
1414 $Right = 'ReplyToTicket';
1416 $Right = 'CreateTicket';
1419 #Set up a queue object
1420 my $SystemQueueObj = RT::Queue->new( $RT::SystemUser );
1421 $SystemQueueObj->Load( $args{'queue'} );
1423 # We can safely have no queue of we have a known-good ticket
1424 unless ( $SystemTicket->id || $SystemQueueObj->id ) {
1425 return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
1428 my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel(
1429 MailPlugins => \@mail_plugins,
1430 Actions => \@actions,
1431 Message => $Message,
1432 RawMessageRef => \$args{message},
1433 SystemTicket => $SystemTicket,
1434 SystemQueue => $SystemQueueObj,
1437 # {{{ If authentication fails and no new user was created, get out.
1438 if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) {
1440 # If the plugins refused to create one, they lose.
1441 unless ( $AuthStat == -1 ) {
1442 _NoAuthorizedUserFound(
1444 Message => $Message,
1445 Requestor => $ErrorsTo,
1446 Queue => $args{'queue'}
1450 return ( 0, "Could not load a valid user", undef );
1453 # If we got a user, but they don't have the right to say things
1454 if ( $AuthStat == 0 ) {
1457 Subject => "Permission Denied",
1459 "You do not have permission to communicate with RT",
1464 "$ErrorsTo tried to submit a message to "
1466 . " without permission.",
1472 unless ($should_store_machine_generated_message) {
1473 return ( 0, $result, undef );
1476 # if plugin's updated SystemTicket then update arguments
1477 $args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id;
1479 my $Ticket = RT::Ticket->new($CurrentUser);
1481 if ( !$args{'ticket'} && grep /^(comment|correspond)$/, @actions )
1485 my @Requestors = ( $CurrentUser->id );
1487 if (RT->Config->Get('ParseNewMessageForTicketCcs')) {
1488 @Cc = ParseCcAddressesFromHead(
1490 CurrentUser => $CurrentUser,
1491 QueueObj => $SystemQueueObj
1495 my ( $id, $Transaction, $ErrStr ) = $Ticket->Create(
1496 Queue => $SystemQueueObj->Id,
1497 Subject => $Subject,
1498 Requestor => \@Requestors,
1505 Subject => "Ticket creation failed: $Subject",
1506 Explanation => $ErrStr,
1509 return ( 0, "Ticket creation failed: $ErrStr", $Ticket );
1512 # strip comments&corresponds from the actions we don't need
1513 # to record them if we've created the ticket just now
1514 @actions = grep !/^(comment|correspond)$/, @actions;
1515 $args{'ticket'} = $id;
1517 } elsif ( $args{'ticket'} ) {
1519 $Ticket->Load( $args{'ticket'} );
1520 unless ( $Ticket->Id ) {
1521 my $error = "Could not find a ticket with id " . $args{'ticket'};
1524 Subject => "Message not recorded: $Subject",
1525 Explanation => $error,
1529 return ( 0, $error );
1531 $args{'ticket'} = $Ticket->id;
1533 return ( 1, "Success", $Ticket );
1538 my $unsafe_actions = RT->Config->Get('UnsafeEmailCommands');
1539 foreach my $action (@actions) {
1541 # If the action is comment, add a comment.
1542 if ( $action =~ /^(?:comment|correspond)$/i ) {
1543 my $method = ucfirst lc $action;
1544 my ( $status, $msg ) = $Ticket->$method( MIMEObj => $Message );
1547 #Warn the sender that we couldn't actually submit the comment.
1550 Subject => "Message not recorded: $Subject",
1551 Explanation => $msg,
1554 return ( 0, "Message not recorded: $msg", $Ticket );
1556 } elsif ($unsafe_actions) {
1557 my ( $status, $msg ) = _RunUnsafeAction(
1559 ErrorsTo => $ErrorsTo,
1560 Message => $Message,
1562 CurrentUser => $CurrentUser,
1564 return ($status, $msg, $Ticket) unless $status == 1;
1567 return ( 1, "Success", $Ticket );
1570 =head2 GetAuthenticationLevel
1572 # Authentication Level
1573 # -1 - Get out. this user has been explicitly declined
1574 # 0 - User may not do anything (Not used at the moment)
1576 # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate
1580 sub GetAuthenticationLevel {
1585 RawMessageRef => undef,
1586 SystemTicket => undef,
1587 SystemQueue => undef,
1591 my ( $CurrentUser, $AuthStat, $error );
1593 # Initalize AuthStat so comparisons work correctly
1594 $AuthStat = -9999999;
1596 # if plugin returns AuthStat -2 we skip action
1597 # NOTE: this is experimental API and it would be changed
1598 my %skip_action = ();
1600 # Since this needs loading, no matter what
1601 foreach (@{ $args{MailPlugins} }) {
1602 my ($Code, $NewAuthStat);
1603 if ( ref($_) eq "CODE" ) {
1607 $Code = *{ $_ . "::GetCurrentUser" }{CODE};
1610 foreach my $action (@{ $args{Actions} }) {
1611 ( $CurrentUser, $NewAuthStat ) = $Code->(
1612 Message => $args{Message},
1613 RawMessageRef => $args{RawMessageRef},
1614 CurrentUser => $CurrentUser,
1615 AuthLevel => $AuthStat,
1617 Ticket => $args{SystemTicket},
1618 Queue => $args{SystemQueue},
1621 # You get the highest level of authentication you were assigned, unless you get the magic -1
1622 # If a module returns a "-1" then we discard the ticket, so.
1623 $AuthStat = $NewAuthStat
1624 if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
1626 last if $AuthStat == -1;
1627 $skip_action{$action}++ if $AuthStat == -2;
1630 # strip actions we should skip
1631 @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
1633 last unless @{$args{Actions}};
1635 last if $AuthStat == -1;
1638 return $AuthStat if !wantarray;
1640 return ($AuthStat, $CurrentUser, $error);
1643 sub _RunUnsafeAction {
1649 CurrentUser => undef,
1653 if ( $args{'Action'} =~ /^take$/i ) {
1654 my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
1657 To => $args{'ErrorsTo'},
1658 Subject => "Ticket not taken",
1659 Explanation => $msg,
1660 MIMEObj => $args{'Message'}
1662 return ( 0, "Ticket not taken" );
1664 } elsif ( $args{'Action'} =~ /^resolve$/i ) {
1665 my ( $status, $msg ) = $args{'Ticket'}->SetStatus('resolved');
1668 #Warn the sender that we couldn't actually submit the comment.
1670 To => $args{'ErrorsTo'},
1671 Subject => "Ticket not resolved",
1672 Explanation => $msg,
1673 MIMEObj => $args{'Message'}
1675 return ( 0, "Ticket not resolved" );
1678 return ( 0, "Not supported unsafe action $args{'Action'}", $args{'Ticket'} );
1680 return ( 1, "Success" );
1683 =head2 _NoAuthorizedUserFound
1685 Emails the RT Owner and the requestor when the auth plugins return "No auth user found"
1689 sub _NoAuthorizedUserFound {
1698 # Notify the RT Admin of the failure.
1700 To => RT->Config->Get('OwnerEmail'),
1701 Subject => "Could not load a valid user",
1702 Explanation => <<EOT,
1703 RT could not load a valid user, and RT's configuration does not allow
1704 for the creation of a new user for this email (@{[$args{Requestor}]}).
1706 You might need to grant 'Everyone' the right '@{[$args{Right}]}' for the
1707 queue @{[$args{'Queue'}]}.
1710 MIMEObj => $args{'Message'},
1714 # Also notify the requestor that his request has been dropped.
1715 if ($args{'Requestor'} ne RT->Config->Get('OwnerEmail')) {
1717 To => $args{'Requestor'},
1718 Subject => "Could not load a valid user",
1719 Explanation => <<EOT,
1720 RT could not load a valid user, and RT's configuration does not allow
1721 for the creation of a new user for your email.
1724 MIMEObj => $args{'Message'},
1730 =head2 _HandleMachineGeneratedMail
1737 Checks the message to see if it's a bounce, if it looks like a loop, if it's autogenerated, etc.
1738 Returns a triple of ("Should we continue (boolean)", "New value for $ErrorsTo", "Status message",
1739 "This message appears to be a loop (boolean)" );
1743 sub _HandleMachineGeneratedMail {
1744 my %args = ( Message => undef, ErrorsTo => undef, Subject => undef, MessageId => undef, @_ );
1745 my $head = $args{'Message'}->head;
1746 my $ErrorsTo = $args{'ErrorsTo'};
1748 my $IsBounce = CheckForBounce($head);
1750 my $IsAutoGenerated = CheckForAutoGenerated($head);
1752 my $IsSuspiciousSender = CheckForSuspiciousSender($head);
1754 my $IsALoop = CheckForLoops($head);
1756 my $SquelchReplies = 0;
1758 my $owner_mail = RT->Config->Get('OwnerEmail');
1760 #If the message is autogenerated, we need to know, so we can not
1761 # send mail to the sender
1762 if ( $IsBounce || $IsSuspiciousSender || $IsAutoGenerated || $IsALoop ) {
1763 $SquelchReplies = 1;
1764 $ErrorsTo = $owner_mail;
1767 # Warn someone if it's a loop, before we drop it on the ground
1769 $RT::Logger->crit("RT Received mail (".$args{MessageId}.") from itself.");
1771 #Should we mail it to RTOwner?
1772 if ( RT->Config->Get('LoopsToRTOwner') ) {
1775 Subject => "RT Bounce: ".$args{'Subject'},
1776 Explanation => "RT thinks this message may be a bounce",
1777 MIMEObj => $args{Message}
1781 #Do we actually want to store it?
1782 return ( 0, $ErrorsTo, "Message Bounced", $IsALoop )
1783 unless RT->Config->Get('StoreLoops');
1786 # Squelch replies if necessary
1787 # Don't let the user stuff the RT-Squelch-Replies-To header.
1788 if ( $head->get('RT-Squelch-Replies-To') ) {
1790 'RT-Relocated-Squelch-Replies-To',
1791 $head->get('RT-Squelch-Replies-To')
1793 $head->delete('RT-Squelch-Replies-To');
1796 if ($SquelchReplies) {
1798 # Squelch replies to the sender, and also leave a clue to
1799 # allow us to squelch ALL outbound messages. This way we
1800 # can punt the logic of "what to do when we get a bounce"
1801 # to the scrip. We might want to notify nobody. Or just
1802 # the RT Owner. Or maybe all Privileged watchers.
1803 my ( $Sender, $junk ) = ParseSenderAddressFromHead($head);
1804 $head->add( 'RT-Squelch-Replies-To', $Sender );
1805 $head->add( 'RT-DetectedAutoGenerated', 'true' );
1807 return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
1810 =head2 IsCorrectAction
1812 Returns a list of valid actions we've found for this message
1816 sub IsCorrectAction {
1818 my @actions = grep $_, split /-/, $action;
1819 return ( 0, '(no value)' ) unless @actions;
1820 foreach ( @actions ) {
1821 return ( 0, $_ ) unless /^(?:comment|correspond|take|resolve)$/;
1823 return ( 1, @actions );
1826 sub _RecordSendEmailFailure {
1829 $ticket->_RecordNote(
1830 NoteType => 'SystemError',
1831 Content => "Sending the previous mail has failed. Please contact your admin, they can find more details in the logs.",
1836 $RT::Logger->error( "Can't record send email failure as ticket is missing" );
1841 RT::Base->_ImportOverlays();