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