1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
46 # Portions Copyright 2000 Tobias Brox <tobix@cpan.org>
48 package RT::Action::SendEmail;
49 require RT::Action::Generic;
53 @ISA = qw(RT::Action::Generic);
55 use MIME::Words qw(encode_mimeword);
62 RT::Action::SendEmail - An Action which users can use to send mail
63 or can subclassed for more specialized mail sending behavior.
64 RT::Action::AutoReply is a good example subclass.
68 require RT::Action::SendEmail;
69 @ISA = qw(RT::Action::SendEmail);
74 Basically, you create another module RT::Action::YourAction which ISA
75 RT::Action::SendEmail.
79 ok (require RT::Action::SendEmail);
86 Jesse Vincent <jesse@bestpractical.com> and Tobias Brox <tobix@cpan.org>
94 # {{{ Scrip methods (_Init, Commit, Prepare, IsApplicable)
102 return($self->SendMessage($self->TemplateObj->MIMEObj));
112 my ( $result, $message ) = $self->TemplateObj->Parse(
113 Argument => $self->Argument,
114 TicketObj => $self->TicketObj,
115 TransactionObj => $self->TransactionObj
121 my $MIMEObj = $self->TemplateObj->MIMEObj;
124 $self->SetRTSpecialHeaders();
126 $self->RemoveInappropriateRecipients();
128 # Go add all the Tos, Ccs and Bccs that we need to to the message to
129 # make it happy, but only if we actually have values in those arrays.
131 # TODO: We should be pulling the recipients out of the template and shove them into To, Cc and Bcc
133 $self->SetHeader( 'To', join ( ', ', @{ $self->{'To'} } ) )
134 if ( ! $MIMEObj->head->get('To') && $self->{'To'} && @{ $self->{'To'} } );
135 $self->SetHeader( 'Cc', join ( ', ', @{ $self->{'Cc'} } ) )
136 if ( !$MIMEObj->head->get('Cc') && $self->{'Cc'} && @{ $self->{'Cc'} } );
137 $self->SetHeader( 'Bcc', join ( ', ', @{ $self->{'Bcc'} } ) )
138 if ( !$MIMEObj->head->get('Bcc') && $self->{'Bcc'} && @{ $self->{'Bcc'} } );
140 # PseudoTo (fake to headers) shouldn't get matched for message recipients.
141 # If we don't have any 'To' header (but do have other recipients), drop in
142 # the pseudo-to header.
143 $self->SetHeader( 'To', join ( ', ', @{ $self->{'PseudoTo'} } ) )
144 if ( $self->{'PseudoTo'} && ( @{ $self->{'PseudoTo'} } )
145 and ( !$MIMEObj->head->get('To') ) ) and ( $MIMEObj->head->get('Cc') or $MIMEObj->head->get('Bcc'));
147 # We should never have to set the MIME-Version header
148 $self->SetHeader( 'MIME-Version', '1.0' );
150 # try to convert message body from utf-8 to $RT::EmailOutputEncoding
151 $self->SetHeader( 'Content-Type', 'text/plain; charset="utf-8"' );
153 # fsck.com #5959: Since RT sends 8bit mail, we should say so.
154 $self->SetHeader( 'Content-Transfer-Encoding','8bit');
157 RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, $RT::EmailOutputEncoding,
159 $self->SetHeader( 'Content-Type', 'text/plain; charset="' . $RT::EmailOutputEncoding . '"' );
161 # Build up a MIME::Entity that looks like the original message.
162 $self->AddAttachments() if ( $MIMEObj->head->get('RT-Attach-Message') );
176 Returns an array of Mail::Address objects containing all the To: recipients for this notification
182 return ($self->_AddressesFromHeader('To'));
187 Returns an array of Mail::Address objects containing all the Cc: recipients for this notification
193 return ($self->_AddressesFromHeader('Cc'));
198 Returns an array of Mail::Address objects containing all the Bcc: recipients for this notification
205 return ($self->_AddressesFromHeader('Bcc'));
209 sub _AddressesFromHeader {
212 my $header = $self->TemplateObj->MIMEObj->head->get($field);
213 my @addresses = Mail::Address->parse($header);
221 =head2 SendMessage MIMEObj
223 sends the message using RT's preferred API.
224 TODO: Break this out to a separate module
232 my $msgid = $MIMEObj->head->get('Message-ID');
235 $self->ScripActionObj->{_Message_ID}++;
237 $RT::Logger->info( $msgid . " #"
238 . $self->TicketObj->id . "/"
239 . $self->TransactionObj->id
241 . $self->ScripObj->id . " "
242 . $self->ScripObj->Description );
244 #If we don't have any recipients to send to, don't send a message;
245 unless ( $MIMEObj->head->get('To')
246 || $MIMEObj->head->get('Cc')
247 || $MIMEObj->head->get('Bcc') )
249 $RT::Logger->info( $msgid . " No recipients found. Not sending.\n" );
254 if ( $RT::MailCommand eq 'sendmailpipe' ) {
256 open( MAIL, "|$RT::SendmailPath $RT::SendmailArguments" ) || die $!;
257 print MAIL $MIMEObj->as_string;
261 $RT::Logger->crit( $msgid . "Could not send mail. -" . $@ );
265 my @mailer_args = ($RT::MailCommand);
267 local $ENV{MAILADDRESS};
269 if ( $RT::MailCommand eq 'sendmail' ) {
270 push @mailer_args, split(/\s+/, $RT::SendmailArguments);
272 elsif ( $RT::MailCommand eq 'smtp' ) {
273 $ENV{MAILADDRESS} = $RT::SMTPFrom || $MIMEObj->head->get('From');
274 push @mailer_args, ( Server => $RT::SMTPServer );
275 push @mailer_args, ( Debug => $RT::SMTPDebug );
278 push @mailer_args, $RT::MailParams;
281 unless ( $MIMEObj->send(@mailer_args) ) {
282 $RT::Logger->crit( $msgid . "Could not send mail." );
290 . $MIMEObj->head->get('To') . " Cc: "
291 . $MIMEObj->head->get('Cc') . " Bcc: "
292 . $MIMEObj->head->get('Bcc') );
293 $success =~ s/\n//gi;
295 $self->RecordOutgoingMailTransaction($MIMEObj) if ($RT::RecordOutgoingEmail);
297 $RT::Logger->info($success);
306 =head2 AddAttachments
308 Takes any attachments to this transaction and attaches them to the message
317 my $MIMEObj = $self->TemplateObj->MIMEObj;
319 $MIMEObj->head->delete('RT-Attach-Message');
321 my $attachments = RT::Attachments->new($RT::SystemUser);
323 FIELD => 'TransactionId',
324 VALUE => $self->TransactionObj->Id
326 $attachments->OrderBy('id');
328 my $transaction_content_obj = $self->TransactionObj->ContentObj;
330 # attach any of this transaction's attachments
331 while ( my $attach = $attachments->Next ) {
333 # Don't attach anything blank
334 next unless ( $attach->ContentLength );
336 # We want to make sure that we don't include the attachment that's being sued as the "Content" of this message"
338 if ( $transaction_content_obj
339 && $transaction_content_obj->Id == $attach->Id
340 && $transaction_content_obj->ContentType =~ qr{text/plain}i );
341 $MIMEObj->make_multipart('mixed');
343 Type => $attach->ContentType,
344 Charset => $attach->OriginalEncoding,
345 Data => $attach->OriginalContent,
346 Filename => $self->MIMEEncodeString( $attach->Filename,
347 $RT::EmailOutputEncoding ),
348 'RT-Attachment:' => $self->TicketObj->Id."/".$self->TransactionObj->Id."/".$attach->id,
349 Encoding => '-SUGGEST'
357 # {{{ RecordOutgoingMailTransaction
359 =head2 RecordOutgoingMailTransaction MIMEObj
361 Record a transaction in RT with this outgoing message for future record-keeping purposes
367 sub RecordOutgoingMailTransaction {
372 my @parts = $MIMEObj->parts;
375 foreach my $part (@parts) {
376 my $attach = $part->head->get('RT-Attachment');
378 $RT::Logger->debug("We found an attachment. we want to not record it.");
379 push @attachments, $attach;
381 $RT::Logger->debug("We found a part. we want to record it.");
385 $MIMEObj->parts(\@keep);
386 foreach my $attachment (@attachments) {
387 $MIMEObj->head->add('RT-Attachment', $attachment);
390 RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, 'utf-8', 'mime_words_ok' );
392 my $transaction = RT::Transaction->new($self->TransactionObj->CurrentUser);
394 # XXX: TODO -> Record attachments as references to things in the attachments table, maybe.
397 if ($self->TransactionObj->Type eq 'Comment') {
398 $type = 'CommentEmailRecord';
400 $type = 'EmailRecord';
403 my $msgid = $MIMEObj->head->get('Message-ID');
406 my ( $id, $msg ) = $transaction->Create(
407 Ticket => $self->TicketObj->Id,
420 # {{{ sub SetRTSpecialHeaders
422 =head2 SetRTSpecialHeaders
424 This routine adds all the random headers that RT wants in a mail message
425 that don't matter much to anybody else.
429 sub SetRTSpecialHeaders {
433 $self->SetSubjectToken();
434 $self->SetHeaderAsEncoding( 'Subject', $RT::EmailOutputEncoding )
435 if ($RT::EmailOutputEncoding);
436 $self->SetReturnAddress();
437 $self->SetReferencesHeaders();
439 unless ($self->TemplateObj->MIMEObj->head->get('Message-ID')) {
440 # Get Message-ID for this txn
442 $msgid = $self->TransactionObj->Message->First->GetHeader("RT-Message-ID")
443 || $self->TransactionObj->Message->First->GetHeader("Message-ID")
444 if $self->TransactionObj->Message && $self->TransactionObj->Message->First;
446 # If there is one, and we can parse it, then base our Message-ID on it
448 and $msgid =~ s/<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>$/
449 "<$1." . $self->TicketObj->id
450 . "-" . $self->ScripObj->id
451 . "-" . $self->ScripActionObj->{_Message_ID}
452 . "@" . $RT::Organization . ">"/eg
453 and $2 == $self->TicketObj->id) {
454 $self->SetHeader( "Message-ID" => $msgid );
456 $self->SetHeader( 'Message-ID',
461 . int(rand(2000)) . '.'
462 . $self->TicketObj->id . "-"
463 . $self->ScripObj->id . "-" # Scrip
464 . $self->ScripActionObj->{_Message_ID} . "@" # Email sent
470 $self->SetHeader( 'Precedence', "bulk" )
471 unless ( $self->TemplateObj->MIMEObj->head->get("Precedence") );
473 $self->SetHeader( 'X-RT-Loop-Prevention', $RT::rtname );
474 $self->SetHeader( 'RT-Ticket',
475 $RT::rtname . " #" . $self->TicketObj->id() );
476 $self->SetHeader( 'Managed-by',
477 "RT $RT::VERSION (http://www.bestpractical.com/rt/)" );
479 $self->SetHeader( 'RT-Originator',
480 $self->TransactionObj->CreatorObj->EmailAddress );
489 # {{{ RemoveInappropriateRecipients
491 =head2 RemoveInappropriateRecipients
493 Remove addresses that are RT addresses or that are on this transaction's blacklist
497 sub RemoveInappropriateRecipients {
502 my @types = qw/To Cc Bcc/;
504 # Weed out any RT addresses. We really don't want to talk to ourselves!
505 foreach my $type (@types) {
506 @{ $self->{$type} } =
507 RT::EmailParser::CullRTAddresses( "", @{ $self->{$type} } );
510 # If there are no recipients, don't try to send the message.
511 # If the transaction has content and has the header RT-Squelch-Replies-To
513 if ( $self->TransactionObj->Attachments->First() ) {
515 $self->TransactionObj->Attachments->First->GetHeader(
516 'RT-DetectedAutoGenerated')
520 # What do we want to do with this? It's probably (?) a bounce
521 # caused by one of the watcher addresses being broken.
522 # Default ("true") is to redistribute, for historical reasons.
524 if ( !$RT::RedistributeAutoGeneratedMessages ) {
526 # Don't send to any watchers.
527 @{ $self->{'To'} } = ();
528 @{ $self->{'Cc'} } = ();
529 @{ $self->{'Bcc'} } = ();
532 elsif ( $RT::RedistributeAutoGeneratedMessages eq 'privileged' ) {
534 # Only send to "privileged" watchers.
537 foreach my $type (@types) {
539 foreach my $addr ( @{ $self->{$type} } ) {
540 my $user = RT::User->new($RT::SystemUser);
541 $user->LoadByEmail($addr);
542 @{ $self->{$type} } =
543 grep ( !/^\Q$addr\E$/, @{ $self->{$type} } )
544 if ( !$user->Privileged );
554 $self->TransactionObj->Attachments->First->GetHeader(
555 'RT-Squelch-Replies-To');
558 @blacklist = split( /,/, $squelch );
562 # Let's grab the SquelchMailTo attribue and push those entries into the @blacklist
563 my @non_recipients = $self->TicketObj->SquelchMailTo;
564 foreach my $attribute (@non_recipients) {
565 push @blacklist, $attribute->Content;
568 # Cycle through the people we're sending to and pull out anyone on the
571 foreach my $person_to_yank (@blacklist) {
572 $person_to_yank =~ s/\s//g;
573 foreach my $type (@types) {
574 @{ $self->{$type} } =
575 grep ( !/^\Q$person_to_yank\E$/, @{ $self->{$type} } );
581 # {{{ sub SetReturnAddress
583 =head2 SetReturnAddress is_comment => BOOLEAN
585 Calculate and set From and Reply-To headers based on the is_comment flag.
589 sub SetReturnAddress {
598 # $args{is_comment} should be set if the comment address is to be used.
601 if ( $args{'is_comment'} ) {
602 $replyto = $self->TicketObj->QueueObj->CommentAddress
603 || $RT::CommentAddress;
606 $replyto = $self->TicketObj->QueueObj->CorrespondAddress
607 || $RT::CorrespondAddress;
610 unless ( $self->TemplateObj->MIMEObj->head->get('From') ) {
611 if ($RT::UseFriendlyFromLine) {
612 my $friendly_name = $self->TransactionObj->CreatorObj->RealName;
613 if ( $friendly_name =~ /^"(.*)"$/ ) { # a quoted string
617 $friendly_name =~ s/"/\\"/g;
621 $RT::FriendlyFromLineFormat,
622 $self->MIMEEncodeString( $friendly_name,
623 $RT::EmailOutputEncoding ),
629 $self->SetHeader( 'From', $replyto );
633 unless ( $self->TemplateObj->MIMEObj->head->get('Reply-To') ) {
634 $self->SetHeader( 'Reply-To', "$replyto" );
643 =head2 SetHeader FIELD, VALUE
645 Set the FIELD of the current MIME object into VALUE.
656 $self->TemplateObj->MIMEObj->head->fold_length( $field, 10000 );
657 $self->TemplateObj->MIMEObj->head->replace( $field, $val );
658 return $self->TemplateObj->MIMEObj->head->get($field);
668 This routine sets the subject. it does not add the rt tag. that gets done elsewhere
669 If $self->{'Subject'} is already defined, it uses that. otherwise, it tries to get
670 the transaction's subject.
678 my $message = $self->TransactionObj->Attachments;
679 if ( $self->TemplateObj->MIMEObj->head->get('Subject') ) {
682 if ( $self->{'Subject'} ) {
683 $subject = $self->{'Subject'};
685 elsif ( ( $message->First() ) && ( $message->First->Headers ) ) {
686 my $header = $message->First->Headers();
687 $header =~ s/\n\s+/ /g;
688 if ( $header =~ /^Subject: (.*?)$/m ) {
692 $subject = $self->TicketObj->Subject();
697 $subject = $self->TicketObj->Subject();
700 $subject =~ s/(\r\n|\n|\s)/ /gi;
703 $self->SetHeader( 'Subject', $subject );
709 # {{{ sub SetSubjectToken
711 =head2 SetSubjectToken
713 This routine fixes the RT tag in the subject. It's unlikely that you want to overwrite this.
717 sub SetSubjectToken {
719 my $tag = "[$RT::rtname #" . $self->TicketObj->id . "]";
720 my $sub = $self->TemplateObj->MIMEObj->head->get('Subject');
721 unless ( $sub =~ /\Q$tag\E/ ) {
722 $sub =~ s/(\r\n|\n|\s)/ /gi;
724 $self->TemplateObj->MIMEObj->head->replace( 'Subject', "$tag $sub" );
730 =head2 SetReferencesHeaders
732 Set References and In-Reply-To headers for this message.
736 sub SetReferencesHeaders {
739 my ( @in_reply_to, @references, @msgid );
741 my $attachments = $self->TransactionObj->Message;
743 if ( my $top = $attachments->First() ) {
744 @in_reply_to = split(/\s+/m, $top->GetHeader('In-Reply-To') || '');
745 @references = split(/\s+/m, $top->GetHeader('References') || '' );
746 @msgid = split(/\s+/m, $top->GetHeader('Message-ID') || '');
752 # There are two main cases -- this transaction was created with
753 # the RT Web UI, and hence we want to *not* append its Message-ID
754 # to the References and In-Reply-To. OR it came from an outside
755 # source, and we should treat it as per the RFC
756 if ( "@msgid" =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) {
758 # Make all references which are internal be to version which we
760 for (@references, @in_reply_to) {
761 s/<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>$/
762 "<$1." . $self->TicketObj->id .
763 "-" . $self->ScripObj->id .
764 "-" . $self->ScripActionObj->{_Message_ID} .
765 "@" . $RT::Organization . ">"/eg
768 # In reply to whatever the internal message was in reply to
769 $self->SetHeader( 'In-Reply-To', join( " ", ( @in_reply_to )));
771 # Default the references to whatever we're in reply to
772 @references = @in_reply_to unless @references;
774 # References are unchanged from internal
776 # In reply to that message
777 $self->SetHeader( 'In-Reply-To', join( " ", ( @msgid )));
779 # Default the references to whatever we're in reply to
780 @references = @in_reply_to unless @references;
782 # Push that message onto the end of the references
783 push @references, @msgid;
786 # Push pseudo-ref to the front
787 my $pseudo_ref = $self->PseudoReference;
788 @references = ($pseudo_ref, grep { $_ ne $pseudo_ref } @references);
790 # If there are more than 10 references headers, remove all but the
791 # first four and the last six (Gotta keep this from growing
793 splice(@references, 4, -6) if ($#references >= 10);
795 # Add on the references
796 $self->SetHeader( 'References', join( " ", @references) );
797 $self->TemplateObj->MIMEObj->head->fold_length( 'References', 80 );
803 =head2 PseudoReference
805 Returns a fake Message-ID: header for the ticket to allow a base level of threading
809 sub PseudoReference {
812 my $pseudo_ref = '<RT-Ticket-'.$self->TicketObj->id .'@'.$RT::Organization .'>';
817 # {{{ SetHeadingAsEncoding
819 =head2 SetHeaderAsEncoding($field_name, $charset_encoding)
821 This routine converts the field into specified charset encoding.
825 sub SetHeaderAsEncoding {
827 my ( $field, $enc ) = ( shift, shift );
829 if ($field eq 'From' and $RT::SMTPFrom) {
830 $self->TemplateObj->MIMEObj->head->replace( $field, $RT::SMTPFrom );
834 my $value = $self->TemplateObj->MIMEObj->head->get($field);
836 # don't bother if it's us-ascii
838 # See RT::I18N, 'NOTES: Why Encode::_utf8_off before Encode::from_to'
840 $value = $self->MIMEEncodeString($value, $enc);
842 $self->TemplateObj->MIMEObj->head->replace( $field, $value );
848 # {{{ MIMEEncodeString
850 =head2 MIMEEncodeString STRING ENCODING
852 Takes a string and a possible encoding and returns the string wrapped in MIME goo.
856 sub MIMEEncodeString {
859 # using RFC2047 notation, sec 2.
860 # encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
863 # An 'encoded-word' may not be more than 75 characters long
865 # MIME encoding increases 4/3*(number of bytes), and always in multiples
866 # of 4. Thus we have to find the best available value of bytes available
869 # First we get the integer max which max*4/3 would fit on space.
870 # Then we find the greater multiple of 3 lower or equal than $max.
871 my $max = int(((75-length('=?'.$charset.'?'.$encoding.'?'.'?='))*3)/4);
872 $max = int($max/3)*3;
875 return ($value) unless $value =~ /[^\x20-\x7e]/;
878 Encode::_utf8_off($value);
879 my $res = Encode::from_to( $value, "utf-8", $charset );
882 # copy value and split in chuncks
884 my @chunks = unpack("a$max" x int(length($str)/$max
885 + ((length($str) % $max) ? 1:0)), $str);
886 # encode an join chuncks
888 map encode_mimeword( $_, $encoding, $charset ), @chunks ;
892 $RT::Logger->crit("Can't encode! Charset or encoding too big.\n");
898 eval "require RT::Action::SendEmail_Vendor";
899 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Vendor.pm});
900 eval "require RT::Action::SendEmail_Local";
901 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Local.pm});