1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 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# Portions Copyright 2000 Tobias Brox <tobix@cpan.org>
50
51package RT::Action::SendEmail;
52
53use strict;
54use warnings;
55
56use base qw(RT::Action);
57
58use RT::EmailParser;
59use RT::Interface::Email;
60use Email::Address;
61use List::MoreUtils qw(uniq);
62our @EMAIL_RECIPIENT_HEADERS = qw(To Cc Bcc);
63
64
65=head1 NAME
66
67RT::Action::SendEmail - An Action which users can use to send mail
68or can subclassed for more specialized mail sending behavior.
69RT::Action::AutoReply is a good example subclass.
70
71=head1 SYNOPSIS
72
73  use base 'RT::Action::SendEmail';
74
75=head1 DESCRIPTION
76
77Basically, you create another module RT::Action::YourAction which ISA
78RT::Action::SendEmail.
79
80=head1 METHODS
81
82=head2 CleanSlate
83
84Cleans class-wide options, like L</AttachTickets>.
85
86=cut
87
88sub CleanSlate {
89    my $self = shift;
90    $self->AttachTickets(undef);
91}
92
93=head2 Commit
94
95Sends the prepared message and writes outgoing record into DB if the feature is
96activated in the config.
97
98=cut
99
100sub Commit {
101    my $self = shift;
102
103    return abs $self->SendMessage( $self->TemplateObj->MIMEObj )
104        unless RT->Config->Get('RecordOutgoingEmail');
105
106    $self->DeferDigestRecipients();
107    my $message = $self->TemplateObj->MIMEObj;
108
109    my $orig_message;
110    $orig_message = $message->dup if RT::Interface::Email::WillSignEncrypt(
111        Attachment => $self->TransactionObj->Attachments->First,
112        Ticket     => $self->TicketObj,
113    );
114
115    my ($ret) = $self->SendMessage($message);
116    return abs( $ret ) if $ret <= 0;
117
118    if ($orig_message) {
119        $message->attach(
120            Type        => 'application/x-rt-original-message',
121            Disposition => 'inline',
122            Data        => $orig_message->as_string,
123        );
124    }
125    $self->RecordOutgoingMailTransaction($message);
126    $self->RecordDeferredRecipients();
127    return 1;
128}
129
130=head2 Prepare
131
132Builds an outgoing email we're going to send using scrip's template.
133
134=cut
135
136sub Prepare {
137    my $self = shift;
138
139    unless ( $self->TemplateObj->MIMEObj ) {
140        my ( $result, $message ) = $self->TemplateObj->Parse(
141            Argument       => $self->Argument,
142            TicketObj      => $self->TicketObj,
143            TransactionObj => $self->TransactionObj
144        );
145        if ( !$result ) {
146            return (undef);
147        }
148    }
149
150    my $MIMEObj = $self->TemplateObj->MIMEObj;
151
152    # Header
153    $self->SetRTSpecialHeaders();
154
155    my %seen;
156    foreach my $type (@EMAIL_RECIPIENT_HEADERS) {
157        @{ $self->{$type} }
158            = grep defined && length && !$seen{ lc $_ }++,
159            @{ $self->{$type} };
160    }
161
162    $self->RemoveInappropriateRecipients();
163
164    # Go add all the Tos, Ccs and Bccs that we need to to the message to
165    # make it happy, but only if we actually have values in those arrays.
166
167# TODO: We should be pulling the recipients out of the template and shove them into To, Cc and Bcc
168
169    for my $header (@EMAIL_RECIPIENT_HEADERS) {
170
171        $self->SetHeader( $header, join( ', ', @{ $self->{$header} } ) )
172          if (!$MIMEObj->head->get($header)
173            && $self->{$header}
174            && @{ $self->{$header} } );
175    }
176    # PseudoTo (fake to headers) shouldn't get matched for message recipients.
177    # If we don't have any 'To' header (but do have other recipients), drop in
178    # the pseudo-to header.
179    $self->SetHeader( 'To', join( ', ', @{ $self->{'PseudoTo'} } ) )
180        if $self->{'PseudoTo'}
181            && @{ $self->{'PseudoTo'} }
182            && !$MIMEObj->head->get('To')
183            && ( $MIMEObj->head->get('Cc') or $MIMEObj->head->get('Bcc') );
184
185    # For security reasons, we only send out textual mails.
186    foreach my $part ( grep !$_->is_multipart, $MIMEObj->parts_DFS ) {
187        my $type = $part->mime_type || 'text/plain';
188        $type = 'text/plain' unless RT::I18N::IsTextualContentType($type);
189        $part->head->mime_attr( "Content-Type" => $type );
190        # utf-8 here is for _FindOrGuessCharset in I18N.pm
191        # it's not the final charset/encoding sent
192        $part->head->mime_attr( "Content-Type.charset" => 'utf-8' );
193
194        # Set proper transfer encoding to prevent long lines in
195        # body(1000+ chars) that are not allowed according to RFC821.
196        # Some mail servers automatically insert "!" into long lines to
197        # indicate this incompatibility.
198        if ( !$part->head->mime_attr('Content-Transfer-Encoding') ) {
199            $part->head->mime_attr( 'Content-Transfer-Encoding' => $part->suggest_encoding );
200        }
201    }
202
203    RT::I18N::SetMIMEEntityToEncoding(
204        Entity        => $MIMEObj,
205        Encoding      => RT->Config->Get('EmailOutputEncoding'),
206        PreserveWords => 1,
207        IsOut         => 1,
208    );
209
210    # Build up a MIME::Entity that looks like the original message.
211    $self->AddAttachments if ( $MIMEObj->head->get('RT-Attach-Message')
212                               && ( $MIMEObj->head->get('RT-Attach-Message') !~ /^(n|no|0|off|false)$/i ) );
213
214    $self->AddTickets;
215
216    my $attachment = $self->TransactionObj->Attachments->First;
217    if ($attachment
218        && !(
219               $attachment->GetHeader('X-RT-Encrypt')
220            || $self->TicketObj->QueueObj->Encrypt
221        )
222        )
223    {
224        $attachment->SetHeader( 'X-RT-Encrypt' => 1 )
225            if ( $attachment->GetHeader("X-RT-Incoming-Encryption") || '' ) eq
226            'Success';
227    }
228
229    return 1;
230}
231
232=head2 To
233
234Returns an array of L<Email::Address> objects containing all the To: recipients for this notification
235
236=cut
237
238sub To {
239    my $self = shift;
240    return ( $self->AddressesFromHeader('To') );
241}
242
243=head2 Cc
244
245Returns an array of L<Email::Address> objects containing all the Cc: recipients for this notification
246
247=cut
248
249sub Cc {
250    my $self = shift;
251    return ( $self->AddressesFromHeader('Cc') );
252}
253
254=head2 Bcc
255
256Returns an array of L<Email::Address> objects containing all the Bcc: recipients for this notification
257
258=cut
259
260sub Bcc {
261    my $self = shift;
262    return ( $self->AddressesFromHeader('Bcc') );
263
264}
265
266sub AddressesFromHeader {
267    my $self      = shift;
268    my $field     = shift;
269    my $header    = Encode::decode("UTF-8",$self->TemplateObj->MIMEObj->head->get($field));
270    my @addresses = Email::Address->parse($header);
271
272    return (@addresses);
273}
274
275=head2 SendMessage MIMEObj
276
277sends the message using RT's preferred API.
278TODO: Break this out to a separate module
279
280=cut
281
282sub SendMessage {
283
284    # DO NOT SHIFT @_ in this subroutine.  It breaks Hook::LexWrap's
285    # ability to pass @_ to a 'post' routine.
286    my ( $self, $MIMEObj ) = @_;
287
288    my $msgid = Encode::decode( "UTF-8", $MIMEObj->head->get('Message-ID') );
289    chomp $msgid;
290
291    $self->ScripActionObj->{_Message_ID}++;
292
293    $RT::Logger->info( $msgid . " #"
294            . $self->TicketObj->id . "/"
295            . $self->TransactionObj->id
296            . " - Scrip "
297            . ($self->ScripObj->id || '#rule'). " "
298            . ( $self->ScripObj->Description || '' ) );
299
300    my $status = RT::Interface::Email::SendEmail(
301        Entity      => $MIMEObj,
302        Ticket      => $self->TicketObj,
303        Transaction => $self->TransactionObj,
304    );
305
306
307    return $status unless ($status > 0 || exists $self->{'Deferred'});
308
309    my $success = $msgid . " sent ";
310    foreach (@EMAIL_RECIPIENT_HEADERS) {
311        my $recipients = Encode::decode( "UTF-8", $MIMEObj->head->get($_) );
312        $success .= " $_: " . $recipients if $recipients;
313    }
314
315    if( exists $self->{'Deferred'} ) {
316        for (qw(daily weekly susp)) {
317            $success .= "\nBatched email $_ for: ". join(", ", keys %{ $self->{'Deferred'}{ $_ } } )
318                if exists $self->{'Deferred'}{ $_ };
319        }
320    }
321
322    $success =~ s/\n//g;
323
324    $RT::Logger->info($success);
325
326    return (1);
327}
328
329=head2 AttachableFromTransaction
330
331Function (not method) that takes an L<RT::Transaction> and returns an
332L<RT::Attachments> collection of attachments suitable for attaching to an
333email.
334
335=cut
336
337sub AttachableFromTransaction {
338    my $txn = shift;
339
340    my $attachments = RT::Attachments->new( RT->SystemUser );
341    $attachments->Limit(
342        FIELD => 'TransactionId',
343        VALUE => $txn->Id
344    );
345
346    # Don't attach anything blank
347    $attachments->LimitNotEmpty;
348    $attachments->OrderBy( FIELD => 'id' );
349
350    # We want to make sure that we don't include the attachment that's
351    # being used as the "Content" of this message" unless that attachment's
352    # content type is not like text/...
353    my $transaction_content_obj = $txn->ContentObj;
354
355    if (   $transaction_content_obj
356        && $transaction_content_obj->ContentType =~ m{text/}i )
357    {
358        # If this was part of a multipart/alternative, skip all of the kids
359        my $parent = $transaction_content_obj->ParentObj;
360        if ($parent and $parent->Id and $parent->ContentType eq "multipart/alternative") {
361            $attachments->Limit(
362                ENTRYAGGREGATOR => 'AND',
363                FIELD           => 'parent',
364                OPERATOR        => '!=',
365                VALUE           => $parent->Id,
366            );
367        } else {
368            $attachments->Limit(
369                ENTRYAGGREGATOR => 'AND',
370                FIELD           => 'id',
371                OPERATOR        => '!=',
372                VALUE           => $transaction_content_obj->Id,
373            );
374        }
375    }
376
377    return $attachments;
378}
379
380=head2 AddAttachments
381
382Takes any attachments to this transaction and attaches them to the message
383we're building.
384
385=cut
386
387sub AddAttachments {
388    my $self = shift;
389
390    my $MIMEObj = $self->TemplateObj->MIMEObj;
391
392    $MIMEObj->head->delete('RT-Attach-Message');
393
394    my $attachments = AttachableFromTransaction($self->TransactionObj);
395
396    # attach any of this transaction's attachments
397    my $seen_attachment = 0;
398    while ( my $attach = $attachments->Next ) {
399        if ( !$seen_attachment ) {
400            $MIMEObj->make_multipart( 'mixed', Force => 1 );
401            $seen_attachment = 1;
402        }
403        $self->AddAttachment($attach);
404    }
405
406    # attach any attachments requested by the transaction or template
407    # that aren't part of the transaction itself
408    $self->AddAttachmentsFromHeaders;
409}
410
411=head2 AddAttachmentsFromHeaders
412
413Add attachments requested by the transaction or template that aren't part of
414the transaction itself.
415
416This inspects C<RT-Attach> headers, which are expected to contain an
417L<RT::Attachment> ID that the transaction's creator can See.
418
419L<RT::Ticket->_RecordNote> accepts an C<AttachExisting> argument which sets
420C<RT-Attach> headers appropriately on Comment/Correspond.
421
422=cut
423
424sub AddAttachmentsFromHeaders {
425    my $self  = shift;
426    my $email = $self->TemplateObj->MIMEObj;
427
428    # Add the RT-Attach headers from the transaction to the email
429    if (my $attachment = $self->TransactionObj->Attachments->First) {
430        for my $id ($attachment->GetAllHeaders('RT-Attach')) {
431            $email->head->add('RT-Attach' => $id);
432        }
433    }
434
435    # Take all RT-Attach headers and add the attachments to the outgoing mail
436    for my $id (uniq $email->head->get_all('RT-Attach')) {
437        $id =~ s/(?:^\s*|\s*$)//g;
438
439        my $attach = RT::Attachment->new( $self->TransactionObj->CreatorObj );
440        $attach->Load($id);
441        next unless $attach->Id
442                and $attach->TransactionObj->CurrentUserCanSee;
443
444        $email->make_multipart( 'mixed', Force => 1 )
445          unless $email->effective_type eq 'multipart/mixed';
446        $self->AddAttachment($attach, $email);
447    }
448}
449
450=head2 AddAttachment $attachment
451
452Takes one attachment object of L<RT::Attachment> class and attaches it to the message
453we're building.
454
455=cut
456
457sub AddAttachment {
458    my $self    = shift;
459    my $attach  = shift;
460    my $MIMEObj = shift || $self->TemplateObj->MIMEObj;
461
462    # $attach->TransactionObj may not always be $self->TransactionObj
463    return unless $attach->Id
464              and $attach->TransactionObj->CurrentUserCanSee;
465
466    # ->attach expects just the disposition type; extract it if we have the header
467    # or default to "attachment"
468    my $disp = ($attach->GetHeader('Content-Disposition') || '')
469                    =~ /^\s*(inline|attachment)/i ? $1 : "attachment";
470
471    $MIMEObj->attach(
472        Type        => $attach->ContentType,
473        Charset     => $attach->OriginalEncoding,
474        Data        => $attach->OriginalContent,
475        Disposition => $disp,
476        Filename    => $self->MIMEEncodeString( $attach->Filename ),
477        Id          => $attach->GetHeader('Content-ID'),
478        'RT-Attachment:' => $self->TicketObj->Id . "/"
479            . $self->TransactionObj->Id . "/"
480            . $attach->id,
481        Encoding => '-SUGGEST',
482    );
483}
484
485=head2 AttachTickets [@IDs]
486
487Returns or set list of ticket's IDs that should be attached to an outgoing message.
488
489B<Note> this method works as a class method and setup things global, so you have to
490clean list by passing undef as argument.
491
492=cut
493
494{
495    my $list = [];
496
497    sub AttachTickets {
498        my $self = shift;
499        $list = [ grep defined, @_ ] if @_;
500        return @$list;
501    }
502}
503
504=head2 AddTickets
505
506Attaches tickets to the current message, list of tickets' ids get from
507L</AttachTickets> method.
508
509=cut
510
511sub AddTickets {
512    my $self = shift;
513    $self->AddTicket($_) foreach $self->AttachTickets;
514    return;
515}
516
517=head2 AddTicket $ID
518
519Attaches a ticket with ID to the message.
520
521Each ticket is attached as multipart entity and all its messages and attachments
522are attached as sub entities in order of creation, but only if transaction type
523is Create or Correspond.
524
525=cut
526
527sub AddTicket {
528    my $self = shift;
529    my $tid  = shift;
530
531    my $attachs   = RT::Attachments->new( $self->TransactionObj->CreatorObj );
532    my $txn_alias = $attachs->TransactionAlias;
533    $attachs->Limit(
534        ALIAS    => $txn_alias,
535        FIELD    => 'Type',
536        OPERATOR => 'IN',
537        VALUE    => [qw(Create Correspond)],
538    );
539    $attachs->LimitByTicket($tid);
540    $attachs->LimitNotEmpty;
541    $attachs->OrderBy( FIELD => 'Created' );
542
543    my $ticket_mime = MIME::Entity->build(
544        Type        => 'multipart/mixed',
545        Top         => 0,
546        Description => "ticket #$tid",
547    );
548    while ( my $attachment = $attachs->Next ) {
549        $self->AddAttachment( $attachment, $ticket_mime );
550    }
551    if ( $ticket_mime->parts ) {
552        my $email_mime = $self->TemplateObj->MIMEObj;
553        $email_mime->make_multipart( 'mixed', Force => 1 )
554            unless $email_mime->effective_type eq 'multipart/mixed';
555        $email_mime->add_part($ticket_mime);
556    }
557    return;
558}
559
560=head2 RecordOutgoingMailTransaction MIMEObj
561
562Record a transaction in RT with this outgoing message for future record-keeping purposes
563
564=cut
565
566sub RecordOutgoingMailTransaction {
567    my $self    = shift;
568    my $MIMEObj = shift;
569
570    my @parts = $MIMEObj->parts;
571    my @attachments;
572    my @keep;
573    foreach my $part (@parts) {
574        my $attach = $part->head->get('RT-Attachment');
575        if ($attach) {
576            $RT::Logger->debug(
577                "We found an attachment. we want to not record it.");
578            push @attachments, $attach;
579        } else {
580            $RT::Logger->debug("We found a part. we want to record it.");
581            push @keep, $part;
582        }
583    }
584    $MIMEObj->parts( \@keep );
585    foreach my $attachment (@attachments) {
586        $MIMEObj->head->add( 'RT-Attachment', $attachment );
587    }
588
589    RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, 'utf-8', 'mime_words_ok' );
590
591    my $transaction
592        = RT::Transaction->new( $self->TransactionObj->CurrentUser );
593
594# XXX: TODO -> Record attachments as references to things in the attachments table, maybe.
595
596    my $type;
597    if ( $self->TransactionObj->Type eq 'Comment' ) {
598        $type = 'CommentEmailRecord';
599    } else {
600        $type = 'EmailRecord';
601    }
602
603    my $msgid = Encode::decode( "UTF-8", $MIMEObj->head->get('Message-ID') );
604    chomp $msgid;
605
606    my ( $id, $msg ) = $transaction->Create(
607        Ticket         => $self->TicketObj->Id,
608        Type           => $type,
609        Data           => $msgid,
610        MIMEObj        => $MIMEObj,
611        ActivateScrips => 0
612    );
613
614    if ($id) {
615        $self->{'OutgoingMailTransaction'} = $id;
616    } else {
617        $RT::Logger->warning(
618            "Could not record outgoing message transaction: $msg");
619    }
620    return $id;
621}
622
623=head2 SetRTSpecialHeaders
624
625This routine adds all the random headers that RT wants in a mail message
626that don't matter much to anybody else.
627
628=cut
629
630sub SetRTSpecialHeaders {
631    my $self = shift;
632
633    $self->SetSubject();
634    $self->SetSubjectToken();
635    $self->SetHeaderAsEncoding( 'Subject',
636        RT->Config->Get('EmailOutputEncoding') )
637        if ( RT->Config->Get('EmailOutputEncoding') );
638    $self->SetReturnAddress();
639    $self->SetReferencesHeaders();
640
641    unless ( $self->TemplateObj->MIMEObj->head->get('Message-ID') ) {
642
643        # Get Message-ID for this txn
644        my $msgid = "";
645        if ( my $msg = $self->TransactionObj->Message->First ) {
646            $msgid = $msg->GetHeader("RT-Message-ID")
647                || $msg->GetHeader("Message-ID");
648        }
649
650        # If there is one, and we can parse it, then base our Message-ID on it
651        if (    $msgid
652            and $msgid
653            =~ s/<(rt-.*?-\d+-\d+)\.(\d+)-\d+-\d+\@\QRT->Config->Get('Organization')\E>$/
654                         "<$1." . $self->TicketObj->id
655                          . "-" . $self->ScripObj->id
656                          . "-" . $self->ScripActionObj->{_Message_ID}
657                          . "@" . RT->Config->Get('Organization') . ">"/eg
658            and $2 == $self->TicketObj->id
659            )
660        {
661            $self->SetHeader( "Message-ID" => $msgid );
662        } else {
663            $self->SetHeader(
664                'Message-ID' => RT::Interface::Email::GenMessageId(
665                    Ticket      => $self->TicketObj,
666                    Scrip       => $self->ScripObj,
667                    ScripAction => $self->ScripActionObj
668                ),
669            );
670        }
671    }
672
673    $self->SetHeader( 'X-RT-Loop-Prevention', RT->Config->Get('rtname') );
674    $self->SetHeader( 'X-RT-Ticket',
675        RT->Config->Get('rtname') . " #" . $self->TicketObj->id() );
676    $self->SetHeader( 'X-Managed-by',
677        "RT $RT::VERSION (http://www.bestpractical.com/rt/)" );
678
679# XXX, TODO: use /ShowUser/ShowUserEntry(or something like that) when it would be
680#            refactored into user's method.
681    if ( my $email = $self->TransactionObj->CreatorObj->EmailAddress
682         and ! defined $self->TemplateObj->MIMEObj->head->get("RT-Originator")
683         and RT->Config->Get('UseOriginatorHeader')
684    ) {
685        $self->SetHeader( 'X-RT-Originator', $email );
686    }
687
688}
689
690
691sub DeferDigestRecipients {
692    my $self = shift;
693    $RT::Logger->debug( "Calling SetRecipientDigests for transaction " . $self->TransactionObj . ", id " . $self->TransactionObj->id );
694
695    # The digest attribute will be an array of notifications that need to
696    # be sent for this transaction.  The array will have the following
697    # format for its objects.
698    # $digest_hash -> {daily|weekly|susp} -> address -> {To|Cc|Bcc}
699    #                                     -> sent -> {true|false}
700    # The "sent" flag will be used by the cron job to indicate that it has
701    # run on this transaction.
702    # In a perfect world we might move this hash construction to the
703    # extension module itself.
704    my $digest_hash = {};
705
706    foreach my $mailfield (@EMAIL_RECIPIENT_HEADERS) {
707        # If we have a "PseudoTo", the "To" contains it, so we don't need to access it
708        next if ( ( $self->{'PseudoTo'} && @{ $self->{'PseudoTo'} } ) && ( $mailfield eq 'To' ) );
709        $RT::Logger->debug( "Working on mailfield $mailfield; recipients are " . join( ',', @{ $self->{$mailfield} } ) );
710
711        # Store the 'daily digest' folk in an array.
712        my ( @send_now, @daily_digest, @weekly_digest, @suspended );
713
714        # Have to get the list of addresses directly from the MIME header
715        # at this point.
716        $RT::Logger->debug( Encode::decode( "UTF-8", $self->TemplateObj->MIMEObj->head->as_string ) );
717        foreach my $rcpt ( map { $_->address } $self->AddressesFromHeader($mailfield) ) {
718            next unless $rcpt;
719            my $user_obj = RT::User->new(RT->SystemUser);
720            $user_obj->LoadByEmail($rcpt);
721            if  ( ! $user_obj->id ) {
722                # If there's an email address in here without an associated
723                # RT user, pass it on through.
724                $RT::Logger->debug( "User $rcpt is not associated with an RT user object.  Send mail.");
725                push( @send_now, $rcpt );
726                next;
727            }
728
729            my $mailpref = RT->Config->Get( 'EmailFrequency', $user_obj ) || '';
730            $RT::Logger->debug( "Got user mail preference '$mailpref' for user $rcpt");
731
732            if ( $mailpref =~ /daily/i ) { push( @daily_digest, $rcpt ) }
733            elsif ( $mailpref =~ /weekly/i ) { push( @weekly_digest, $rcpt ) }
734            elsif ( $mailpref =~ /suspend/i ) { push( @suspended, $rcpt ) }
735            else { push( @send_now, $rcpt ) }
736        }
737
738        # Reset the relevant mail field.
739        $RT::Logger->debug( "Removing deferred recipients from $mailfield: line");
740        if (@send_now) {
741            $self->SetHeader( $mailfield, join( ', ', @send_now ) );
742        } else {    # No recipients!  Remove the header.
743            $self->TemplateObj->MIMEObj->head->delete($mailfield);
744        }
745
746        # Push the deferred addresses into the appropriate field in
747        # our attribute hash, with the appropriate mail header.
748        $RT::Logger->debug(
749            "Setting deferred recipients for attribute creation");
750        $digest_hash->{'daily'}->{$_} = {'header' => $mailfield , _sent => 0}  for (@daily_digest);
751        $digest_hash->{'weekly'}->{$_} ={'header' =>  $mailfield, _sent => 0}  for (@weekly_digest);
752        $digest_hash->{'susp'}->{$_} = {'header' => $mailfield, _sent =>0 }  for (@suspended);
753    }
754
755    if ( scalar keys %$digest_hash ) {
756
757        # Save the hash so that we can add it as an attribute to the
758        # outgoing email transaction.
759        $self->{'Deferred'} = $digest_hash;
760    } else {
761        $RT::Logger->debug( "No recipients found for deferred delivery on "
762                . "transaction #"
763                . $self->TransactionObj->id );
764    }
765}
766
767
768
769sub RecordDeferredRecipients {
770    my $self = shift;
771    return unless exists $self->{'Deferred'};
772
773    my $txn_id = $self->{'OutgoingMailTransaction'};
774    return unless $txn_id;
775
776    my $txn_obj = RT::Transaction->new( $self->CurrentUser );
777    $txn_obj->Load( $txn_id );
778    my( $ret, $msg ) = $txn_obj->AddAttribute(
779        Name => 'DeferredRecipients',
780        Content => $self->{'Deferred'}
781    );
782    $RT::Logger->warning( "Unable to add deferred recipients to outgoing transaction: $msg" )
783        unless $ret;
784
785    return ($ret,$msg);
786}
787
788=head2 SquelchMailTo
789
790Returns list of the addresses to squelch on this transaction.
791
792=cut
793
794sub SquelchMailTo {
795    my $self = shift;
796    return map $_->Content, $self->TransactionObj->SquelchMailTo;
797}
798
799=head2 RemoveInappropriateRecipients
800
801Remove addresses that are RT addresses or that are on this transaction's blacklist
802
803=cut
804
805my %squelch_reasons = (
806    'not privileged'
807        => "because autogenerated messages are configured to only be sent to privileged users (RedistributeAutoGeneratedMessages)",
808    'squelch:attachment'
809        => "by RT-Squelch-Replies-To header in the incoming message",
810    'squelch:transaction'
811        => "by notification checkboxes for this transaction",
812    'squelch:ticket'
813        => "by notification checkboxes on this ticket's People page",
814);
815
816
817sub RemoveInappropriateRecipients {
818    my $self = shift;
819
820    my %blacklist = ();
821
822    # If there are no recipients, don't try to send the message.
823    # If the transaction has content and has the header RT-Squelch-Replies-To
824
825    my $msgid = Encode::decode( "UTF-8", $self->TemplateObj->MIMEObj->head->get('Message-Id') );
826    chomp $msgid;
827
828    if ( my $attachment = $self->TransactionObj->Attachments->First ) {
829
830        if ( $attachment->GetHeader('RT-DetectedAutoGenerated') ) {
831
832            # What do we want to do with this? It's probably (?) a bounce
833            # caused by one of the watcher addresses being broken.
834            # Default ("true") is to redistribute, for historical reasons.
835
836            my $redistribute = RT->Config->Get('RedistributeAutoGeneratedMessages');
837
838            if ( !$redistribute ) {
839
840                # Don't send to any watchers.
841                @{ $self->{$_} } = () for (@EMAIL_RECIPIENT_HEADERS);
842                $RT::Logger->info( $msgid
843                        . " The incoming message was autogenerated. "
844                        . "Not redistributing this message based on site configuration."
845                );
846            } elsif ( $redistribute eq 'privileged' ) {
847
848                # Only send to "privileged" watchers.
849                foreach my $type (@EMAIL_RECIPIENT_HEADERS) {
850                    foreach my $addr ( @{ $self->{$type} } ) {
851                        my $user = RT::User->new(RT->SystemUser);
852                        $user->LoadByEmail($addr);
853                        $blacklist{ $addr } ||= 'not privileged'
854                            unless $user->id && $user->Privileged;
855                    }
856                }
857                $RT::Logger->info( $msgid
858                        . " The incoming message was autogenerated. "
859                        . "Not redistributing this message to unprivileged users based on site configuration."
860                );
861            }
862        }
863
864        if ( my $squelch = $attachment->GetHeader('RT-Squelch-Replies-To') ) {
865            $blacklist{ $_->address } ||= 'squelch:attachment'
866                foreach Email::Address->parse( $squelch );
867        }
868    }
869
870    # Let's grab the SquelchMailTo attributes and push those entries
871    # into the blacklisted
872    $blacklist{ $_->Content } ||= 'squelch:transaction'
873        foreach $self->TransactionObj->SquelchMailTo;
874    $blacklist{ $_->Content } ||= 'squelch:ticket'
875        foreach $self->TicketObj->SquelchMailTo;
876
877    # canonicalize emails
878    foreach my $address ( keys %blacklist ) {
879        my $reason = delete $blacklist{ $address };
880        $blacklist{ lc $_ } = $reason
881            foreach map RT::User->CanonicalizeEmailAddress( $_->address ),
882            Email::Address->parse( $address );
883    }
884
885    $self->RecipientFilter(
886        Callback => sub {
887            return unless RT::EmailParser->IsRTAddress( $_[0] );
888            return "$_[0] appears to point to this RT instance. Skipping";
889        },
890        All => 1,
891    );
892
893    $self->RecipientFilter(
894        Callback => sub {
895            return unless $blacklist{ lc $_[0] };
896            return "$_[0] is blacklisted $squelch_reasons{ $blacklist{ lc $_[0] } }. Skipping";
897        },
898    );
899
900
901    # Cycle through the people we're sending to and pull out anyone that meets any of the callbacks
902    for my $type (@EMAIL_RECIPIENT_HEADERS) {
903        my @addrs;
904
905      ADDRESS:
906        for my $addr ( @{ $self->{$type} } ) {
907            for my $filter ( map {$_->{Callback}} @{$self->{RecipientFilter}} ) {
908                my $skip = $filter->($addr);
909                next unless $skip;
910                $RT::Logger->info( "$msgid $skip" );
911                next ADDRESS;
912            }
913            push @addrs, $addr;
914        }
915
916      NOSQUELCH_ADDRESS:
917        for my $addr ( @{ $self->{NoSquelch}{$type} } ) {
918            for my $filter ( map {$_->{Callback}} grep {$_->{All}} @{$self->{RecipientFilter}} ) {
919                my $skip = $filter->($addr);
920                next unless $skip;
921                $RT::Logger->info( "$msgid $skip" );
922                next NOSQUELCH_ADDRESS;
923            }
924            push @addrs, $addr;
925        }
926
927        @{ $self->{$type} } = @addrs;
928    }
929}
930
931=head2 RecipientFilter Callback => SUB, [All => 1]
932
933Registers a filter to be applied to addresses by
934L<RemoveInappropriateRecipients>.  The C<Callback> will be called with
935one address at a time, and should return false if the address should
936receive mail, or a message explaining why it should not be.  Passing a
937true value for C<All> will cause the filter to also be applied to
938NoSquelch (one-time Cc and Bcc) recipients as well.
939
940=cut
941
942sub RecipientFilter {
943    my $self = shift;
944    push @{ $self->{RecipientFilter}}, {@_};
945}
946
947=head2 SetReturnAddress is_comment => BOOLEAN
948
949Calculate and set From and Reply-To headers based on the is_comment flag.
950
951=cut
952
953sub SetReturnAddress {
954
955    my $self = shift;
956    my %args = (
957        is_comment => 0,
958        friendly_name => undef,
959        @_
960    );
961
962    # From and Reply-To
963    # $args{is_comment} should be set if the comment address is to be used.
964    my $replyto;
965
966    if ( $args{'is_comment'} ) {
967        $replyto = $self->TicketObj->QueueObj->CommentAddress
968            || RT->Config->Get('CommentAddress');
969    } else {
970        $replyto = $self->TicketObj->QueueObj->CorrespondAddress
971            || RT->Config->Get('CorrespondAddress');
972    }
973
974    unless ( $self->TemplateObj->MIMEObj->head->get('From') ) {
975        $self->SetFrom( %args, From => $replyto );
976    }
977
978    unless ( $self->TemplateObj->MIMEObj->head->get('Reply-To') ) {
979        $self->SetHeader( 'Reply-To', "$replyto" );
980    }
981
982}
983
984=head2 SetFrom ( From => emailaddress )
985
986Set the From: address for outgoing email
987
988=cut
989
990sub SetFrom {
991    my $self = shift;
992    my %args = @_;
993
994    if ( RT->Config->Get('UseFriendlyFromLine') ) {
995        my $friendly_name = $self->GetFriendlyName(%args);
996        $self->SetHeader(
997            'From',
998            sprintf(
999                RT->Config->Get('FriendlyFromLineFormat'),
1000                $self->MIMEEncodeString(
1001                    $friendly_name, RT->Config->Get('EmailOutputEncoding')
1002                ),
1003                $args{From}
1004            ),
1005        );
1006    } else {
1007        $self->SetHeader( 'From', $args{From} );
1008    }
1009}
1010
1011=head2 GetFriendlyName
1012
1013Calculate the proper Friendly Name based on the creator of the transaction
1014
1015=cut
1016
1017sub GetFriendlyName {
1018    my $self = shift;
1019    my %args = (
1020        is_comment => 0,
1021        friendly_name => '',
1022        @_
1023    );
1024    my $friendly_name = $args{friendly_name};
1025
1026    unless ( $friendly_name ) {
1027        $friendly_name = $self->TransactionObj->CreatorObj->FriendlyName;
1028        if ( $friendly_name =~ /^"(.*)"$/ ) {    # a quoted string
1029            $friendly_name = $1;
1030        }
1031    }
1032
1033    $friendly_name =~ s/"/\\"/g;
1034    return $friendly_name;
1035
1036}
1037
1038=head2 SetHeader FIELD, VALUE
1039
1040Set the FIELD of the current MIME object into VALUE, which should be in
1041characters, not bytes.  Returns the new header, in bytes.
1042
1043=cut
1044
1045sub SetHeader {
1046    my $self  = shift;
1047    my $field = shift;
1048    my $val   = shift;
1049
1050    chomp $val;
1051    chomp $field;
1052    my $head = $self->TemplateObj->MIMEObj->head;
1053    $head->fold_length( $field, 10000 );
1054    $head->replace( $field, Encode::encode( "UTF-8", $val ) );
1055    return $head->get($field);
1056}
1057
1058=head2 SetSubject
1059
1060This routine sets the subject. it does not add the rt tag. That gets done elsewhere
1061If subject is already defined via template, it uses that. otherwise, it tries to get
1062the transaction's subject.
1063
1064=cut 
1065
1066sub SetSubject {
1067    my $self = shift;
1068    my $subject;
1069
1070    if ( $self->TemplateObj->MIMEObj->head->get('Subject') ) {
1071        return ();
1072    }
1073
1074    # don't use Transaction->Attachments because it caches
1075    # and anything which later calls ->Attachments will be hurt
1076    # by our RowsPerPage() call.  caching is hard.
1077    my $message = RT::Attachments->new( $self->CurrentUser );
1078    $message->Limit( FIELD => 'TransactionId', VALUE => $self->TransactionObj->id);
1079    $message->OrderBy( FIELD => 'id', ORDER => 'ASC' );
1080    $message->RowsPerPage(1);
1081
1082    if ( $self->{'Subject'} ) {
1083        $subject = $self->{'Subject'};
1084    } elsif ( my $first = $message->First ) {
1085        my $tmp = $first->GetHeader('Subject');
1086        $subject = defined $tmp ? $tmp : $self->TicketObj->Subject;
1087    } else {
1088        $subject = $self->TicketObj->Subject;
1089    }
1090    $subject = '' unless defined $subject;
1091    chomp $subject;
1092
1093    $subject =~ s/(\r\n|\n|\s)/ /g;
1094
1095    $self->SetHeader( 'Subject', $subject );
1096
1097}
1098
1099=head2 SetSubjectToken
1100
1101This routine fixes the RT tag in the subject. It's unlikely that you want to overwrite this.
1102
1103=cut
1104
1105sub SetSubjectToken {
1106    my $self = shift;
1107
1108    my $head = $self->TemplateObj->MIMEObj->head;
1109    $self->SetHeader(
1110        Subject =>
1111            RT::Interface::Email::AddSubjectTag(
1112                Encode::decode( "UTF-8", $head->get('Subject') ),
1113                $self->TicketObj,
1114            ),
1115    );
1116}
1117
1118=head2 SetReferencesHeaders
1119
1120Set References and In-Reply-To headers for this message.
1121
1122=cut
1123
1124sub SetReferencesHeaders {
1125    my $self = shift;
1126
1127    my $top = $self->TransactionObj->Message->First;
1128    unless ( $top ) {
1129        $self->SetHeader( References => $self->PseudoReference );
1130        return (undef);
1131    }
1132
1133    my @in_reply_to = split( /\s+/m, $top->GetHeader('In-Reply-To') || '' );
1134    my @references  = split( /\s+/m, $top->GetHeader('References')  || '' );
1135    my @msgid       = split( /\s+/m, $top->GetHeader('Message-ID')  || '' );
1136
1137    # There are two main cases -- this transaction was created with
1138    # the RT Web UI, and hence we want to *not* append its Message-ID
1139    # to the References and In-Reply-To.  OR it came from an outside
1140    # source, and we should treat it as per the RFC
1141    my $org = RT->Config->Get('Organization');
1142    if ( "@msgid" =~ /<(rt-.*?-\d+-\d+)\.(\d+)-0-0\@\Q$org\E>/ ) {
1143
1144        # Make all references which are internal be to version which we
1145        # have sent out
1146
1147        for ( @references, @in_reply_to ) {
1148            s/<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>$/
1149          "<$1." . $self->TicketObj->id .
1150             "-" . $self->ScripObj->id .
1151             "-" . $self->ScripActionObj->{_Message_ID} .
1152             "@" . $org . ">"/eg
1153        }
1154
1155        # In reply to whatever the internal message was in reply to
1156        $self->SetHeader( 'In-Reply-To', join( " ", (@in_reply_to) ) );
1157
1158        # Default the references to whatever we're in reply to
1159        @references = @in_reply_to unless @references;
1160
1161        # References are unchanged from internal
1162    } else {
1163
1164        # In reply to that message
1165        $self->SetHeader( 'In-Reply-To', join( " ", (@msgid) ) );
1166
1167        # Default the references to whatever we're in reply to
1168        @references = @in_reply_to unless @references;
1169
1170        # Push that message onto the end of the references
1171        push @references, @msgid;
1172    }
1173
1174    # Push pseudo-ref to the front
1175    my $pseudo_ref = $self->PseudoReference;
1176    @references = ( $pseudo_ref, grep { $_ ne $pseudo_ref } @references );
1177
1178    # If there are more than 10 references headers, remove all but the
1179    # first four and the last six (Gotta keep this from growing
1180    # forever)
1181    splice( @references, 4, -6 ) if ( $#references >= 10 );
1182
1183    # Add on the references
1184    $self->SetHeader( 'References', join( " ", @references ) );
1185    $self->TemplateObj->MIMEObj->head->fold_length( 'References', 80 );
1186
1187}
1188
1189=head2 PseudoReference
1190
1191Returns a fake Message-ID: header for the ticket to allow a base level of threading
1192
1193=cut
1194
1195sub PseudoReference {
1196    my $self = shift;
1197    return RT::Interface::Email::PseudoReference( $self->TicketObj );
1198}
1199
1200=head2 SetHeaderAsEncoding($field_name, $charset_encoding)
1201
1202This routine converts the field into specified charset encoding, then
1203applies the MIME-Header transfer encoding.
1204
1205=cut
1206
1207sub SetHeaderAsEncoding {
1208    my $self = shift;
1209    my ( $field, $enc ) = ( shift, shift );
1210
1211    my $head = $self->TemplateObj->MIMEObj->head;
1212
1213    my $value = Encode::decode("UTF-8", $head->get( $field ));
1214    $value = $self->MIMEEncodeString( $value, $enc ); # Returns bytes
1215    $head->replace( $field, $value );
1216
1217}
1218
1219=head2 MIMEEncodeString
1220
1221Takes a perl string and optional encoding pass it over
1222L<RT::Interface::Email/EncodeToMIME>.
1223
1224Basicly encode a string using B encoding according to RFC2047, returning
1225bytes.
1226
1227=cut
1228
1229sub MIMEEncodeString {
1230    my $self  = shift;
1231    return RT::Interface::Email::EncodeToMIME( String => $_[0], Charset => $_[1] );
1232}
1233
1234RT::Base->_ImportOverlays();
1235
12361;
1237
1238