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