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