1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 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 RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, $RT::EmailOutputEncoding,
155 $self->SetHeader( 'Content-Type', 'text/plain; charset="' . $RT::EmailOutputEncoding . '"' );
157 # Build up a MIME::Entity that looks like the original message.
158 $self->AddAttachments() if ( $MIMEObj->head->get('RT-Attach-Message') );
172 Returns an array of Mail::Address objects containing all the To: recipients for this notification
178 return ($self->_AddressesFromHeader('To'));
183 Returns an array of Mail::Address objects containing all the Cc: recipients for this notification
189 return ($self->_AddressesFromHeader('Cc'));
194 Returns an array of Mail::Address objects containing all the Bcc: recipients for this notification
201 return ($self->_AddressesFromHeader('Bcc'));
205 sub _AddressesFromHeader {
208 my $header = $self->TemplateObj->MIMEObj->head->get($field);
209 my @addresses = Mail::Address->parse($header);
217 =head2 SendMessage MIMEObj
219 sends the message using RT's preferred API.
220 TODO: Break this out to a separate module
228 my $msgid = $MIMEObj->head->get('Message-Id');
231 $RT::Logger->info( $msgid . " #"
232 . $self->TicketObj->id . "/"
233 . $self->TransactionObj->id
235 . $self->ScripObj->id . " "
236 . $self->ScripObj->Description );
238 #If we don't have any recipients to send to, don't send a message;
239 unless ( $MIMEObj->head->get('To')
240 || $MIMEObj->head->get('Cc')
241 || $MIMEObj->head->get('Bcc') )
243 $RT::Logger->info( $msgid . " No recipients found. Not sending.\n" );
248 if ( $RT::MailCommand eq 'sendmailpipe' ) {
250 open( MAIL, "|$RT::SendmailPath $RT::SendmailArguments" ) || die $!;
251 print MAIL $MIMEObj->as_string;
255 $RT::Logger->crit( $msgid . "Could not send mail. -" . $@ );
259 my @mailer_args = ($RT::MailCommand);
261 local $ENV{MAILADDRESS};
263 if ( $RT::MailCommand eq 'sendmail' ) {
264 push @mailer_args, split(/\s+/, $RT::SendmailArguments);
266 elsif ( $RT::MailCommand eq 'smtp' ) {
267 $ENV{MAILADDRESS} = $RT::SMTPFrom || $MIMEObj->head->get('From');
268 push @mailer_args, ( Server => $RT::SMTPServer );
269 push @mailer_args, ( Debug => $RT::SMTPDebug );
272 push @mailer_args, $RT::MailParams;
275 unless ( $MIMEObj->send(@mailer_args) ) {
276 $RT::Logger->crit( $msgid . "Could not send mail." );
284 . $MIMEObj->head->get('To') . " Cc: "
285 . $MIMEObj->head->get('Cc') . " Bcc: "
286 . $MIMEObj->head->get('Bcc') );
287 $success =~ s/\n//gi;
289 $self->RecordOutgoingMailTransaction($MIMEObj) if ($RT::RecordOutgoingEmail);
291 $RT::Logger->info($success);
300 =head2 AddAttachments
302 Takes any attachments to this transaction and attaches them to the message
311 my $MIMEObj = $self->TemplateObj->MIMEObj;
313 $MIMEObj->head->delete('RT-Attach-Message');
315 my $attachments = RT::Attachments->new($RT::SystemUser);
317 FIELD => 'TransactionId',
318 VALUE => $self->TransactionObj->Id
320 $attachments->OrderBy('id');
322 my $transaction_content_obj = $self->TransactionObj->ContentObj;
324 # attach any of this transaction's attachments
325 while ( my $attach = $attachments->Next ) {
327 # Don't attach anything blank
328 next unless ( $attach->ContentLength );
330 # We want to make sure that we don't include the attachment that's being sued as the "Content" of this message"
332 if ( $transaction_content_obj
333 && $transaction_content_obj->Id == $attach->Id
334 && $transaction_content_obj->ContentType =~ qr{text/plain}i );
335 $MIMEObj->make_multipart('mixed');
337 Type => $attach->ContentType,
338 Charset => $attach->OriginalEncoding,
339 Data => $attach->OriginalContent,
340 Filename => $self->MIMEEncodeString( $attach->Filename,
341 $RT::EmailOutputEncoding ),
342 'RT-Attachment:' => $self->TicketObj->Id."/".$self->TransactionObj->Id."/".$attach->id,
343 Encoding => '-SUGGEST'
351 # {{{ RecordOutgoingMailTransaction
353 =head2 RecordOutgoingMailTransaction MIMEObj
355 Record a transaction in RT with this outgoing message for future record-keeping purposes
361 sub RecordOutgoingMailTransaction {
366 my @parts = $MIMEObj->parts;
369 foreach my $part (@parts) {
370 my $attach = $part->head->get('RT-Attachment');
372 $RT::Logger->debug("We found an attachment. we want to not record it.");
373 push @attachments, $attach;
375 $RT::Logger->debug("We found a part. we want to record it.");
379 $MIMEObj->parts(\@keep);
380 foreach my $attachment (@attachments) {
381 $MIMEObj->head->add('RT-Attachment', $attachment);
384 RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, 'utf-8', 'mime_words_ok' );
386 my $transaction = RT::Transaction->new($self->TransactionObj->CurrentUser);
388 # XXX: TODO -> Record attachments as references to things in the attachments table, maybe.
391 if ($self->TransactionObj->Type eq 'Comment') {
392 $type = 'CommentEmailRecord';
394 $type = 'EmailRecord';
399 my ( $id, $msg ) = $transaction->Create(
400 Ticket => $self->TicketObj->Id,
402 Data => $MIMEObj->head->get('Message-Id'),
413 # {{{ sub SetRTSpecialHeaders
415 =head2 SetRTSpecialHeaders
417 This routine adds all the random headers that RT wants in a mail message
418 that don't matter much to anybody else.
422 sub SetRTSpecialHeaders {
426 $self->SetSubjectToken();
427 $self->SetHeaderAsEncoding( 'Subject', $RT::EmailOutputEncoding )
428 if ($RT::EmailOutputEncoding);
429 $self->SetReturnAddress();
431 # TODO: this one is broken. What is this email really a reply to?
432 # If it's a reply to an incoming message, we'll need to use the
433 # actual message-id from the appropriate Attachment object. For
434 # incoming mails, we would like to preserve the In-Reply-To and/or
437 $self->SetHeader( 'In-Reply-To',
438 "<rt-" . $self->TicketObj->id() . "\@" . $RT::rtname . ">" );
440 # TODO We should always add References headers for all message-ids
441 # of previous messages related to this ticket.
443 $self->SetHeader( 'Message-ID',
446 . $self->TicketObj->id() . "-"
447 . $self->TransactionObj->id() . "-"
448 . $self->ScripObj->Id . "."
450 . $RT::Organization . ">" )
451 unless $self->TemplateObj->MIMEObj->head->get('Message-ID');
453 $self->SetHeader( 'Precedence', "bulk" )
454 unless ( $self->TemplateObj->MIMEObj->head->get("Precedence") );
456 $self->SetHeader( 'X-RT-Loop-Prevention', $RT::rtname );
457 $self->SetHeader( 'RT-Ticket',
458 $RT::rtname . " #" . $self->TicketObj->id() );
459 $self->SetHeader( 'Managed-by',
460 "RT $RT::VERSION (http://www.bestpractical.com/rt/)" );
462 $self->SetHeader( 'RT-Originator',
463 $self->TransactionObj->CreatorObj->EmailAddress );
472 # {{{ RemoveInappropriateRecipients
474 =head2 RemoveInappropriateRecipients
476 Remove addresses that are RT addresses or that are on this transaction's blacklist
480 sub RemoveInappropriateRecipients {
485 # Weed out any RT addresses. We really don't want to talk to ourselves!
487 RT::EmailParser::CullRTAddresses( "", @{ $self->{'To'} } );
489 RT::EmailParser::CullRTAddresses( "", @{ $self->{'Cc'} } );
490 @{ $self->{'Bcc'} } =
491 RT::EmailParser::CullRTAddresses( "", @{ $self->{'Bcc'} } );
493 # If there are no recipients, don't try to send the message.
494 # If the transaction has content and has the header RT-Squelch-Replies-To
496 if ( defined $self->TransactionObj->Attachments->First() ) {
498 $self->TransactionObj->Attachments->First->GetHeader(
499 'RT-Squelch-Replies-To');
502 @blacklist = split ( /,/, $squelch );
506 # Let's grab the SquelchMailTo attribue and push those entries into the @blacklist
507 my @non_recipients = $self->TicketObj->SquelchMailTo;
508 foreach my $attribute (@non_recipients) {
509 push @blacklist, $attribute->Content;
512 # Cycle through the people we're sending to and pull out anyone on the
515 foreach my $person_to_yank (@blacklist) {
516 $person_to_yank =~ s/\s//g;
517 @{ $self->{'To'} } = grep ( !/^$person_to_yank$/, @{ $self->{'To'} } );
518 @{ $self->{'Cc'} } = grep ( !/^$person_to_yank$/, @{ $self->{'Cc'} } );
519 @{ $self->{'Bcc'} } =
520 grep ( !/^$person_to_yank$/, @{ $self->{'Bcc'} } );
525 # {{{ sub SetReturnAddress
527 =head2 SetReturnAddress is_comment => BOOLEAN
529 Calculate and set From and Reply-To headers based on the is_comment flag.
533 sub SetReturnAddress {
542 # $args{is_comment} should be set if the comment address is to be used.
545 if ( $args{'is_comment'} ) {
546 $replyto = $self->TicketObj->QueueObj->CommentAddress
547 || $RT::CommentAddress;
550 $replyto = $self->TicketObj->QueueObj->CorrespondAddress
551 || $RT::CorrespondAddress;
554 unless ( $self->TemplateObj->MIMEObj->head->get('From') ) {
555 if ($RT::UseFriendlyFromLine) {
556 my $friendly_name = $self->TransactionObj->CreatorObj->RealName;
557 if ( $friendly_name =~ /^"(.*)"$/ ) { # a quoted string
561 $friendly_name =~ s/"/\\"/g;
565 $RT::FriendlyFromLineFormat,
566 $self->MIMEEncodeString( $friendly_name,
567 $RT::EmailOutputEncoding ),
573 $self->SetHeader( 'From', $replyto );
577 unless ( $self->TemplateObj->MIMEObj->head->get('Reply-To') ) {
578 $self->SetHeader( 'Reply-To', "$replyto" );
587 =head2 SetHeader FIELD, VALUE
589 Set the FIELD of the current MIME object into VALUE.
600 $self->TemplateObj->MIMEObj->head->fold_length( $field, 10000 );
601 $self->TemplateObj->MIMEObj->head->replace( $field, $val );
602 return $self->TemplateObj->MIMEObj->head->get($field);
612 This routine sets the subject. it does not add the rt tag. that gets done elsewhere
613 If $self->{'Subject'} is already defined, it uses that. otherwise, it tries to get
614 the transaction's subject.
622 my $message = $self->TransactionObj->Attachments;
623 if ( $self->TemplateObj->MIMEObj->head->get('Subject') ) {
626 if ( $self->{'Subject'} ) {
627 $subject = $self->{'Subject'};
629 elsif ( ( $message->First() ) && ( $message->First->Headers ) ) {
630 my $header = $message->First->Headers();
631 $header =~ s/\n\s+/ /g;
632 if ( $header =~ /^Subject: (.*?)$/m ) {
636 $subject = $self->TicketObj->Subject();
641 $subject = $self->TicketObj->Subject();
644 $subject =~ s/(\r\n|\n|\s)/ /gi;
647 $self->SetHeader( 'Subject', $subject );
653 # {{{ sub SetSubjectToken
655 =head2 SetSubjectToken
657 This routine fixes the RT tag in the subject. It's unlikely that you want to overwrite this.
661 sub SetSubjectToken {
663 my $tag = "[$RT::rtname #" . $self->TicketObj->id . "]";
664 my $sub = $self->TemplateObj->MIMEObj->head->get('Subject');
665 unless ( $sub =~ /\Q$tag\E/ ) {
666 $sub =~ s/(\r\n|\n|\s)/ /gi;
668 $self->TemplateObj->MIMEObj->head->replace( 'Subject', "$tag $sub" );
676 # {{{ SetHeadingAsEncoding
678 =head2 SetHeaderAsEncoding($field_name, $charset_encoding)
680 This routine converts the field into specified charset encoding.
684 sub SetHeaderAsEncoding {
686 my ( $field, $enc ) = ( shift, shift );
688 if ($field eq 'From' and $RT::SMTPFrom) {
689 $self->TemplateObj->MIMEObj->head->replace( $field, $RT::SMTPFrom );
693 my $value = $self->TemplateObj->MIMEObj->head->get($field);
695 # don't bother if it's us-ascii
697 # See RT::I18N, 'NOTES: Why Encode::_utf8_off before Encode::from_to'
699 $value = $self->MIMEEncodeString($value, $enc);
701 $self->TemplateObj->MIMEObj->head->replace( $field, $value );
707 # {{{ MIMEEncodeString
709 =head2 MIMEEncodeString STRING ENCODING
711 Takes a string and a possible encoding and returns the string wrapped in MIME goo.
715 sub MIMEEncodeString {
718 # using RFC2047 notation, sec 2.
719 # encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
722 # An 'encoded-word' may not be more than 75 characters long
724 # MIME encoding increases 4/3*(number of bytes), and always in multiples
725 # of 4. Thus we have to find the best available value of bytes available
728 # First we get the integer max which max*4/3 would fit on space.
729 # Then we find the greater multiple of 3 lower or equal than $max.
730 my $max = int(((75-length('=?'.$charset.'?'.$encoding.'?'.'?='))*3)/4);
731 $max = int($max/3)*3;
734 return ($value) unless $value =~ /[^\x20-\x7e]/;
737 Encode::_utf8_off($value);
738 my $res = Encode::from_to( $value, "utf-8", $charset );
741 # copy value and split in chuncks
743 my @chunks = unpack("a$max" x int(length($str)/$max
744 + ((length($str) % $max) ? 1:0)), $str);
745 # encode an join chuncks
747 map encode_mimeword( $_, $encoding, $charset ), @chunks ;
751 $RT::Logger->crit("Can't encode! Charset or encoding too big.\n");
757 eval "require RT::Action::SendEmail_Vendor";
758 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Vendor.pm});
759 eval "require RT::Action::SendEmail_Local";
760 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Local.pm});