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 # don't ignore CHLD signal to get proper exit code
257 local $SIG{'CHLD'} = 'DEFAULT';
260 unless( open $mail, "|$RT::SendmailPath $RT::SendmailArguments" ) {
261 die "Couldn't run $RT::SendmailPath: $!";
264 # if something wrong with $mail->print we will get PIPE signal, handle it
265 local $SIG{'PIPE'} = sub { die "$RT::SendmailPath closed pipe" };
266 $MIMEObj->print($mail);
268 unless ( close $mail ) {
269 die "Close failed: $!" if $!; # system error
270 # sendmail exit statuses mostly errors with data not software
271 # TODO: status parsing: core dump, exit on signal or EX_*
272 $RT::Logger->warning( "$RT::SendmailPath exitted with status $?" );
276 $RT::Logger->crit( $msgid . "Could not send mail: " . $@ );
281 my @mailer_args = ($RT::MailCommand);
283 local $ENV{MAILADDRESS};
285 if ( $RT::MailCommand eq 'sendmail' ) {
286 push @mailer_args, split(/\s+/, $RT::SendmailArguments);
288 elsif ( $RT::MailCommand eq 'smtp' ) {
289 $ENV{MAILADDRESS} = $RT::SMTPFrom || $MIMEObj->head->get('From');
290 push @mailer_args, ( Server => $RT::SMTPServer );
291 push @mailer_args, ( Debug => $RT::SMTPDebug );
294 push @mailer_args, $RT::MailParams;
297 unless ( $MIMEObj->send(@mailer_args) ) {
298 $RT::Logger->crit( $msgid . "Could not send mail." );
303 my $success = "$msgid sent";
304 foreach (qw(To Cc Bcc)) {
305 next unless my $addresses = $MIMEObj->head->get($_);
306 $success .= " $_: ". $addresses;
310 $self->RecordOutgoingMailTransaction($MIMEObj) if ($RT::RecordOutgoingEmail);
312 $RT::Logger->info($success);
321 =head2 AddAttachments
323 Takes any attachments to this transaction and attaches them to the message
332 my $MIMEObj = $self->TemplateObj->MIMEObj;
334 $MIMEObj->head->delete('RT-Attach-Message');
336 my $attachments = RT::Attachments->new($RT::SystemUser);
338 FIELD => 'TransactionId',
339 VALUE => $self->TransactionObj->Id
341 $attachments->OrderBy( FIELD => 'id');
343 my $transaction_content_obj = $self->TransactionObj->ContentObj;
345 # attach any of this transaction's attachments
346 while ( my $attach = $attachments->Next ) {
348 # Don't attach anything blank
349 next unless ( $attach->ContentLength );
351 # We want to make sure that we don't include the attachment that's being sued as the "Content" of this message"
353 if ( $transaction_content_obj
354 && $transaction_content_obj->Id == $attach->Id
355 && $transaction_content_obj->ContentType =~ qr{text/plain}i );
356 $MIMEObj->make_multipart('mixed');
358 Type => $attach->ContentType,
359 Charset => $attach->OriginalEncoding,
360 Data => $attach->OriginalContent,
361 Filename => $self->MIMEEncodeString( $attach->Filename,
362 $RT::EmailOutputEncoding ),
363 'RT-Attachment:' => $self->TicketObj->Id."/".$self->TransactionObj->Id."/".$attach->id,
364 Encoding => '-SUGGEST'
372 # {{{ RecordOutgoingMailTransaction
374 =head2 RecordOutgoingMailTransaction MIMEObj
376 Record a transaction in RT with this outgoing message for future record-keeping purposes
382 sub RecordOutgoingMailTransaction {
387 my @parts = $MIMEObj->parts;
390 foreach my $part (@parts) {
391 my $attach = $part->head->get('RT-Attachment');
393 $RT::Logger->debug("We found an attachment. we want to not record it.");
394 push @attachments, $attach;
396 $RT::Logger->debug("We found a part. we want to record it.");
400 $MIMEObj->parts(\@keep);
401 foreach my $attachment (@attachments) {
402 $MIMEObj->head->add('RT-Attachment', $attachment);
405 RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, 'utf-8', 'mime_words_ok' );
407 my $transaction = RT::Transaction->new($self->TransactionObj->CurrentUser);
409 # XXX: TODO -> Record attachments as references to things in the attachments table, maybe.
412 if ($self->TransactionObj->Type eq 'Comment') {
413 $type = 'CommentEmailRecord';
415 $type = 'EmailRecord';
418 my $msgid = $MIMEObj->head->get('Message-ID');
421 my ( $id, $msg ) = $transaction->Create(
422 Ticket => $self->TicketObj->Id,
435 # {{{ sub SetRTSpecialHeaders
437 =head2 SetRTSpecialHeaders
439 This routine adds all the random headers that RT wants in a mail message
440 that don't matter much to anybody else.
444 sub SetRTSpecialHeaders {
448 $self->SetSubjectToken();
449 $self->SetHeaderAsEncoding( 'Subject', $RT::EmailOutputEncoding )
450 if ($RT::EmailOutputEncoding);
451 $self->SetReturnAddress();
452 $self->SetReferencesHeaders();
454 unless ($self->TemplateObj->MIMEObj->head->get('Message-ID')) {
455 # Get Message-ID for this txn
457 $msgid = $self->TransactionObj->Message->First->GetHeader("RT-Message-ID")
458 || $self->TransactionObj->Message->First->GetHeader("Message-ID")
459 if $self->TransactionObj->Message && $self->TransactionObj->Message->First;
461 # If there is one, and we can parse it, then base our Message-ID on it
463 and $msgid =~ s/<(rt-.*?-\d+-\d+)\.(\d+)-\d+-\d+\@\Q$RT::Organization\E>$/
464 "<$1." . $self->TicketObj->id
465 . "-" . $self->ScripObj->id
466 . "-" . $self->ScripActionObj->{_Message_ID}
467 . "@" . $RT::Organization . ">"/eg
468 and $2 == $self->TicketObj->id) {
469 $self->SetHeader( "Message-ID" => $msgid );
471 $self->SetHeader( 'Message-ID',
476 . int(rand(2000)) . '.'
477 . $self->TicketObj->id . "-"
478 . $self->ScripObj->id . "-" # Scrip
479 . $self->ScripActionObj->{_Message_ID} . "@" # Email sent
485 $self->SetHeader( 'Precedence', "bulk" )
486 unless ( $self->TemplateObj->MIMEObj->head->get("Precedence") );
488 $self->SetHeader( 'X-RT-Loop-Prevention', $RT::rtname );
489 $self->SetHeader( 'RT-Ticket',
490 $RT::rtname . " #" . $self->TicketObj->id() );
491 $self->SetHeader( 'Managed-by',
492 "RT $RT::VERSION (http://www.bestpractical.com/rt/)" );
494 $self->SetHeader( 'RT-Originator',
495 $self->TransactionObj->CreatorObj->EmailAddress );
504 # {{{ RemoveInappropriateRecipients
506 =head2 RemoveInappropriateRecipients
508 Remove addresses that are RT addresses or that are on this transaction's blacklist
512 sub RemoveInappropriateRecipients {
517 my @types = qw/To Cc Bcc/;
519 # Weed out any RT addresses. We really don't want to talk to ourselves!
520 foreach my $type (@types) {
521 @{ $self->{$type} } =
522 RT::EmailParser::CullRTAddresses( "", @{ $self->{$type} } );
525 # If there are no recipients, don't try to send the message.
526 # If the transaction has content and has the header RT-Squelch-Replies-To
528 if ( $self->TransactionObj->Attachments->First() ) {
530 $self->TransactionObj->Attachments->First->GetHeader(
531 'RT-DetectedAutoGenerated')
535 # What do we want to do with this? It's probably (?) a bounce
536 # caused by one of the watcher addresses being broken.
537 # Default ("true") is to redistribute, for historical reasons.
539 if ( !$RT::RedistributeAutoGeneratedMessages ) {
541 # Don't send to any watchers.
542 @{ $self->{'To'} } = ();
543 @{ $self->{'Cc'} } = ();
544 @{ $self->{'Bcc'} } = ();
547 elsif ( $RT::RedistributeAutoGeneratedMessages eq 'privileged' ) {
549 # Only send to "privileged" watchers.
552 foreach my $type (@types) {
554 foreach my $addr ( @{ $self->{$type} } ) {
555 my $user = RT::User->new($RT::SystemUser);
556 $user->LoadByEmail($addr);
557 @{ $self->{$type} } =
558 grep ( !/^\Q$addr\E$/, @{ $self->{$type} } )
559 if ( !$user->Privileged );
569 $self->TransactionObj->Attachments->First->GetHeader(
570 'RT-Squelch-Replies-To');
573 @blacklist = split( /,/, $squelch );
577 # Let's grab the SquelchMailTo attribue and push those entries into the @blacklist
578 my @non_recipients = $self->TicketObj->SquelchMailTo;
579 foreach my $attribute (@non_recipients) {
580 push @blacklist, $attribute->Content;
583 # Cycle through the people we're sending to and pull out anyone on the
586 foreach my $person_to_yank (@blacklist) {
587 $person_to_yank =~ s/\s//g;
588 foreach my $type (@types) {
589 @{ $self->{$type} } =
590 grep ( !/^\Q$person_to_yank\E$/, @{ $self->{$type} } );
596 # {{{ sub SetReturnAddress
598 =head2 SetReturnAddress is_comment => BOOLEAN
600 Calculate and set From and Reply-To headers based on the is_comment flag.
604 sub SetReturnAddress {
613 # $args{is_comment} should be set if the comment address is to be used.
616 if ( $args{'is_comment'} ) {
617 $replyto = $self->TicketObj->QueueObj->CommentAddress
618 || $RT::CommentAddress;
621 $replyto = $self->TicketObj->QueueObj->CorrespondAddress
622 || $RT::CorrespondAddress;
625 unless ( $self->TemplateObj->MIMEObj->head->get('From') ) {
626 if ($RT::UseFriendlyFromLine) {
627 my $friendly_name = $self->TransactionObj->CreatorObj->RealName;
628 if ( $friendly_name =~ /^"(.*)"$/ ) { # a quoted string
632 $friendly_name =~ s/"/\\"/g;
636 $RT::FriendlyFromLineFormat,
637 $self->MIMEEncodeString( $friendly_name,
638 $RT::EmailOutputEncoding ),
644 $self->SetHeader( 'From', $replyto );
648 unless ( $self->TemplateObj->MIMEObj->head->get('Reply-To') ) {
649 $self->SetHeader( 'Reply-To', "$replyto" );
658 =head2 SetHeader FIELD, VALUE
660 Set the FIELD of the current MIME object into VALUE.
671 $self->TemplateObj->MIMEObj->head->fold_length( $field, 10000 );
672 $self->TemplateObj->MIMEObj->head->replace( $field, $val );
673 return $self->TemplateObj->MIMEObj->head->get($field);
683 This routine sets the subject. it does not add the rt tag. that gets done elsewhere
684 If $self->{'Subject'} is already defined, it uses that. otherwise, it tries to get
685 the transaction's subject.
693 my $message = $self->TransactionObj->Attachments;
694 if ( $self->TemplateObj->MIMEObj->head->get('Subject') ) {
697 if ( $self->{'Subject'} ) {
698 $subject = $self->{'Subject'};
700 elsif ( ( $message->First() ) && ( $message->First->Headers ) ) {
701 my $header = $message->First->Headers();
702 $header =~ s/\n\s+/ /g;
703 if ( $header =~ /^Subject: (.*?)$/m ) {
707 $subject = $self->TicketObj->Subject();
712 $subject = $self->TicketObj->Subject();
715 $subject =~ s/(\r\n|\n|\s)/ /gi;
718 $self->SetHeader( 'Subject', $subject );
724 # {{{ sub SetSubjectToken
726 =head2 SetSubjectToken
728 This routine fixes the RT tag in the subject. It's unlikely that you want to overwrite this.
732 sub SetSubjectToken {
734 my $tag = "[$RT::rtname #" . $self->TicketObj->id . "]";
735 my $sub = $self->TemplateObj->MIMEObj->head->get('Subject');
736 unless ( $sub =~ /\Q$tag\E/ ) {
737 $sub =~ s/(\r\n|\n|\s)/ /gi;
739 $self->TemplateObj->MIMEObj->head->replace( 'Subject', "$tag $sub" );
745 =head2 SetReferencesHeaders
747 Set References and In-Reply-To headers for this message.
751 sub SetReferencesHeaders {
754 my ( @in_reply_to, @references, @msgid );
756 my $attachments = $self->TransactionObj->Message;
758 if ( my $top = $attachments->First() ) {
759 @in_reply_to = split(/\s+/m, $top->GetHeader('In-Reply-To') || '');
760 @references = split(/\s+/m, $top->GetHeader('References') || '' );
761 @msgid = split(/\s+/m, $top->GetHeader('Message-ID') || '');
767 # There are two main cases -- this transaction was created with
768 # the RT Web UI, and hence we want to *not* append its Message-ID
769 # to the References and In-Reply-To. OR it came from an outside
770 # source, and we should treat it as per the RFC
771 if ( "@msgid" =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) {
773 # Make all references which are internal be to version which we
775 for (@references, @in_reply_to) {
776 s/<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>$/
777 "<$1." . $self->TicketObj->id .
778 "-" . $self->ScripObj->id .
779 "-" . $self->ScripActionObj->{_Message_ID} .
780 "@" . $RT::Organization . ">"/eg
783 # In reply to whatever the internal message was in reply to
784 $self->SetHeader( 'In-Reply-To', join( " ", ( @in_reply_to )));
786 # Default the references to whatever we're in reply to
787 @references = @in_reply_to unless @references;
789 # References are unchanged from internal
791 # In reply to that message
792 $self->SetHeader( 'In-Reply-To', join( " ", ( @msgid )));
794 # Default the references to whatever we're in reply to
795 @references = @in_reply_to unless @references;
797 # Push that message onto the end of the references
798 push @references, @msgid;
801 # Push pseudo-ref to the front
802 my $pseudo_ref = $self->PseudoReference;
803 @references = ($pseudo_ref, grep { $_ ne $pseudo_ref } @references);
805 # If there are more than 10 references headers, remove all but the
806 # first four and the last six (Gotta keep this from growing
808 splice(@references, 4, -6) if ($#references >= 10);
810 # Add on the references
811 $self->SetHeader( 'References', join( " ", @references) );
812 $self->TemplateObj->MIMEObj->head->fold_length( 'References', 80 );
818 =head2 PseudoReference
820 Returns a fake Message-ID: header for the ticket to allow a base level of threading
824 sub PseudoReference {
827 my $pseudo_ref = '<RT-Ticket-'.$self->TicketObj->id .'@'.$RT::Organization .'>';
832 # {{{ SetHeadingAsEncoding
834 =head2 SetHeaderAsEncoding($field_name, $charset_encoding)
836 This routine converts the field into specified charset encoding.
840 sub SetHeaderAsEncoding {
842 my ( $field, $enc ) = ( shift, shift );
844 if ($field eq 'From' and $RT::SMTPFrom) {
845 $self->TemplateObj->MIMEObj->head->replace( $field, $RT::SMTPFrom );
849 my $value = $self->TemplateObj->MIMEObj->head->get($field);
851 # don't bother if it's us-ascii
853 # See RT::I18N, 'NOTES: Why Encode::_utf8_off before Encode::from_to'
855 $value = $self->MIMEEncodeString($value, $enc);
857 $self->TemplateObj->MIMEObj->head->replace( $field, $value );
863 # {{{ MIMEEncodeString
865 =head2 MIMEEncodeString STRING ENCODING
867 Takes a string and a possible encoding and returns the string wrapped in MIME goo.
871 sub MIMEEncodeString {
874 # using RFC2047 notation, sec 2.
875 # encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
878 # An 'encoded-word' may not be more than 75 characters long
880 # MIME encoding increases 4/3*(number of bytes), and always in multiples
881 # of 4. Thus we have to find the best available value of bytes available
884 # First we get the integer max which max*4/3 would fit on space.
885 # Then we find the greater multiple of 3 lower or equal than $max.
886 my $max = int(((75-length('=?'.$charset.'?'.$encoding.'?'.'?='))*3)/4);
887 $max = int($max/3)*3;
890 return ($value) unless $value =~ /[^\x20-\x7e]/;
893 Encode::_utf8_off($value);
894 my $res = Encode::from_to( $value, "utf-8", $charset );
897 # copy value and split in chuncks
899 my @chunks = unpack("a$max" x int(length($str)/$max
900 + ((length($str) % $max) ? 1:0)), $str);
901 # encode an join chuncks
903 map encode_mimeword( $_, $encoding, $charset ), @chunks ;
907 $RT::Logger->crit("Can't encode! Charset or encoding too big.\n");
913 eval "require RT::Action::SendEmail_Vendor";
914 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Vendor.pm});
915 eval "require RT::Action::SendEmail_Local";
916 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Local.pm});