401e970e8de4f336c4fce7324e746fd7ad8bb78a
[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 && !RT::EmailParser->IsRTAddress( $_ ),
1001         map lc $user->CanonicalizeEmailAddress( $_->address ),
1002         map Email::Address->parse( $args{'Head'}->get( $_ ) ),
1003         qw(To Cc);
1004 }
1005
1006
1007
1008 =head2 ParseSenderAddressFromHead HEAD
1009
1010 Takes a MIME::Header object. Returns a tuple: (user@host, friendly name)
1011 of the From (evaluated in order of Reply-To:, From:, Sender)
1012
1013 =cut
1014
1015 sub ParseSenderAddressFromHead {
1016     my $head = shift;
1017
1018     #Figure out who's sending this message.
1019     foreach my $header ('Reply-To', 'From', 'Sender') {
1020         my $addr_line = $head->get($header) || next;
1021         my ($addr, $name) = ParseAddressFromHeader( $addr_line );
1022         # only return if the address is not empty
1023         return ($addr, $name) if $addr;
1024     }
1025
1026     return (undef, undef);
1027 }
1028
1029 =head2 ParseErrorsToAddressFromHead HEAD
1030
1031 Takes a MIME::Header object. Return a single value : user@host
1032 of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
1033 From:, Sender)
1034
1035 =cut
1036
1037 sub ParseErrorsToAddressFromHead {
1038     my $head = shift;
1039
1040     #Figure out who's sending this message.
1041
1042     foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) {
1043
1044         # If there's a header of that name
1045         my $headerobj = $head->get($header);
1046         if ($headerobj) {
1047             my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
1048
1049             # If it's got actual useful content...
1050             return ($addr) if ($addr);
1051         }
1052     }
1053 }
1054
1055
1056
1057 =head2 ParseAddressFromHeader ADDRESS
1058
1059 Takes an address from C<$head->get('Line')> and returns a tuple: user@host, friendly name
1060
1061 =cut
1062
1063 sub ParseAddressFromHeader {
1064     my $Addr = shift;
1065
1066     # Some broken mailers send:  ""Vincent, Jesse"" <jesse@fsck.com>. Hate
1067     $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
1068     my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
1069
1070     my ($AddrObj) = grep ref $_, @Addresses;
1071     unless ( $AddrObj ) {
1072         return ( undef, undef );
1073     }
1074
1075     my $Name = ( $AddrObj->name || $AddrObj->phrase || $AddrObj->comment || $AddrObj->address );
1076
1077     #Lets take the from and load a user object.
1078     my $Address = $AddrObj->address;
1079
1080     return ( $Address, $Name );
1081 }
1082
1083 =head2 DeleteRecipientsFromHead HEAD RECIPIENTS
1084
1085 Gets a head object and list of addresses.
1086 Deletes addresses from To, Cc or Bcc fields.
1087
1088 =cut
1089
1090 sub DeleteRecipientsFromHead {
1091     my $head = shift;
1092     my %skip = map { lc $_ => 1 } @_;
1093
1094     foreach my $field ( qw(To Cc Bcc) ) {
1095         $head->set( $field =>
1096             join ', ', map $_->format, grep !$skip{ lc $_->address },
1097                 Email::Address->parse( $head->get( $field ) )
1098         );
1099     }
1100 }
1101
1102 sub GenMessageId {
1103     my %args = (
1104         Ticket      => undef,
1105         Scrip       => undef,
1106         ScripAction => undef,
1107         @_
1108     );
1109     my $org = RT->Config->Get('Organization');
1110     my $ticket_id = ( ref $args{'Ticket'}? $args{'Ticket'}->id : $args{'Ticket'} ) || 0;
1111     my $scrip_id = ( ref $args{'Scrip'}? $args{'Scrip'}->id : $args{'Scrip'} ) || 0;
1112     my $sent = ( ref $args{'ScripAction'}? $args{'ScripAction'}->{'_Message_ID'} : 0 ) || 0;
1113
1114     return "<rt-". $RT::VERSION ."-". $$ ."-". CORE::time() ."-". int(rand(2000)) .'.'
1115         . $ticket_id ."-". $scrip_id ."-". $sent ."@". $org .">" ;
1116 }
1117
1118 sub SetInReplyTo {
1119     my %args = (
1120         Message   => undef,
1121         InReplyTo => undef,
1122         Ticket    => undef,
1123         @_
1124     );
1125     return unless $args{'Message'} && $args{'InReplyTo'};
1126
1127     my $get_header = sub {
1128         my @res;
1129         if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
1130             @res = $args{'InReplyTo'}->head->get( shift );
1131         } else {
1132             @res = $args{'InReplyTo'}->GetHeader( shift ) || '';
1133         }
1134         return grep length, map { split /\s+/m, $_ } grep defined, @res;
1135     };
1136
1137     my @id = $get_header->('Message-ID');
1138     #XXX: custom header should begin with X- otherwise is violation of the standard
1139     my @rtid = $get_header->('RT-Message-ID');
1140     my @references = $get_header->('References');
1141     unless ( @references ) {
1142         @references = $get_header->('In-Reply-To');
1143     }
1144     push @references, @id, @rtid;
1145     if ( $args{'Ticket'} ) {
1146         my $pseudo_ref =  '<RT-Ticket-'. $args{'Ticket'}->id .'@'. RT->Config->Get('Organization') .'>';
1147         push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references;
1148     }
1149     @references = splice @references, 4, -6
1150         if @references > 10;
1151
1152     my $mail = $args{'Message'};
1153     $mail->head->set( 'In-Reply-To' => join ' ', @rtid? (@rtid) : (@id) ) if @id || @rtid;
1154     $mail->head->set( 'References' => join ' ', @references );
1155 }
1156
1157 sub ParseTicketId {
1158     my $Subject = shift;
1159
1160     my $rtname = RT->Config->Get('rtname');
1161     my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;
1162
1163     my $id;
1164     if ( $Subject =~ s/\[$test_name\s+\#(\d+)\s*\]//i ) {
1165         $id = $1;
1166     } else {
1167         foreach my $tag ( RT->System->SubjectTag ) {
1168             next unless $Subject =~ s/\[\Q$tag\E\s+\#(\d+)\s*\]//i;
1169             $id = $1;
1170             last;
1171         }
1172     }
1173     return undef unless $id;
1174
1175     $RT::Logger->debug("Found a ticket ID. It's $id");
1176     return $id;
1177 }
1178
1179 sub AddSubjectTag {
1180     my $subject = shift;
1181     my $ticket  = shift;
1182     unless ( ref $ticket ) {
1183         my $tmp = RT::Ticket->new( $RT::SystemUser );
1184         $tmp->Load( $ticket );
1185         $ticket = $tmp;
1186     }
1187     my $id = $ticket->id;
1188     my $queue_tag = $ticket->QueueObj->SubjectTag;
1189
1190     my $tag_re = RT->Config->Get('EmailSubjectTagRegex');
1191     unless ( $tag_re ) {
1192         my $tag = $queue_tag || RT->Config->Get('rtname');
1193         $tag_re = qr/\Q$tag\E/;
1194     } elsif ( $queue_tag ) {
1195         $tag_re = qr/$tag_re|\Q$queue_tag\E/;
1196     }
1197     return $subject if $subject =~ /\[$tag_re\s+#$id\]/;
1198
1199     $subject =~ s/(\r\n|\n|\s)/ /gi;
1200     chomp $subject;
1201     return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject";
1202 }
1203
1204
1205 =head2 Gateway ARGSREF
1206
1207
1208 Takes parameters:
1209
1210     action
1211     queue
1212     message
1213
1214
1215 This performs all the "guts" of the mail rt-mailgate program, and is
1216 designed to be called from the web interface with a message, user
1217 object, and so on.
1218
1219 Can also take an optional 'ticket' parameter; this ticket id overrides
1220 any ticket id found in the subject.
1221
1222 Returns:
1223
1224     An array of:
1225
1226     (status code, message, optional ticket object)
1227
1228     status code is a numeric value.
1229
1230       for temporary failures, the status code should be -75
1231
1232       for permanent failures which are handled by RT, the status code
1233       should be 0
1234
1235       for succces, the status code should be 1
1236
1237
1238
1239 =cut
1240
1241 sub _LoadPlugins {
1242     my @mail_plugins = @_;
1243
1244     my @res;
1245     foreach my $plugin (@mail_plugins) {
1246         if ( ref($plugin) eq "CODE" ) {
1247             push @res, $plugin;
1248         } elsif ( !ref $plugin ) {
1249             my $Class = $plugin;
1250             $Class = "RT::Interface::Email::" . $Class
1251                 unless $Class =~ /^RT::Interface::Email::/;
1252             $Class->require or
1253                 do { $RT::Logger->error("Couldn't load $Class: $@"); next };
1254
1255             no strict 'refs';
1256             unless ( defined *{ $Class . "::GetCurrentUser" }{CODE} ) {
1257                 $RT::Logger->crit( "No GetCurrentUser code found in $Class module");
1258                 next;
1259             }
1260             push @res, $Class;
1261         } else {
1262             $RT::Logger->crit( "$plugin - is not class name or code reference");
1263         }
1264     }
1265     return @res;
1266 }
1267
1268 sub Gateway {
1269     my $argsref = shift;
1270     my %args    = (
1271         action  => 'correspond',
1272         queue   => '1',
1273         ticket  => undef,
1274         message => undef,
1275         %$argsref
1276     );
1277
1278     my $SystemTicket;
1279     my $Right;
1280
1281     # Validate the action
1282     my ( $status, @actions ) = IsCorrectAction( $args{'action'} );
1283     unless ($status) {
1284         return (
1285             -75,
1286             "Invalid 'action' parameter "
1287                 . $actions[0]
1288                 . " for queue "
1289                 . $args{'queue'},
1290             undef
1291         );
1292     }
1293
1294     my $parser = RT::EmailParser->new();
1295     $parser->SmartParseMIMEEntityFromScalar(
1296         Message => $args{'message'},
1297         Decode => 0,
1298         Exact => 1,
1299     );
1300
1301     my $Message = $parser->Entity();
1302     unless ($Message) {
1303         MailError(
1304             Subject     => "RT Bounce: Unparseable message",
1305             Explanation => "RT couldn't process the message below",
1306             Attach      => $args{'message'}
1307         );
1308
1309         return ( 0,
1310             "Failed to parse this message. Something is likely badly wrong with the message"
1311         );
1312     }
1313
1314     my @mail_plugins = grep $_, RT->Config->Get('MailPlugins');
1315     push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
1316     @mail_plugins = _LoadPlugins( @mail_plugins );
1317
1318     my %skip_plugin;
1319     foreach my $class( grep !ref, @mail_plugins ) {
1320         # check if we should apply filter before decoding
1321         my $check_cb = do {
1322             no strict 'refs';
1323             *{ $class . "::ApplyBeforeDecode" }{CODE};
1324         };
1325         next unless defined $check_cb;
1326         next unless $check_cb->(
1327             Message       => $Message,
1328             RawMessageRef => \$args{'message'},
1329         );
1330
1331         $skip_plugin{ $class }++;
1332
1333         my $Code = do {
1334             no strict 'refs';
1335             *{ $class . "::GetCurrentUser" }{CODE};
1336         };
1337         my ($status, $msg) = $Code->(
1338             Message       => $Message,
1339             RawMessageRef => \$args{'message'},
1340         );
1341         next if $status > 0;
1342
1343         if ( $status == -2 ) {
1344             return (1, $msg, undef);
1345         } elsif ( $status == -1 ) {
1346             return (0, $msg, undef);
1347         }
1348     }
1349     @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins;
1350     $parser->_DecodeBodies;
1351     $parser->_PostProcessNewEntity;
1352
1353     my $head = $Message->head;
1354     my $ErrorsTo = ParseErrorsToAddressFromHead( $head );
1355
1356     my $MessageId = $head->get('Message-ID')
1357         || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
1358
1359     #Pull apart the subject line
1360     my $Subject = $head->get('Subject') || '';
1361     chomp $Subject;
1362     
1363     # {{{ Lets check for mail loops of various sorts.
1364     my ($should_store_machine_generated_message, $IsALoop, $result);
1365     ( $should_store_machine_generated_message, $ErrorsTo, $result, $IsALoop ) =
1366       _HandleMachineGeneratedMail(
1367         Message  => $Message,
1368         ErrorsTo => $ErrorsTo,
1369         Subject  => $Subject,
1370         MessageId => $MessageId
1371     );
1372
1373     # Do not pass loop messages to MailPlugins, to make sure the loop
1374     # is broken, unless $RT::StoreLoops is set.
1375     if ($IsALoop && !$should_store_machine_generated_message) {
1376         return ( 0, $result, undef );
1377     }
1378     # }}}
1379
1380     $args{'ticket'} ||= ParseTicketId( $Subject );
1381
1382     $SystemTicket = RT::Ticket->new( $RT::SystemUser );
1383     $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
1384     if ( $SystemTicket->id ) {
1385         $Right = 'ReplyToTicket';
1386     } else {
1387         $Right = 'CreateTicket';
1388     }
1389
1390     #Set up a queue object
1391     my $SystemQueueObj = RT::Queue->new( $RT::SystemUser );
1392     $SystemQueueObj->Load( $args{'queue'} );
1393
1394     # We can safely have no queue of we have a known-good ticket
1395     unless ( $SystemTicket->id || $SystemQueueObj->id ) {
1396         return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
1397     }
1398
1399     my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel(
1400         MailPlugins   => \@mail_plugins,
1401         Actions       => \@actions,
1402         Message       => $Message,
1403         RawMessageRef => \$args{message},
1404         SystemTicket  => $SystemTicket,
1405         SystemQueue   => $SystemQueueObj,
1406     );
1407
1408     # {{{ If authentication fails and no new user was created, get out.
1409     if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) {
1410
1411         # If the plugins refused to create one, they lose.
1412         unless ( $AuthStat == -1 ) {
1413             _NoAuthorizedUserFound(
1414                 Right     => $Right,
1415                 Message   => $Message,
1416                 Requestor => $ErrorsTo,
1417                 Queue     => $args{'queue'}
1418             );
1419
1420         }
1421         return ( 0, "Could not load a valid user", undef );
1422     }
1423
1424     # If we got a user, but they don't have the right to say things
1425     if ( $AuthStat == 0 ) {
1426         MailError(
1427             To          => $ErrorsTo,
1428             Subject     => "Permission Denied",
1429             Explanation =>
1430                 "You do not have permission to communicate with RT",
1431             MIMEObj => $Message
1432         );
1433         return (
1434             0,
1435             "$ErrorsTo tried to submit a message to "
1436                 . $args{'Queue'}
1437                 . " without permission.",
1438             undef
1439         );
1440     }
1441
1442
1443     unless ($should_store_machine_generated_message) {
1444         return ( 0, $result, undef );
1445     }
1446
1447     # if plugin's updated SystemTicket then update arguments
1448     $args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id;
1449
1450     my $Ticket = RT::Ticket->new($CurrentUser);
1451
1452     if ( !$args{'ticket'} && grep /^(comment|correspond)$/, @actions )
1453     {
1454
1455         my @Cc;
1456         my @Requestors = ( $CurrentUser->id );
1457
1458         if (RT->Config->Get('ParseNewMessageForTicketCcs')) {
1459             @Cc = ParseCcAddressesFromHead(
1460                 Head        => $head,
1461                 CurrentUser => $CurrentUser,
1462                 QueueObj    => $SystemQueueObj
1463             );
1464         }
1465
1466         my ( $id, $Transaction, $ErrStr ) = $Ticket->Create(
1467             Queue     => $SystemQueueObj->Id,
1468             Subject   => $Subject,
1469             Requestor => \@Requestors,
1470             Cc        => \@Cc,
1471             MIMEObj   => $Message
1472         );
1473         if ( $id == 0 ) {
1474             MailError(
1475                 To          => $ErrorsTo,
1476                 Subject     => "Ticket creation failed: $Subject",
1477                 Explanation => $ErrStr,
1478                 MIMEObj     => $Message
1479             );
1480             return ( 0, "Ticket creation failed: $ErrStr", $Ticket );
1481         }
1482
1483         # strip comments&corresponds from the actions we don't need
1484         # to record them if we've created the ticket just now
1485         @actions = grep !/^(comment|correspond)$/, @actions;
1486         $args{'ticket'} = $id;
1487
1488     } elsif ( $args{'ticket'} ) {
1489
1490         $Ticket->Load( $args{'ticket'} );
1491         unless ( $Ticket->Id ) {
1492             my $error = "Could not find a ticket with id " . $args{'ticket'};
1493             MailError(
1494                 To          => $ErrorsTo,
1495                 Subject     => "Message not recorded: $Subject",
1496                 Explanation => $error,
1497                 MIMEObj     => $Message
1498             );
1499
1500             return ( 0, $error );
1501         }
1502         $args{'ticket'} = $Ticket->id;
1503     } else {
1504         return ( 1, "Success", $Ticket );
1505     }
1506
1507     # }}}
1508
1509     my $unsafe_actions = RT->Config->Get('UnsafeEmailCommands');
1510     foreach my $action (@actions) {
1511
1512         #   If the action is comment, add a comment.
1513         if ( $action =~ /^(?:comment|correspond)$/i ) {
1514             my $method = ucfirst lc $action;
1515             my ( $status, $msg ) = $Ticket->$method( MIMEObj => $Message );
1516             unless ($status) {
1517
1518                 #Warn the sender that we couldn't actually submit the comment.
1519                 MailError(
1520                     To          => $ErrorsTo,
1521                     Subject     => "Message not recorded: $Subject",
1522                     Explanation => $msg,
1523                     MIMEObj     => $Message
1524                 );
1525                 return ( 0, "Message not recorded: $msg", $Ticket );
1526             }
1527         } elsif ($unsafe_actions) {
1528             my ( $status, $msg ) = _RunUnsafeAction(
1529                 Action      => $action,
1530                 ErrorsTo    => $ErrorsTo,
1531                 Message     => $Message,
1532                 Ticket      => $Ticket,
1533                 CurrentUser => $CurrentUser,
1534             );
1535             return ($status, $msg, $Ticket) unless $status == 1;
1536         }
1537     }
1538     return ( 1, "Success", $Ticket );
1539 }
1540
1541 =head2 GetAuthenticationLevel
1542
1543     # Authentication Level
1544     # -1 - Get out.  this user has been explicitly declined
1545     # 0 - User may not do anything (Not used at the moment)
1546     # 1 - Normal user
1547     # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate
1548
1549 =cut
1550
1551 sub GetAuthenticationLevel {
1552     my %args = (
1553         MailPlugins   => [],
1554         Actions       => [],
1555         Message       => undef,
1556         RawMessageRef => undef,
1557         SystemTicket  => undef,
1558         SystemQueue   => undef,
1559         @_,
1560     );
1561
1562     my ( $CurrentUser, $AuthStat, $error );
1563
1564     # Initalize AuthStat so comparisons work correctly
1565     $AuthStat = -9999999;
1566
1567     # if plugin returns AuthStat -2 we skip action
1568     # NOTE: this is experimental API and it would be changed
1569     my %skip_action = ();
1570
1571     # Since this needs loading, no matter what
1572     foreach (@{ $args{MailPlugins} }) {
1573         my ($Code, $NewAuthStat);
1574         if ( ref($_) eq "CODE" ) {
1575             $Code = $_;
1576         } else {
1577             no strict 'refs';
1578             $Code = *{ $_ . "::GetCurrentUser" }{CODE};
1579         }
1580
1581         foreach my $action (@{ $args{Actions} }) {
1582             ( $CurrentUser, $NewAuthStat ) = $Code->(
1583                 Message       => $args{Message},
1584                 RawMessageRef => $args{RawMessageRef},
1585                 CurrentUser   => $CurrentUser,
1586                 AuthLevel     => $AuthStat,
1587                 Action        => $action,
1588                 Ticket        => $args{SystemTicket},
1589                 Queue         => $args{SystemQueue},
1590             );
1591
1592 # You get the highest level of authentication you were assigned, unless you get the magic -1
1593 # If a module returns a "-1" then we discard the ticket, so.
1594             $AuthStat = $NewAuthStat
1595                 if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
1596
1597             last if $AuthStat == -1;
1598             $skip_action{$action}++ if $AuthStat == -2;
1599         }
1600
1601         # strip actions we should skip
1602         @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
1603             if $AuthStat == -2;
1604         last unless @{$args{Actions}};
1605
1606         last if $AuthStat == -1;
1607     }
1608
1609     return $AuthStat if !wantarray;
1610
1611     return ($AuthStat, $CurrentUser, $error);
1612 }
1613
1614 sub _RunUnsafeAction {
1615     my %args = (
1616         Action      => undef,
1617         ErrorsTo    => undef,
1618         Message     => undef,
1619         Ticket      => undef,
1620         CurrentUser => undef,
1621         @_
1622     );
1623
1624     if ( $args{'Action'} =~ /^take$/i ) {
1625         my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
1626         unless ($status) {
1627             MailError(
1628                 To          => $args{'ErrorsTo'},
1629                 Subject     => "Ticket not taken",
1630                 Explanation => $msg,
1631                 MIMEObj     => $args{'Message'}
1632             );
1633             return ( 0, "Ticket not taken" );
1634         }
1635     } elsif ( $args{'Action'} =~ /^resolve$/i ) {
1636         my ( $status, $msg ) = $args{'Ticket'}->SetStatus('resolved');
1637         unless ($status) {
1638
1639             #Warn the sender that we couldn't actually submit the comment.
1640             MailError(
1641                 To          => $args{'ErrorsTo'},
1642                 Subject     => "Ticket not resolved",
1643                 Explanation => $msg,
1644                 MIMEObj     => $args{'Message'}
1645             );
1646             return ( 0, "Ticket not resolved" );
1647         }
1648     } else {
1649         return ( 0, "Not supported unsafe action $args{'Action'}", $args{'Ticket'} );
1650     }
1651     return ( 1, "Success" );
1652 }
1653
1654 =head2 _NoAuthorizedUserFound
1655
1656 Emails the RT Owner and the requestor when the auth plugins return "No auth user found"
1657
1658 =cut
1659
1660 sub _NoAuthorizedUserFound {
1661     my %args = (
1662         Right     => undef,
1663         Message   => undef,
1664         Requestor => undef,
1665         Queue     => undef,
1666         @_
1667     );
1668
1669     # Notify the RT Admin of the failure.
1670     MailError(
1671         To          => RT->Config->Get('OwnerEmail'),
1672         Subject     => "Could not load a valid user",
1673         Explanation => <<EOT,
1674 RT could not load a valid user, and RT's configuration does not allow
1675 for the creation of a new user for this email (@{[$args{Requestor}]}).
1676
1677 You might need to grant 'Everyone' the right '@{[$args{Right}]}' for the
1678 queue @{[$args{'Queue'}]}.
1679
1680 EOT
1681         MIMEObj  => $args{'Message'},
1682         LogLevel => 'error'
1683     );
1684
1685     # Also notify the requestor that his request has been dropped.
1686     if ($args{'Requestor'} ne RT->Config->Get('OwnerEmail')) {
1687     MailError(
1688         To          => $args{'Requestor'},
1689         Subject     => "Could not load a valid user",
1690         Explanation => <<EOT,
1691 RT could not load a valid user, and RT's configuration does not allow
1692 for the creation of a new user for your email.
1693
1694 EOT
1695         MIMEObj  => $args{'Message'},
1696         LogLevel => 'error'
1697     );
1698     }
1699 }
1700
1701 =head2 _HandleMachineGeneratedMail
1702
1703 Takes named params:
1704     Message
1705     ErrorsTo
1706     Subject
1707
1708 Checks the message to see if it's a bounce, if it looks like a loop, if it's autogenerated, etc.
1709 Returns a triple of ("Should we continue (boolean)", "New value for $ErrorsTo", "Status message",
1710 "This message appears to be a loop (boolean)" );
1711
1712 =cut
1713
1714 sub _HandleMachineGeneratedMail {
1715     my %args = ( Message => undef, ErrorsTo => undef, Subject => undef, MessageId => undef, @_ );
1716     my $head = $args{'Message'}->head;
1717     my $ErrorsTo = $args{'ErrorsTo'};
1718
1719     my $IsBounce = CheckForBounce($head);
1720
1721     my $IsAutoGenerated = CheckForAutoGenerated($head);
1722
1723     my $IsSuspiciousSender = CheckForSuspiciousSender($head);
1724
1725     my $IsALoop = CheckForLoops($head);
1726
1727     my $SquelchReplies = 0;
1728
1729     my $owner_mail = RT->Config->Get('OwnerEmail');
1730
1731     #If the message is autogenerated, we need to know, so we can not
1732     # send mail to the sender
1733     if ( $IsBounce || $IsSuspiciousSender || $IsAutoGenerated || $IsALoop ) {
1734         $SquelchReplies = 1;
1735         $ErrorsTo       = $owner_mail;
1736     }
1737
1738     # Warn someone if it's a loop, before we drop it on the ground
1739     if ($IsALoop) {
1740         $RT::Logger->crit("RT Received mail (".$args{MessageId}.") from itself.");
1741
1742         #Should we mail it to RTOwner?
1743         if ( RT->Config->Get('LoopsToRTOwner') ) {
1744             MailError(
1745                 To          => $owner_mail,
1746                 Subject     => "RT Bounce: ".$args{'Subject'},
1747                 Explanation => "RT thinks this message may be a bounce",
1748                 MIMEObj     => $args{Message}
1749             );
1750         }
1751
1752         #Do we actually want to store it?
1753         return ( 0, $ErrorsTo, "Message Bounced", $IsALoop )
1754             unless RT->Config->Get('StoreLoops');
1755     }
1756
1757     # Squelch replies if necessary
1758     # Don't let the user stuff the RT-Squelch-Replies-To header.
1759     if ( $head->get('RT-Squelch-Replies-To') ) {
1760         $head->add(
1761             'RT-Relocated-Squelch-Replies-To',
1762             $head->get('RT-Squelch-Replies-To')
1763         );
1764         $head->delete('RT-Squelch-Replies-To');
1765     }
1766
1767     if ($SquelchReplies) {
1768
1769         # Squelch replies to the sender, and also leave a clue to
1770         # allow us to squelch ALL outbound messages. This way we
1771         # can punt the logic of "what to do when we get a bounce"
1772         # to the scrip. We might want to notify nobody. Or just
1773         # the RT Owner. Or maybe all Privileged watchers.
1774         my ( $Sender, $junk ) = ParseSenderAddressFromHead($head);
1775         $head->add( 'RT-Squelch-Replies-To',    $Sender );
1776         $head->add( 'RT-DetectedAutoGenerated', 'true' );
1777     }
1778     return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
1779 }
1780
1781 =head2 IsCorrectAction
1782
1783 Returns a list of valid actions we've found for this message
1784
1785 =cut
1786
1787 sub IsCorrectAction {
1788     my $action = shift;
1789     my @actions = grep $_, split /-/, $action;
1790     return ( 0, '(no value)' ) unless @actions;
1791     foreach ( @actions ) {
1792         return ( 0, $_ ) unless /^(?:comment|correspond|take|resolve)$/;
1793     }
1794     return ( 1, @actions );
1795 }
1796
1797 sub _RecordSendEmailFailure {
1798     my $ticket = shift;
1799     if ($ticket) {
1800         $ticket->_RecordNote(
1801             NoteType => 'SystemError',
1802             Content => "Sending the previous mail has failed.  Please contact your admin, they can find more details in the logs.",
1803         );
1804         return 1;
1805     }
1806     else {
1807         $RT::Logger->error( "Can't record send email failure as ticket is missing" );
1808         return;
1809     }
1810 }
1811
1812 RT::Base->_ImportOverlays();
1813
1814 1;