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