RT 3.8.17
[freeside.git] / rt / lib / RT / Interface / Email.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
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
16 # from www.gnu.org.
17 #
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.
22 #
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.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
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.)
37 #
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.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Interface::Email;
50
51 use strict;
52 use warnings;
53
54 use Email::Address;
55 use MIME::Entity;
56 use RT::EmailParser;
57 use File::Temp;
58 use UNIVERSAL::require;
59 use Mail::Mailer ();
60 use Text::ParseWords qw/shellwords/;
61
62 BEGIN {
63     use base 'Exporter';
64     use vars qw ( @EXPORT_OK);
65
66     # set the version for version checking
67     our $VERSION = 2.0;
68
69     # your exported package globals go here,
70     # as well as any optionally exported functions
71     @EXPORT_OK = qw(
72         &CreateUser
73         &GetMessageContent
74         &CheckForLoops
75         &CheckForSuspiciousSender
76         &CheckForAutoGenerated
77         &CheckForBounce
78         &MailError
79         &ParseCcAddressesFromHead
80         &ParseSenderAddressFromHead
81         &ParseErrorsToAddressFromHead
82         &ParseAddressFromHeader
83         &Gateway);
84
85 }
86
87 =head1 NAME
88
89   RT::Interface::Email - helper functions for parsing email sent to RT
90
91 =head1 SYNOPSIS
92
93   use lib "!!RT_LIB_PATH!!";
94   use lib "!!RT_ETC_PATH!!";
95
96   use RT::Interface::Email  qw(Gateway CreateUser);
97
98 =head1 DESCRIPTION
99
100
101
102
103 =head1 METHODS
104
105 =head2 CheckForLoops HEAD
106
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.
110
111 =cut
112
113 sub CheckForLoops {
114     my $head = shift;
115
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') ) {
120         return 1;
121     }
122
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 ...
125     return undef;
126 }
127
128 =head2 CheckForSuspiciousSender HEAD
129
130 Takes a HEAD object of L<MIME::Head> class and returns true if sender
131 is suspicious. Suspicious means mailer daemon.
132
133 See also L</ParseSenderAddressFromHead>.
134
135 =cut
136
137 sub CheckForSuspiciousSender {
138     my $head = shift;
139
140     #if it's from a postmaster or mailer daemon, it's likely a bounce.
141
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.
147
148     #TODO: search through the whole email and find the right Ticket ID.
149
150     my ( $From, $junk ) = ParseSenderAddressFromHead($head);
151
152     if (   ( $From =~ /^mailer-daemon\@/i )
153         or ( $From =~ /^postmaster\@/i )
154         or ( $From eq "" ))
155     {
156         return (1);
157
158     }
159
160     return undef;
161 }
162
163 =head2 CheckForAutoGenerated HEAD
164
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.
168
169 =cut
170
171 sub CheckForAutoGenerated {
172     my $head = shift;
173
174     my $Precedence = $head->get("Precedence") || "";
175     if ( $Precedence =~ /^(bulk|junk)/i ) {
176         return (1);
177     }
178
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" ) {
183         return (1);
184     }
185
186     # First Class mailer uses this as a clue.
187     my $FCJunk = $head->get("X-FC-Machinegenerated") || "";
188     if ( $FCJunk =~ /^true/i ) {
189         return (1);
190     }
191
192     return (0);
193 }
194
195
196 sub CheckForBounce {
197     my $head = shift;
198
199     my $ReturnPath = $head->get("Return-path") || "";
200     return ( $ReturnPath =~ /<>/ );
201 }
202
203
204 =head2 MailError PARAM HASH
205
206 Sends an error message. Takes a param hash:
207
208 =over 4
209
210 =item From - sender's address, by default is 'CorrespondAddress';
211
212 =item To - recipient, by default is 'OwnerEmail';
213
214 =item Bcc - optional Bcc recipients;
215
216 =item Subject - subject of the message, default is 'There has been an error';
217
218 =item Explanation - main content of the error, default value is 'Unexplained error';
219
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.
222
223 =item Attach - optional text that attached to the error as 'message/rfc822' part.
224
225 =item LogLevel - log level under which we should write explanation message into the
226 log, by default we log it as critical.
227
228 =back
229
230 =cut
231
232 sub MailError {
233     my %args = (
234         To          => RT->Config->Get('OwnerEmail'),
235         Bcc         => undef,
236         From        => RT->Config->Get('CorrespondAddress'),
237         Subject     => 'There has been an error',
238         Explanation => 'Unexplained error',
239         MIMEObj     => undef,
240         Attach      => undef,
241         LogLevel    => 'crit',
242         @_
243     );
244
245     $RT::Logger->log(
246         level   => $args{'LogLevel'},
247         message => $args{'Explanation'}
248     ) if $args{'LogLevel'};
249
250     # the colons are necessary to make ->build include non-standard headers
251     my %entity_args = (
252         Type                    => "multipart/mixed",
253         From                    => $args{'From'},
254         Bcc                     => $args{'Bcc'},
255         To                      => $args{'To'},
256         Subject                 => $args{'Subject'},
257         'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'),
258     );
259
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');
263     }
264
265     my $entity = MIME::Entity->build(%entity_args);
266     SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} );
267
268     $entity->attach( Data => $args{'Explanation'} . "\n" );
269
270     if ( $args{'MIMEObj'} ) {
271         $args{'MIMEObj'}->sync_headers;
272         $entity->add_part( $args{'MIMEObj'} );
273     }
274
275     if ( $args{'Attach'} ) {
276         $entity->attach( Data => $args{'Attach'}, Type => 'message/rfc822' );
277
278     }
279
280     SendEmail( Entity => $entity, Bounce => 1 );
281 }
282
283
284 =head2 SendEmail Entity => undef, [ Bounce => 0, Ticket => undef, Transaction => undef ]
285
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.
290
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.
293
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.
297
298 Returns 1 on success, 0 on error or -1 if message has no recipients
299 and hasn't been sent.
300
301 =head3 Signing and Encrypting
302
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.
308
309 The following precedence of arguments are used to figure out if
310 the message should be encrypted and/or signed:
311
312 * if Sign or Encrypt argument is defined then its value is used
313
314 * else if Transaction's first attachment has X-RT-Sign or X-RT-Encrypt
315 header field then it's value is used
316
317 * else properties of a queue of the Ticket are used.
318
319 =cut
320
321 sub WillSignEncrypt {
322     my %args = @_;
323     my $attachment = delete $args{Attachment};
324     my $ticket     = delete $args{Ticket};
325
326     if ( not RT->Config->Get('GnuPG')->{'Enable'} ) {
327         $args{Sign} = $args{Encrypt} = 0;
328         return wantarray ? %args : 0;
329     }
330
331     for my $argument ( qw(Sign Encrypt) ) {
332         next if defined $args{ $argument };
333
334         if ( $attachment and defined $attachment->GetHeader("X-RT-$argument") ) {
335             $args{$argument} = $attachment->GetHeader("X-RT-$argument");
336         } elsif ( $ticket and $argument eq "Encrypt" ) {
337             $args{Encrypt} = $ticket->QueueObj->Encrypt();
338         } elsif ( $ticket and $argument eq "Sign" ) {
339             # Note that $queue->Sign is UI-only, and that all
340             # UI-generated messages explicitly set the X-RT-Crypt header
341             # to 0 or 1; thus this path is only taken for messages
342             # generated _not_ via the web UI.
343             $args{Sign} = $ticket->QueueObj->SignAuto();
344         }
345     }
346
347     return wantarray ? %args : ($args{Sign} || $args{Encrypt});
348 }
349
350 sub SendEmail {
351     my (%args) = (
352         Entity => undef,
353         Bounce => 0,
354         Ticket => undef,
355         Transaction => undef,
356         @_,
357     );
358
359     my $TicketObj = $args{'Ticket'};
360     my $TransactionObj = $args{'Transaction'};
361
362     foreach my $arg( qw(Entity Bounce) ) {
363         next unless defined $args{ lc $arg };
364
365         $RT::Logger->warning("'". lc($arg) ."' argument is deprecated, use '$arg' instead");
366         $args{ $arg } = delete $args{ lc $arg };
367     }
368
369     unless ( $args{'Entity'} ) {
370         $RT::Logger->crit( "Could not send mail without 'Entity' object" );
371         return 0;
372     }
373
374     my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
375     chomp $msgid;
376     
377     # If we don't have any recipients to send to, don't send a message;
378     unless ( $args{'Entity'}->head->get('To')
379         || $args{'Entity'}->head->get('Cc')
380         || $args{'Entity'}->head->get('Bcc') )
381     {
382         $RT::Logger->info( $msgid . " No recipients found. Not sending." );
383         return -1;
384     }
385
386     if ($args{'Entity'}->head->get('X-RT-Squelch')) {
387         $RT::Logger->info( $msgid . " Squelch header found. Not sending." );
388         return -1;
389     }
390
391     if ( $TransactionObj && !$TicketObj
392         && $TransactionObj->ObjectType eq 'RT::Ticket' )
393     {
394         $TicketObj = $TransactionObj->Object;
395     }
396
397     if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
398         %args = WillSignEncrypt(
399             %args,
400             Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef,
401             Ticket     => $TicketObj,
402         );
403         my $res = SignEncrypt( %args );
404         return $res unless $res > 0;
405     }
406
407     unless ( $args{'Entity'}->head->get('Date') ) {
408         require RT::Date;
409         my $date = RT::Date->new( $RT::SystemUser );
410         $date->SetToNow;
411         $args{'Entity'}->head->set( 'Date', $date->RFC2822( Timezone => 'server' ) );
412     }
413
414     my $mail_command = RT->Config->Get('MailCommand');
415
416     if ($mail_command eq 'testfile' and not $Mail::Mailer::testfile::config{outfile}) {
417         $Mail::Mailer::testfile::config{outfile} = File::Temp->new;
418         $RT::Logger->info("Storing outgoing emails in $Mail::Mailer::testfile::config{outfile}");
419     }
420
421     # if it is a sub routine, we just return it;
422     return $mail_command->($args{'Entity'}) if UNIVERSAL::isa( $mail_command, 'CODE' );
423
424     if ( $mail_command eq 'sendmailpipe' ) {
425         my $path = RT->Config->Get('SendmailPath');
426         my @args = shellwords(RT->Config->Get('SendmailArguments'));
427
428         # SetOutgoingMailFrom
429         if ( RT->Config->Get('SetOutgoingMailFrom') ) {
430             my $OutgoingMailAddress;
431
432             if ($TicketObj) {
433                 my $QueueName = $TicketObj->QueueObj->Name;
434                 my $QueueAddressOverride = RT->Config->Get('OverrideOutgoingMailFrom')->{$QueueName};
435
436                 if ($QueueAddressOverride) {
437                     $OutgoingMailAddress = $QueueAddressOverride;
438                 } else {
439                     $OutgoingMailAddress = $TicketObj->QueueObj->CorrespondAddress;
440                 }
441             }
442
443             $OutgoingMailAddress ||= RT->Config->Get('OverrideOutgoingMailFrom')->{'Default'};
444
445             push @args, "-f", $OutgoingMailAddress
446                 if $OutgoingMailAddress;
447         }
448
449         # Set Bounce Arguments
450         push @args, shellwords(RT->Config->Get('SendmailBounceArguments'))
451             if $args{'Bounce'};
452
453         # VERP
454         if ( $TransactionObj and
455              my $prefix = RT->Config->Get('VERPPrefix') and
456              my $domain = RT->Config->Get('VERPDomain') )
457         {
458             my $from = $TransactionObj->CreatorObj->EmailAddress;
459             $from =~ s/@/=/g;
460             $from =~ s/\s//g;
461             push @args, "-f", "$prefix$from\@$domain";
462         }
463
464         eval {
465             # don't ignore CHLD signal to get proper exit code
466             local $SIG{'CHLD'} = 'DEFAULT';
467
468             # if something wrong with $mail->print we will get PIPE signal, handle it
469             local $SIG{'PIPE'} = sub { die "program unexpectedly closed pipe" };
470
471             # Make it look to open2 like STDIN is on FD 0, like it
472             # should be; this is necessary because under mod_perl with
473             # the perl-script handler, it's not.  This causes our
474             # child's "STDIN" (FD 10-ish) to be set to the pipe we want,
475             # but FD 0 (which the exec'd sendmail assumes is STDIN) is
476             # still open to /dev/null; this ends disasterously.
477             local *STDIN = IO::Handle->new_from_fd( 0, "r" );
478
479             require IPC::Open2;
480             my ($mail, $stdout);
481             my $pid = IPC::Open2::open2( $stdout, $mail, $path, @args )
482                 or die "couldn't execute program: $!";
483
484             $args{'Entity'}->print($mail);
485             close $mail or die "close pipe failed: $!";
486
487             waitpid($pid, 0);
488             if ($?) {
489                 # sendmail exit statuses mostly errors with data not software
490                 # TODO: status parsing: core dump, exit on signal or EX_*
491                 my $msg = "$msgid: `$path @args` exited with code ". ($?>>8);
492                 $msg = ", interrupted by signal ". ($?&127) if $?&127;
493                 $RT::Logger->error( $msg );
494                 die $msg;
495             }
496         };
497         if ( $@ ) {
498             $RT::Logger->crit( "$msgid: Could not send mail with command `$path @args`: " . $@ );
499             if ( $TicketObj ) {
500                 _RecordSendEmailFailure( $TicketObj );
501             }
502             return 0;
503         }
504     }
505     elsif ( $mail_command eq 'smtp' ) {
506         require Net::SMTP;
507         my $smtp = do { local $@; eval { Net::SMTP->new(
508             Host  => RT->Config->Get('SMTPServer'),
509             Debug => RT->Config->Get('SMTPDebug'),
510         ) } };
511         unless ( $smtp ) {
512             $RT::Logger->crit( "Could not connect to SMTP server.");
513             if ($TicketObj) {
514                 _RecordSendEmailFailure( $TicketObj );
515             }
516             return 0;
517         }
518
519         # duplicate head as we want drop Bcc field
520         my $head = $args{'Entity'}->head->dup;
521         my @recipients = map $_->address, map 
522             Email::Address->parse($head->get($_)), qw(To Cc Bcc);                       
523         $head->delete('Bcc');
524
525         my $sender = RT->Config->Get('SMTPFrom')
526             || $args{'Entity'}->head->get('From');
527         chomp $sender;
528
529         my $status = $smtp->mail( $sender )
530             && $smtp->recipient( @recipients );
531
532         if ( $status ) {
533             $smtp->data;
534             my $fh = $smtp->tied_fh;
535             $head->print( $fh );
536             print $fh "\n";
537             $args{'Entity'}->print_body( $fh );
538             $smtp->dataend;
539         }
540         $smtp->quit;
541
542         unless ( $status ) {
543             $RT::Logger->crit( "$msgid: Could not send mail via SMTP." );
544             if ( $TicketObj ) {
545                 _RecordSendEmailFailure( $TicketObj );
546             }
547             return 0;
548         }
549     }
550     else {
551         local ($ENV{'MAILADDRESS'}, $ENV{'PERL_MAILERS'});
552
553         my @mailer_args = ($mail_command);
554         if ( $mail_command eq 'sendmail' ) {
555             $ENV{'PERL_MAILERS'} = RT->Config->Get('SendmailPath');
556             push @mailer_args, split(/\s+/, RT->Config->Get('SendmailArguments'));
557         }
558         else {
559             push @mailer_args, RT->Config->Get('MailParams');
560         }
561
562         unless ( $args{'Entity'}->send( @mailer_args ) ) {
563             $RT::Logger->crit( "$msgid: Could not send mail." );
564             if ( $TicketObj ) {
565                 _RecordSendEmailFailure( $TicketObj );
566             }
567             return 0;
568         }
569     }
570     return 1;
571 }
572
573 =head2 PrepareEmailUsingTemplate Template => '', Arguments => {}
574
575 Loads a template. Parses it using arguments if it's not empty.
576 Returns a tuple (L<RT::Template> object, error message).
577
578 Note that even if a template object is returned MIMEObj method
579 may return undef for empty templates.
580
581 =cut
582
583 sub PrepareEmailUsingTemplate {
584     my %args = (
585         Template => '',
586         Arguments => {},
587         @_
588     );
589
590     my $template = RT::Template->new( $RT::SystemUser );
591     $template->LoadGlobalTemplate( $args{'Template'} );
592     unless ( $template->id ) {
593         return (undef, "Couldn't load template '". $args{'Template'} ."'");
594     }
595     return $template if $template->IsEmpty;
596
597     my ($status, $msg) = $template->Parse( %{ $args{'Arguments'} } );
598     return (undef, $msg) unless $status;
599
600     return $template;
601 }
602
603 =head2 SendEmailUsingTemplate Template => '', Arguments => {}, From => CorrespondAddress, To => '', Cc => '', Bcc => ''
604
605 Sends email using a template, takes name of template, arguments for it and recipients.
606
607 =cut
608
609 sub SendEmailUsingTemplate {
610     my %args = (
611         Template => '',
612         Arguments => {},
613         To => undef,
614         Cc => undef,
615         Bcc => undef,
616         From => RT->Config->Get('CorrespondAddress'),
617         InReplyTo => undef,
618         @_
619     );
620
621     my ($template, $msg) = PrepareEmailUsingTemplate( %args );
622     return (0, $msg) unless $template;
623
624     my $mail = $template->MIMEObj;
625     unless ( $mail ) {
626         $RT::Logger->info("Message is not sent as template #". $template->id ." is empty");
627         return -1;
628     }
629
630     $mail->head->set( $_ => Encode::encode_utf8( $args{ $_ } ) )
631         foreach grep defined $args{$_}, qw(To Cc Bcc From);
632
633     SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
634
635     return SendEmail( Entity => $mail );
636 }
637
638 =head2 ForwardTransaction TRANSACTION, To => '', Cc => '', Bcc => ''
639
640 Forwards transaction with all attachments as 'message/rfc822'.
641
642 =cut
643
644 sub ForwardTransaction {
645     my $txn = shift;
646     my %args = ( To => '', Cc => '', Bcc => '', @_ );
647
648     my $entity = $txn->ContentAsMIME;
649
650     return SendForward( %args, Entity => $entity, Transaction => $txn );
651 }
652
653 =head2 ForwardTicket TICKET, To => '', Cc => '', Bcc => ''
654
655 Forwards a ticket's Create and Correspond Transactions and their Attachments as 'message/rfc822'.
656
657 =cut
658
659 sub ForwardTicket {
660     my $ticket = shift;
661     my %args = ( To => '', Cc => '', Bcc => '', @_ );
662
663     my $txns = $ticket->Transactions;
664     $txns->Limit(
665         FIELD    => 'Type',
666         VALUE    => $_,
667     ) for qw(Create Correspond);
668
669     my $entity = MIME::Entity->build(
670         Type => 'multipart/mixed',
671     );
672     $entity->add_part( $_ ) foreach 
673         map $_->ContentAsMIME,
674         @{ $txns->ItemsArrayRef };
675
676     return SendForward( %args, Entity => $entity, Ticket => $ticket, Template => 'Forward Ticket' );
677 }
678
679 =head2 SendForward Entity => undef, Ticket => undef, Transaction => undef, Template => undef, To => '', Cc => '', Bcc => ''
680
681 Forwards an Entity representing Ticket or Transaction as 'message/rfc822'. Entity is wrapped into Template.
682
683 =cut
684
685 sub SendForward {
686     my (%args) = (
687         Entity => undef,
688         Ticket => undef,
689         Transaction => undef,
690         Template => 'Forward',
691         To => '', Cc => '', Bcc => '',
692         @_
693     );
694
695     my $txn = $args{'Transaction'};
696     my $ticket = $args{'Ticket'};
697     $ticket ||= $txn->Object if $txn;
698
699     my $entity = $args{'Entity'};
700     unless ( $entity ) {
701         require Carp;
702         $RT::Logger->error(Carp::longmess("No entity provided"));
703         return (0, $ticket->loc("Couldn't send email"));
704     }
705
706     my ($template, $msg) = PrepareEmailUsingTemplate(
707         Template  => $args{'Template'},
708         Arguments => {
709             Ticket      => $ticket,
710             Transaction => $txn,
711         },
712     );
713
714     my $mail;
715     if ( $template ) {
716         $mail = $template->MIMEObj;
717     } else {
718         $RT::Logger->warning($msg);
719     }
720     unless ( $mail ) {
721         $RT::Logger->warning("Couldn't generate email using template '$args{Template}'");
722
723         my $description;
724         unless ( $args{'Transaction'} ) {
725             $description = 'This is forward of ticket #'. $ticket->id;
726         } else {
727             $description = 'This is forward of transaction #'
728                 . $txn->id ." of a ticket #". $txn->ObjectId;
729         }
730         $mail = MIME::Entity->build(
731             Type => 'text/plain',
732             Data => $description,
733         );
734     }
735
736     $mail->head->set( $_ => EncodeToMIME( String => $args{$_} ) )
737         foreach grep defined $args{$_}, qw(To Cc Bcc);
738
739     $mail->attach(
740         Type => 'message/rfc822',
741         Disposition => 'attachment',
742         Description => 'forwarded message',
743         Data => $entity->as_string,
744     );
745
746     my $from;
747     my $subject = '';
748     $subject = $txn->Subject if $txn;
749     $subject ||= $ticket->Subject if $ticket;
750     if ( RT->Config->Get('ForwardFromUser') ) {
751         $from = ($txn || $ticket)->CurrentUser->UserObj->EmailAddress;
752     } else {
753         # XXX: what if want to forward txn of other object than ticket?
754         $subject = AddSubjectTag( $subject, $ticket );
755         $from = $ticket->QueueObj->CorrespondAddress
756             || RT->Config->Get('CorrespondAddress');
757     }
758     $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) );
759     $mail->head->set( From    => EncodeToMIME( String => $from ) );
760
761     my $status = RT->Config->Get('ForwardFromUser')
762         # never sign if we forward from User
763         ? SendEmail( %args, Entity => $mail, Sign => 0 )
764         : SendEmail( %args, Entity => $mail );
765     return (0, $ticket->loc("Couldn't send email")) unless $status;
766     return (1, $ticket->loc("Send email successfully"));
767 }
768
769 =head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0
770
771 Signs and encrypts message using L<RT::Crypt::GnuPG>, but as well
772 handle errors with users' keys.
773
774 If a recipient has no key or has other problems with it, then the
775 unction sends a error to him using 'Error: public key' template.
776 Also, notifies RT's owner using template 'Error to RT owner: public key'
777 to inform that there are problems with users' keys. Then we filter
778 all bad recipients and retry.
779
780 Returns 1 on success, 0 on error and -1 if all recipients are bad and
781 had been filtered out.
782
783 =cut
784
785 sub SignEncrypt {
786     my %args = (
787         Entity => undef,
788         Sign => 0,
789         Encrypt => 0,
790         @_
791     );
792     return 1 unless $args{'Sign'} || $args{'Encrypt'};
793
794     my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
795     chomp $msgid;
796
797     $RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
798     $RT::Logger->debug("$msgid Encrypting message") if $args{'Encrypt'};
799
800     require RT::Crypt::GnuPG;
801     my %res = RT::Crypt::GnuPG::SignEncrypt( %args );
802     return 1 unless $res{'exit_code'};
803
804     my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
805
806     my @bad_recipients;
807     foreach my $line ( @status ) {
808         # if the passphrase fails, either you have a bad passphrase
809         # or gpg-agent has died.  That should get caught in Create and
810         # Update, but at least throw an error here
811         if (($line->{'Operation'}||'') eq 'PassphraseCheck'
812             && $line->{'Status'} =~ /^(?:BAD|MISSING)$/ ) {
813             $RT::Logger->error( "$line->{'Status'} PASSPHRASE: $line->{'Message'}" );
814             return 0;
815         }
816         next unless ($line->{'Operation'}||'') eq 'RecipientsCheck';
817         next if $line->{'Status'} eq 'DONE';
818         $RT::Logger->error( $line->{'Message'} );
819         push @bad_recipients, $line;
820     }
821     return 0 unless @bad_recipients;
822
823     $_->{'AddressObj'} = (Email::Address->parse( $_->{'Recipient'} ))[0]
824         foreach @bad_recipients;
825
826     foreach my $recipient ( @bad_recipients ) {
827         my $status = SendEmailUsingTemplate(
828             To        => $recipient->{'AddressObj'}->address,
829             Template  => 'Error: public key',
830             Arguments => {
831                 %$recipient,
832                 TicketObj      => $args{'Ticket'},
833                 TransactionObj => $args{'Transaction'},
834             },
835         );
836         unless ( $status ) {
837             $RT::Logger->error("Couldn't send 'Error: public key'");
838         }
839     }
840
841     my $status = SendEmailUsingTemplate(
842         To        => RT->Config->Get('OwnerEmail'),
843         Template  => 'Error to RT owner: public key',
844         Arguments => {
845             BadRecipients  => \@bad_recipients,
846             TicketObj      => $args{'Ticket'},
847             TransactionObj => $args{'Transaction'},
848         },
849     );
850     unless ( $status ) {
851         $RT::Logger->error("Couldn't send 'Error to RT owner: public key'");
852     }
853
854     DeleteRecipientsFromHead(
855         $args{'Entity'}->head,
856         map $_->{'AddressObj'}->address, @bad_recipients
857     );
858
859     unless ( $args{'Entity'}->head->get('To')
860           || $args{'Entity'}->head->get('Cc')
861           || $args{'Entity'}->head->get('Bcc') )
862     {
863         $RT::Logger->debug("$msgid No recipients that have public key, not sending");
864         return -1;
865     }
866
867     # redo without broken recipients
868     %res = RT::Crypt::GnuPG::SignEncrypt( %args );
869     return 0 if $res{'exit_code'};
870
871     return 1;
872 }
873
874 use MIME::Words ();
875
876 =head2 EncodeToMIME
877
878 Takes a hash with a String and a Charset. Returns the string encoded
879 according to RFC2047, using B (base64 based) encoding.
880
881 String must be a perl string, octets are returned.
882
883 If Charset is not provided then $EmailOutputEncoding config option
884 is used, or "latin-1" if that is not set.
885
886 =cut
887
888 sub EncodeToMIME {
889     my %args = (
890         String => undef,
891         Charset  => undef,
892         @_
893     );
894     my $value = $args{'String'};
895     return $value unless $value; # 0 is perfect ascii
896     my $charset  = $args{'Charset'} || RT->Config->Get('EmailOutputEncoding');
897     my $encoding = 'B';
898
899     # using RFC2047 notation, sec 2.
900     # encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
901
902     # An 'encoded-word' may not be more than 75 characters long
903     #
904     # MIME encoding increases 4/3*(number of bytes), and always in multiples
905     # of 4. Thus we have to find the best available value of bytes available
906     # for each chunk.
907     #
908     # First we get the integer max which max*4/3 would fit on space.
909     # Then we find the greater multiple of 3 lower or equal than $max.
910     my $max = int(
911         (   ( 75 - length( '=?' . $charset . '?' . $encoding . '?' . '?=' ) )
912             * 3
913         ) / 4
914     );
915     $max = int( $max / 3 ) * 3;
916
917     chomp $value;
918
919     if ( $max <= 0 ) {
920
921         # gives an error...
922         $RT::Logger->crit("Can't encode! Charset or encoding too big.");
923         return ($value);
924     }
925
926     return ($value) if $value =~ /^(?:[\t\x20-\x7e]|\x0D*\x0A[ \t])+$/s;
927
928     $value =~ s/\s+$//;
929
930     # we need perl string to split thing char by char
931     Encode::_utf8_on($value) unless Encode::is_utf8($value);
932
933     my ( $tmp, @chunks ) = ( '', () );
934     while ( length $value ) {
935         my $char = substr( $value, 0, 1, '' );
936         my $octets = Encode::encode( $charset, $char );
937         if ( length($tmp) + length($octets) > $max ) {
938             push @chunks, $tmp;
939             $tmp = '';
940         }
941         $tmp .= $octets;
942     }
943     push @chunks, $tmp if length $tmp;
944
945     # encode an join chuncks
946     $value = join "\n ",
947         map MIME::Words::encode_mimeword( $_, $encoding, $charset ),
948         @chunks;
949     return ($value);
950 }
951
952 sub CreateUser {
953     my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_;
954
955     my $NewUser = RT::User->new( $RT::SystemUser );
956
957     my ( $Val, $Message ) = $NewUser->Create(
958         Name => ( $Username || $Address ),
959         EmailAddress => $Address,
960         RealName     => $Name,
961         Password     => undef,
962         Privileged   => 0,
963         Comments     => 'Autocreated on ticket submission',
964     );
965
966     unless ($Val) {
967
968         # Deal with the race condition of two account creations at once
969         if ($Username) {
970             $NewUser->LoadByName($Username);
971         }
972
973         unless ( $NewUser->Id ) {
974             $NewUser->LoadByEmail($Address);
975         }
976
977         unless ( $NewUser->Id ) {
978             MailError(
979                 To          => $ErrorsTo,
980                 Subject     => "User could not be created",
981                 Explanation =>
982                     "User creation failed in mailgateway: $Message",
983                 MIMEObj  => $entity,
984                 LogLevel => 'crit',
985             );
986         }
987     }
988
989     #Load the new user object
990     my $CurrentUser = new RT::CurrentUser;
991     $CurrentUser->LoadByEmail( $Address );
992
993     unless ( $CurrentUser->id ) {
994         $RT::Logger->warning(
995             "Couldn't load user '$Address'." . "giving up" );
996         MailError(
997             To          => $ErrorsTo,
998             Subject     => "User could not be loaded",
999             Explanation =>
1000                 "User  '$Address' could not be loaded in the mail gateway",
1001             MIMEObj  => $entity,
1002             LogLevel => 'crit'
1003         );
1004     }
1005
1006     return $CurrentUser;
1007 }
1008
1009
1010
1011 =head2 ParseCcAddressesFromHead HASH
1012
1013 Takes a hash containing QueueObj, Head and CurrentUser objects.
1014 Returns a list of all email addresses in the To and Cc
1015 headers b<except> the current Queue\'s email addresses, the CurrentUser\'s
1016 email address  and anything that the configuration sub RT::IsRTAddress matches.
1017
1018 =cut
1019
1020 sub ParseCcAddressesFromHead {
1021     my %args = (
1022         Head        => undef,
1023         QueueObj    => undef,
1024         CurrentUser => undef,
1025         @_
1026     );
1027
1028     my $current_address = lc $args{'CurrentUser'}->EmailAddress;
1029     my $user = $args{'CurrentUser'}->UserObj;
1030
1031     return
1032         grep {  $_ ne $current_address 
1033                 && !RT::EmailParser->IsRTAddress( $_ )
1034                 && !IgnoreCcAddress( $_ )
1035              }
1036         map lc $user->CanonicalizeEmailAddress( $_->address ),
1037         map Email::Address->parse( $args{'Head'}->get( $_ ) ),
1038         qw(To Cc);
1039 }
1040
1041 =head2 IgnoreCcAddress ADDRESS
1042
1043 Returns true if ADDRESS matches the $IgnoreCcRegexp config variable.
1044
1045 =cut
1046
1047 sub IgnoreCcAddress {
1048     my $address = shift;
1049     if ( my $address_re = RT->Config->Get('IgnoreCcRegexp') ) {
1050         return 1 if $address =~ /$address_re/i;
1051     }
1052     return undef;
1053 }
1054
1055 =head2 ParseSenderAddressFromHead HEAD
1056
1057 Takes a MIME::Header object. Returns a tuple: (user@host, friendly name)
1058 of the From (evaluated in order of Reply-To:, From:, Sender)
1059
1060 =cut
1061
1062 sub ParseSenderAddressFromHead {
1063     my $head = shift;
1064
1065     #Figure out who's sending this message.
1066     foreach my $header ('Reply-To', 'From', 'Sender') {
1067         my $addr_line = $head->get($header) || next;
1068         my ($addr, $name) = ParseAddressFromHeader( $addr_line );
1069         # only return if the address is not empty
1070         return ($addr, $name) if $addr;
1071     }
1072
1073     return (undef, undef);
1074 }
1075
1076 =head2 ParseErrorsToAddressFromHead HEAD
1077
1078 Takes a MIME::Header object. Return a single value : user@host
1079 of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
1080 From:, Sender)
1081
1082 =cut
1083
1084 sub ParseErrorsToAddressFromHead {
1085     my $head = shift;
1086
1087     #Figure out who's sending this message.
1088
1089     foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) {
1090
1091         # If there's a header of that name
1092         my $headerobj = $head->get($header);
1093         if ($headerobj) {
1094             my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
1095
1096             # If it's got actual useful content...
1097             return ($addr) if ($addr);
1098         }
1099     }
1100 }
1101
1102
1103
1104 =head2 ParseAddressFromHeader ADDRESS
1105
1106 Takes an address from C<$head->get('Line')> and returns a tuple: user@host, friendly name
1107
1108 =cut
1109
1110 sub ParseAddressFromHeader {
1111     my $Addr = shift;
1112
1113     # Some broken mailers send:  ""Vincent, Jesse"" <jesse@fsck.com>. Hate
1114     $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
1115     my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
1116
1117     my ($AddrObj) = grep ref $_, @Addresses;
1118     unless ( $AddrObj ) {
1119         return ( undef, undef );
1120     }
1121
1122     my $Name = ( $AddrObj->name || $AddrObj->phrase || $AddrObj->comment || $AddrObj->address );
1123
1124     #Lets take the from and load a user object.
1125     my $Address = $AddrObj->address;
1126
1127     return ( $Address, $Name );
1128 }
1129
1130 =head2 DeleteRecipientsFromHead HEAD RECIPIENTS
1131
1132 Gets a head object and list of addresses.
1133 Deletes addresses from To, Cc or Bcc fields.
1134
1135 =cut
1136
1137 sub DeleteRecipientsFromHead {
1138     my $head = shift;
1139     my %skip = map { lc $_ => 1 } @_;
1140
1141     foreach my $field ( qw(To Cc Bcc) ) {
1142         $head->set( $field =>
1143             join ', ', map $_->format, grep !$skip{ lc $_->address },
1144                 Email::Address->parse( $head->get( $field ) )
1145         );
1146     }
1147 }
1148
1149 sub GenMessageId {
1150     my %args = (
1151         Ticket      => undef,
1152         Scrip       => undef,
1153         ScripAction => undef,
1154         @_
1155     );
1156     my $org = RT->Config->Get('Organization');
1157     my $ticket_id = ( ref $args{'Ticket'}? $args{'Ticket'}->id : $args{'Ticket'} ) || 0;
1158     my $scrip_id = ( ref $args{'Scrip'}? $args{'Scrip'}->id : $args{'Scrip'} ) || 0;
1159     my $sent = ( ref $args{'ScripAction'}? $args{'ScripAction'}->{'_Message_ID'} : 0 ) || 0;
1160
1161     return "<rt-". $RT::VERSION ."-". $$ ."-". CORE::time() ."-". int(rand(2000)) .'.'
1162         . $ticket_id ."-". $scrip_id ."-". $sent ."@". $org .">" ;
1163 }
1164
1165 sub SetInReplyTo {
1166     my %args = (
1167         Message   => undef,
1168         InReplyTo => undef,
1169         Ticket    => undef,
1170         @_
1171     );
1172     return unless $args{'Message'} && $args{'InReplyTo'};
1173
1174     my $get_header = sub {
1175         my @res;
1176         if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
1177             @res = $args{'InReplyTo'}->head->get( shift );
1178         } else {
1179             @res = $args{'InReplyTo'}->GetHeader( shift ) || '';
1180         }
1181         return grep length, map { split /\s+/m, $_ } grep defined, @res;
1182     };
1183
1184     my @id = $get_header->('Message-ID');
1185     #XXX: custom header should begin with X- otherwise is violation of the standard
1186     my @rtid = $get_header->('RT-Message-ID');
1187     my @references = $get_header->('References');
1188     unless ( @references ) {
1189         @references = $get_header->('In-Reply-To');
1190     }
1191     push @references, @id, @rtid;
1192     if ( $args{'Ticket'} ) {
1193         my $pseudo_ref =  '<RT-Ticket-'. $args{'Ticket'}->id .'@'. RT->Config->Get('Organization') .'>';
1194         push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references;
1195     }
1196     @references = splice @references, 4, -6
1197         if @references > 10;
1198
1199     my $mail = $args{'Message'};
1200     $mail->head->set( 'In-Reply-To' => join ' ', @rtid? (@rtid) : (@id) ) if @id || @rtid;
1201     $mail->head->set( 'References' => join ' ', @references );
1202 }
1203
1204 sub ParseTicketId {
1205     my $Subject = shift;
1206
1207     my $rtname = RT->Config->Get('rtname');
1208     my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;
1209
1210     my $id;
1211     if ( $Subject =~ s/\[$test_name\s+\#(\d+)\s*\]//i ) {
1212         $id = $1;
1213     } else {
1214         foreach my $tag ( RT->System->SubjectTag ) {
1215             next unless $Subject =~ s/\[\Q$tag\E\s+\#(\d+)\s*\]//i;
1216             $id = $1;
1217             last;
1218         }
1219     }
1220     return undef unless $id;
1221
1222     $RT::Logger->debug("Found a ticket ID. It's $id");
1223     return $id;
1224 }
1225
1226 sub AddSubjectTag {
1227     my $subject = shift;
1228     my $ticket  = shift;
1229     unless ( ref $ticket ) {
1230         my $tmp = RT::Ticket->new( $RT::SystemUser );
1231         $tmp->Load( $ticket );
1232         $ticket = $tmp;
1233     }
1234     my $id = $ticket->id;
1235     my $queue_tag = $ticket->QueueObj->SubjectTag;
1236
1237     my $tag_re = RT->Config->Get('EmailSubjectTagRegex');
1238     unless ( $tag_re ) {
1239         my $tag = $queue_tag || RT->Config->Get('rtname');
1240         $tag_re = qr/\Q$tag\E/;
1241     } elsif ( $queue_tag ) {
1242         $tag_re = qr/$tag_re|\Q$queue_tag\E/;
1243     }
1244     return $subject if $subject =~ /\[$tag_re\s+#$id\]/;
1245
1246     $subject =~ s/(\r\n|\n|\s)/ /gi;
1247     chomp $subject;
1248     return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject";
1249 }
1250
1251
1252 =head2 Gateway ARGSREF
1253
1254
1255 Takes parameters:
1256
1257     action
1258     queue
1259     message
1260
1261
1262 This performs all the "guts" of the mail rt-mailgate program, and is
1263 designed to be called from the web interface with a message, user
1264 object, and so on.
1265
1266 Can also take an optional 'ticket' parameter; this ticket id overrides
1267 any ticket id found in the subject.
1268
1269 Returns:
1270
1271     An array of:
1272
1273     (status code, message, optional ticket object)
1274
1275     status code is a numeric value.
1276
1277       for temporary failures, the status code should be -75
1278
1279       for permanent failures which are handled by RT, the status code
1280       should be 0
1281
1282       for succces, the status code should be 1
1283
1284
1285
1286 =cut
1287
1288 sub _LoadPlugins {
1289     my @mail_plugins = @_;
1290
1291     my @res;
1292     foreach my $plugin (@mail_plugins) {
1293         if ( ref($plugin) eq "CODE" ) {
1294             push @res, $plugin;
1295         } elsif ( !ref $plugin ) {
1296             my $Class = $plugin;
1297             $Class = "RT::Interface::Email::" . $Class
1298                 unless $Class =~ /^RT::Interface::Email::/;
1299             $Class->require or
1300                 do { $RT::Logger->error("Couldn't load $Class: $@"); next };
1301
1302             no strict 'refs';
1303             unless ( defined *{ $Class . "::GetCurrentUser" }{CODE} ) {
1304                 $RT::Logger->crit( "No GetCurrentUser code found in $Class module");
1305                 next;
1306             }
1307             push @res, $Class;
1308         } else {
1309             $RT::Logger->crit( "$plugin - is not class name or code reference");
1310         }
1311     }
1312     return @res;
1313 }
1314
1315 sub Gateway {
1316     my $argsref = shift;
1317     my %args    = (
1318         action  => 'correspond',
1319         queue   => '1',
1320         ticket  => undef,
1321         message => undef,
1322         %$argsref
1323     );
1324
1325     my $SystemTicket;
1326     my $Right;
1327
1328     # Validate the action
1329     my ( $status, @actions ) = IsCorrectAction( $args{'action'} );
1330     unless ($status) {
1331         return (
1332             -75,
1333             "Invalid 'action' parameter "
1334                 . $actions[0]
1335                 . " for queue "
1336                 . $args{'queue'},
1337             undef
1338         );
1339     }
1340
1341     my $parser = RT::EmailParser->new();
1342     $parser->SmartParseMIMEEntityFromScalar(
1343         Message => $args{'message'},
1344         Decode => 0,
1345         Exact => 1,
1346     );
1347
1348     my $Message = $parser->Entity();
1349     unless ($Message) {
1350         MailError(
1351             Subject     => "RT Bounce: Unparseable message",
1352             Explanation => "RT couldn't process the message below",
1353             Attach      => $args{'message'}
1354         );
1355
1356         return ( 0,
1357             "Failed to parse this message. Something is likely badly wrong with the message"
1358         );
1359     }
1360
1361     my @mail_plugins = grep $_, RT->Config->Get('MailPlugins');
1362     push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
1363     @mail_plugins = _LoadPlugins( @mail_plugins );
1364
1365     my %skip_plugin;
1366     foreach my $class( grep !ref, @mail_plugins ) {
1367         # check if we should apply filter before decoding
1368         my $check_cb = do {
1369             no strict 'refs';
1370             *{ $class . "::ApplyBeforeDecode" }{CODE};
1371         };
1372         next unless defined $check_cb;
1373         next unless $check_cb->(
1374             Message       => $Message,
1375             RawMessageRef => \$args{'message'},
1376         );
1377
1378         $skip_plugin{ $class }++;
1379
1380         my $Code = do {
1381             no strict 'refs';
1382             *{ $class . "::GetCurrentUser" }{CODE};
1383         };
1384         my ($status, $msg) = $Code->(
1385             Message       => $Message,
1386             RawMessageRef => \$args{'message'},
1387         );
1388         next if $status > 0;
1389
1390         if ( $status == -2 ) {
1391             return (1, $msg, undef);
1392         } elsif ( $status == -1 ) {
1393             return (0, $msg, undef);
1394         }
1395     }
1396     @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins;
1397     $parser->_DecodeBodies;
1398     $parser->_PostProcessNewEntity;
1399
1400     my $head = $Message->head;
1401     my $ErrorsTo = ParseErrorsToAddressFromHead( $head );
1402
1403     my $MessageId = $head->get('Message-ID')
1404         || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
1405
1406     #Pull apart the subject line
1407     my $Subject = $head->get('Subject') || '';
1408     chomp $Subject;
1409     
1410     # {{{ Lets check for mail loops of various sorts.
1411     my ($should_store_machine_generated_message, $IsALoop, $result);
1412     ( $should_store_machine_generated_message, $ErrorsTo, $result, $IsALoop ) =
1413       _HandleMachineGeneratedMail(
1414         Message  => $Message,
1415         ErrorsTo => $ErrorsTo,
1416         Subject  => $Subject,
1417         MessageId => $MessageId
1418     );
1419
1420     # Do not pass loop messages to MailPlugins, to make sure the loop
1421     # is broken, unless $RT::StoreLoops is set.
1422     if ($IsALoop && !$should_store_machine_generated_message) {
1423         return ( 0, $result, undef );
1424     }
1425     # }}}
1426
1427     $args{'ticket'} ||= ParseTicketId( $Subject );
1428
1429     $SystemTicket = RT::Ticket->new( $RT::SystemUser );
1430     $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
1431     if ( $SystemTicket->id ) {
1432         $Right = 'ReplyToTicket';
1433     } else {
1434         $Right = 'CreateTicket';
1435     }
1436
1437     #Set up a queue object
1438     my $SystemQueueObj = RT::Queue->new( $RT::SystemUser );
1439     $SystemQueueObj->Load( $args{'queue'} );
1440
1441     # We can safely have no queue of we have a known-good ticket
1442     unless ( $SystemTicket->id || $SystemQueueObj->id ) {
1443         return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
1444     }
1445
1446     my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel(
1447         MailPlugins   => \@mail_plugins,
1448         Actions       => \@actions,
1449         Message       => $Message,
1450         RawMessageRef => \$args{message},
1451         SystemTicket  => $SystemTicket,
1452         SystemQueue   => $SystemQueueObj,
1453     );
1454
1455     # {{{ If authentication fails and no new user was created, get out.
1456     if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) {
1457
1458         # If the plugins refused to create one, they lose.
1459         unless ( $AuthStat == -1 ) {
1460             _NoAuthorizedUserFound(
1461                 Right     => $Right,
1462                 Message   => $Message,
1463                 Requestor => $ErrorsTo,
1464                 Queue     => $args{'queue'}
1465             );
1466
1467         }
1468         return ( 0, "Could not load a valid user", undef );
1469     }
1470
1471     # If we got a user, but they don't have the right to say things
1472     if ( $AuthStat == 0 ) {
1473         MailError(
1474             To          => $ErrorsTo,
1475             Subject     => "Permission Denied",
1476             Explanation =>
1477                 "You do not have permission to communicate with RT",
1478             MIMEObj => $Message
1479         );
1480         return (
1481             0,
1482             "$ErrorsTo tried to submit a message to "
1483                 . $args{'Queue'}
1484                 . " without permission.",
1485             undef
1486         );
1487     }
1488
1489
1490     unless ($should_store_machine_generated_message) {
1491         return ( 0, $result, undef );
1492     }
1493
1494     # if plugin's updated SystemTicket then update arguments
1495     $args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id;
1496
1497     my $Ticket = RT::Ticket->new($CurrentUser);
1498
1499     if ( !$args{'ticket'} && grep /^(comment|correspond)$/, @actions )
1500     {
1501
1502         my @Cc;
1503         my @Requestors = ( $CurrentUser->id );
1504
1505         if (RT->Config->Get('ParseNewMessageForTicketCcs')) {
1506             @Cc = ParseCcAddressesFromHead(
1507                 Head        => $head,
1508                 CurrentUser => $CurrentUser,
1509                 QueueObj    => $SystemQueueObj
1510             );
1511         }
1512
1513         my ( $id, $Transaction, $ErrStr ) = $Ticket->Create(
1514             Queue     => $SystemQueueObj->Id,
1515             Subject   => $Subject,
1516             Requestor => \@Requestors,
1517             Cc        => \@Cc,
1518             MIMEObj   => $Message
1519         );
1520         if ( $id == 0 ) {
1521             MailError(
1522                 To          => $ErrorsTo,
1523                 Subject     => "Ticket creation failed: $Subject",
1524                 Explanation => $ErrStr,
1525                 MIMEObj     => $Message
1526             );
1527             return ( 0, "Ticket creation failed: $ErrStr", $Ticket );
1528         }
1529
1530         # strip comments&corresponds from the actions we don't need
1531         # to record them if we've created the ticket just now
1532         @actions = grep !/^(comment|correspond)$/, @actions;
1533         $args{'ticket'} = $id;
1534
1535     } elsif ( $args{'ticket'} ) {
1536
1537         $Ticket->Load( $args{'ticket'} );
1538         unless ( $Ticket->Id ) {
1539             my $error = "Could not find a ticket with id " . $args{'ticket'};
1540             MailError(
1541                 To          => $ErrorsTo,
1542                 Subject     => "Message not recorded: $Subject",
1543                 Explanation => $error,
1544                 MIMEObj     => $Message
1545             );
1546
1547             return ( 0, $error );
1548         }
1549         $args{'ticket'} = $Ticket->id;
1550     } else {
1551         return ( 1, "Success", $Ticket );
1552     }
1553
1554     # }}}
1555
1556     my $unsafe_actions = RT->Config->Get('UnsafeEmailCommands');
1557     foreach my $action (@actions) {
1558
1559         #   If the action is comment, add a comment.
1560         if ( $action =~ /^(?:comment|correspond)$/i ) {
1561             my $method = ucfirst lc $action;
1562             my ( $status, $msg ) = $Ticket->$method( MIMEObj => $Message );
1563             unless ($status) {
1564
1565                 #Warn the sender that we couldn't actually submit the comment.
1566                 MailError(
1567                     To          => $ErrorsTo,
1568                     Subject     => "Message not recorded: $Subject",
1569                     Explanation => $msg,
1570                     MIMEObj     => $Message
1571                 );
1572                 return ( 0, "Message not recorded: $msg", $Ticket );
1573             }
1574         } elsif ($unsafe_actions) {
1575             my ( $status, $msg ) = _RunUnsafeAction(
1576                 Action      => $action,
1577                 ErrorsTo    => $ErrorsTo,
1578                 Message     => $Message,
1579                 Ticket      => $Ticket,
1580                 CurrentUser => $CurrentUser,
1581             );
1582             return ($status, $msg, $Ticket) unless $status == 1;
1583         }
1584     }
1585     return ( 1, "Success", $Ticket );
1586 }
1587
1588 =head2 GetAuthenticationLevel
1589
1590     # Authentication Level
1591     # -1 - Get out.  this user has been explicitly declined
1592     # 0 - User may not do anything (Not used at the moment)
1593     # 1 - Normal user
1594     # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate
1595
1596 =cut
1597
1598 sub GetAuthenticationLevel {
1599     my %args = (
1600         MailPlugins   => [],
1601         Actions       => [],
1602         Message       => undef,
1603         RawMessageRef => undef,
1604         SystemTicket  => undef,
1605         SystemQueue   => undef,
1606         @_,
1607     );
1608
1609     my ( $CurrentUser, $AuthStat, $error );
1610
1611     # Initalize AuthStat so comparisons work correctly
1612     $AuthStat = -9999999;
1613
1614     # if plugin returns AuthStat -2 we skip action
1615     # NOTE: this is experimental API and it would be changed
1616     my %skip_action = ();
1617
1618     # Since this needs loading, no matter what
1619     foreach (@{ $args{MailPlugins} }) {
1620         my ($Code, $NewAuthStat);
1621         if ( ref($_) eq "CODE" ) {
1622             $Code = $_;
1623         } else {
1624             no strict 'refs';
1625             $Code = *{ $_ . "::GetCurrentUser" }{CODE};
1626         }
1627
1628         foreach my $action (@{ $args{Actions} }) {
1629             ( $CurrentUser, $NewAuthStat ) = $Code->(
1630                 Message       => $args{Message},
1631                 RawMessageRef => $args{RawMessageRef},
1632                 CurrentUser   => $CurrentUser,
1633                 AuthLevel     => $AuthStat,
1634                 Action        => $action,
1635                 Ticket        => $args{SystemTicket},
1636                 Queue         => $args{SystemQueue},
1637             );
1638
1639 # You get the highest level of authentication you were assigned, unless you get the magic -1
1640 # If a module returns a "-1" then we discard the ticket, so.
1641             $AuthStat = $NewAuthStat
1642                 if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
1643
1644             last if $AuthStat == -1;
1645             $skip_action{$action}++ if $AuthStat == -2;
1646         }
1647
1648         # strip actions we should skip
1649         @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
1650             if $AuthStat == -2;
1651         last unless @{$args{Actions}};
1652
1653         last if $AuthStat == -1;
1654     }
1655
1656     return $AuthStat if !wantarray;
1657
1658     return ($AuthStat, $CurrentUser, $error);
1659 }
1660
1661 sub _RunUnsafeAction {
1662     my %args = (
1663         Action      => undef,
1664         ErrorsTo    => undef,
1665         Message     => undef,
1666         Ticket      => undef,
1667         CurrentUser => undef,
1668         @_
1669     );
1670
1671     if ( $args{'Action'} =~ /^take$/i ) {
1672         my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
1673         unless ($status) {
1674             MailError(
1675                 To          => $args{'ErrorsTo'},
1676                 Subject     => "Ticket not taken",
1677                 Explanation => $msg,
1678                 MIMEObj     => $args{'Message'}
1679             );
1680             return ( 0, "Ticket not taken" );
1681         }
1682     } elsif ( $args{'Action'} =~ /^resolve$/i ) {
1683         my ( $status, $msg ) = $args{'Ticket'}->SetStatus('resolved');
1684         unless ($status) {
1685
1686             #Warn the sender that we couldn't actually submit the comment.
1687             MailError(
1688                 To          => $args{'ErrorsTo'},
1689                 Subject     => "Ticket not resolved",
1690                 Explanation => $msg,
1691                 MIMEObj     => $args{'Message'}
1692             );
1693             return ( 0, "Ticket not resolved" );
1694         }
1695     } else {
1696         return ( 0, "Not supported unsafe action $args{'Action'}", $args{'Ticket'} );
1697     }
1698     return ( 1, "Success" );
1699 }
1700
1701 =head2 _NoAuthorizedUserFound
1702
1703 Emails the RT Owner and the requestor when the auth plugins return "No auth user found"
1704
1705 =cut
1706
1707 sub _NoAuthorizedUserFound {
1708     my %args = (
1709         Right     => undef,
1710         Message   => undef,
1711         Requestor => undef,
1712         Queue     => undef,
1713         @_
1714     );
1715
1716     # Notify the RT Admin of the failure.
1717     MailError(
1718         To          => RT->Config->Get('OwnerEmail'),
1719         Subject     => "Could not load a valid user",
1720         Explanation => <<EOT,
1721 RT could not load a valid user, and RT's configuration does not allow
1722 for the creation of a new user for this email (@{[$args{Requestor}]}).
1723
1724 You might need to grant 'Everyone' the right '@{[$args{Right}]}' for the
1725 queue @{[$args{'Queue'}]}.
1726
1727 EOT
1728         MIMEObj  => $args{'Message'},
1729         LogLevel => 'error'
1730     );
1731
1732     # Also notify the requestor that his request has been dropped.
1733     if ($args{'Requestor'} ne RT->Config->Get('OwnerEmail')) {
1734     MailError(
1735         To          => $args{'Requestor'},
1736         Subject     => "Could not load a valid user",
1737         Explanation => <<EOT,
1738 RT could not load a valid user, and RT's configuration does not allow
1739 for the creation of a new user for your email.
1740
1741 EOT
1742         MIMEObj  => $args{'Message'},
1743         LogLevel => 'error'
1744     );
1745     }
1746 }
1747
1748 =head2 _HandleMachineGeneratedMail
1749
1750 Takes named params:
1751     Message
1752     ErrorsTo
1753     Subject
1754
1755 Checks the message to see if it's a bounce, if it looks like a loop, if it's autogenerated, etc.
1756 Returns a triple of ("Should we continue (boolean)", "New value for $ErrorsTo", "Status message",
1757 "This message appears to be a loop (boolean)" );
1758
1759 =cut
1760
1761 sub _HandleMachineGeneratedMail {
1762     my %args = ( Message => undef, ErrorsTo => undef, Subject => undef, MessageId => undef, @_ );
1763     my $head = $args{'Message'}->head;
1764     my $ErrorsTo = $args{'ErrorsTo'};
1765
1766     my $IsBounce = CheckForBounce($head);
1767
1768     my $IsAutoGenerated = CheckForAutoGenerated($head);
1769
1770     my $IsSuspiciousSender = CheckForSuspiciousSender($head);
1771
1772     my $IsALoop = CheckForLoops($head);
1773
1774     my $SquelchReplies = 0;
1775
1776     my $owner_mail = RT->Config->Get('OwnerEmail');
1777
1778     #If the message is autogenerated, we need to know, so we can not
1779     # send mail to the sender
1780     if ( $IsBounce || $IsSuspiciousSender || $IsAutoGenerated || $IsALoop ) {
1781         $SquelchReplies = 1;
1782         $ErrorsTo       = $owner_mail;
1783     }
1784
1785     # Warn someone if it's a loop, before we drop it on the ground
1786     if ($IsALoop) {
1787         $RT::Logger->crit("RT Received mail (".$args{MessageId}.") from itself.");
1788
1789         #Should we mail it to RTOwner?
1790         if ( RT->Config->Get('LoopsToRTOwner') ) {
1791             MailError(
1792                 To          => $owner_mail,
1793                 Subject     => "RT Bounce: ".$args{'Subject'},
1794                 Explanation => "RT thinks this message may be a bounce",
1795                 MIMEObj     => $args{Message}
1796             );
1797         }
1798
1799         #Do we actually want to store it?
1800         return ( 0, $ErrorsTo, "Message Bounced", $IsALoop )
1801             unless RT->Config->Get('StoreLoops');
1802     }
1803
1804     # Squelch replies if necessary
1805     # Don't let the user stuff the RT-Squelch-Replies-To header.
1806     if ( $head->get('RT-Squelch-Replies-To') ) {
1807         $head->add(
1808             'RT-Relocated-Squelch-Replies-To',
1809             $head->get('RT-Squelch-Replies-To')
1810         );
1811         $head->delete('RT-Squelch-Replies-To');
1812     }
1813
1814     if ($SquelchReplies) {
1815
1816         # Squelch replies to the sender, and also leave a clue to
1817         # allow us to squelch ALL outbound messages. This way we
1818         # can punt the logic of "what to do when we get a bounce"
1819         # to the scrip. We might want to notify nobody. Or just
1820         # the RT Owner. Or maybe all Privileged watchers.
1821         my ( $Sender, $junk ) = ParseSenderAddressFromHead($head);
1822         $head->add( 'RT-Squelch-Replies-To',    $Sender );
1823         $head->add( 'RT-DetectedAutoGenerated', 'true' );
1824     }
1825     return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
1826 }
1827
1828 =head2 IsCorrectAction
1829
1830 Returns a list of valid actions we've found for this message
1831
1832 =cut
1833
1834 sub IsCorrectAction {
1835     my $action = shift;
1836     my @actions = grep $_, split /-/, $action;
1837     return ( 0, '(no value)' ) unless @actions;
1838     foreach ( @actions ) {
1839         return ( 0, $_ ) unless /^(?:comment|correspond|take|resolve)$/;
1840     }
1841     return ( 1, @actions );
1842 }
1843
1844 sub _RecordSendEmailFailure {
1845     my $ticket = shift;
1846     if ($ticket) {
1847         $ticket->_RecordNote(
1848             NoteType => 'SystemError',
1849             Content => "Sending the previous mail has failed.  Please contact your admin, they can find more details in the logs.",
1850         );
1851         return 1;
1852     }
1853     else {
1854         $RT::Logger->error( "Can't record send email failure as ticket is missing" );
1855         return;
1856     }
1857 }
1858
1859 RT::Base->_ImportOverlays();
1860
1861 1;