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