rt 4.2.15
[freeside.git] / rt / lib / RT / Action / SendEmail.pm
index 3492822..8897cb9 100755 (executable)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -80,13 +80,12 @@ RT::Action::SendEmail.
 
 =head2 CleanSlate
 
-Cleans class-wide options, like L</SquelchMailTo> or L</AttachTickets>.
+Cleans class-wide options, like L</AttachTickets>.
 
 =cut
 
 sub CleanSlate {
     my $self = shift;
-    $self->SquelchMailTo(undef);
     $self->AttachTickets(undef);
 }
 
@@ -100,47 +99,31 @@ activated in the config.
 sub Commit {
     my $self = shift;
 
-    $self->DeferDigestRecipients() if RT->Config->Get('RecordOutgoingEmail');
+    return abs $self->SendMessage( $self->TemplateObj->MIMEObj )
+        unless RT->Config->Get('RecordOutgoingEmail');
+
+    $self->DeferDigestRecipients();
     my $message = $self->TemplateObj->MIMEObj;
 
     my $orig_message;
-    if (   RT->Config->Get('RecordOutgoingEmail')
-        && RT->Config->Get('GnuPG')->{'Enable'} )
-    {
-
-        # it's hacky, but we should know if we're going to crypt things
-        my $attachment = $self->TransactionObj->Attachments->First;
-
-        my %crypt;
-        foreach my $argument (qw(Sign Encrypt)) {
-            if ( $attachment
-                && defined $attachment->GetHeader("X-RT-$argument") )
-            {
-                $crypt{$argument} = $attachment->GetHeader("X-RT-$argument");
-            } else {
-                $crypt{$argument} = $self->TicketObj->QueueObj->$argument();
-            }
-        }
-        if ( $crypt{'Sign'} || $crypt{'Encrypt'} ) {
-            $orig_message = $message->dup;
-        }
-    }
+    $orig_message = $message->dup if RT::Interface::Email::WillSignEncrypt(
+        Attachment => $self->TransactionObj->Attachments->First,
+        Ticket     => $self->TicketObj,
+    );
 
     my ($ret) = $self->SendMessage($message);
-    if ( $ret > 0 && RT->Config->Get('RecordOutgoingEmail') ) {
-        if ($orig_message) {
-            $message->attach(
-                Type        => 'application/x-rt-original-message',
-                Disposition => 'inline',
-                Data        => $orig_message->as_string,
-            );
-        }
-        $self->RecordOutgoingMailTransaction($message);
-        $self->RecordDeferredRecipients();
+    return abs( $ret ) if $ret <= 0;
+
+    if ($orig_message) {
+        $message->attach(
+            Type        => 'application/x-rt-original-message',
+            Disposition => 'inline',
+            Data        => $orig_message->as_string,
+        );
     }
-
-
-    return ( abs $ret );
+    $self->RecordOutgoingMailTransaction($message);
+    $self->RecordDeferredRecipients();
+    return 1;
 }
 
 =head2 Prepare
@@ -152,13 +135,15 @@ Builds an outgoing email we're going to send using scrip's template.
 sub Prepare {
     my $self = shift;
 
-    my ( $result, $message ) = $self->TemplateObj->Parse(
-        Argument       => $self->Argument,
-        TicketObj      => $self->TicketObj,
-        TransactionObj => $self->TransactionObj
-    );
-    if ( !$result ) {
-        return (undef);
+    unless ( $self->TemplateObj->MIMEObj ) {
+        my ( $result, $message ) = $self->TemplateObj->Parse(
+            Argument       => $self->Argument,
+            TicketObj      => $self->TicketObj,
+            TransactionObj => $self->TransactionObj
+        );
+        if ( !$result ) {
+            return (undef);
+        }
     }
 
     my $MIMEObj = $self->TemplateObj->MIMEObj;
@@ -166,8 +151,6 @@ sub Prepare {
     # Header
     $self->SetRTSpecialHeaders();
 
-    $self->RemoveInappropriateRecipients();
-
     my %seen;
     foreach my $type (@EMAIL_RECIPIENT_HEADERS) {
         @{ $self->{$type} }
@@ -175,6 +158,8 @@ sub Prepare {
             @{ $self->{$type} };
     }
 
+    $self->RemoveInappropriateRecipients();
+
     # Go add all the Tos, Ccs and Bccs that we need to to the message to
     # make it happy, but only if we actually have values in those arrays.
 
@@ -196,12 +181,6 @@ sub Prepare {
             && !$MIMEObj->head->get('To')
             && ( $MIMEObj->head->get('Cc') or $MIMEObj->head->get('Bcc') );
 
-    # We should never have to set the MIME-Version header
-    $self->SetHeader( 'MIME-Version', '1.0' );
-
-    # fsck.com #5959: Since RT sends 8bit mail, we should say so.
-    $self->SetHeader( 'Content-Transfer-Encoding', '8bit' );
-
     # For security reasons, we only send out textual mails.
     foreach my $part ( grep !$_->is_multipart, $MIMEObj->parts_DFS ) {
         my $type = $part->mime_type || 'text/plain';
@@ -212,9 +191,12 @@ sub Prepare {
         $part->head->mime_attr( "Content-Type.charset" => 'utf-8' );
     }
 
-    RT::I18N::SetMIMEEntityToEncoding( $MIMEObj,
-        RT->Config->Get('EmailOutputEncoding'),
-        'mime_words_ok', );
+    RT::I18N::SetMIMEEntityToEncoding(
+        Entity        => $MIMEObj,
+        Encoding      => RT->Config->Get('EmailOutputEncoding'),
+        PreserveWords => 1,
+        IsOut         => 1,
+    );
 
     # Build up a MIME::Entity that looks like the original message.
     $self->AddAttachments if ( $MIMEObj->head->get('RT-Attach-Message')
@@ -235,7 +217,7 @@ sub Prepare {
             'Success';
     }
 
-    return $result;
+    return 1;
 }
 
 =head2 To
@@ -275,7 +257,7 @@ sub Bcc {
 sub AddressesFromHeader {
     my $self      = shift;
     my $field     = shift;
-    my $header    = $self->TemplateObj->MIMEObj->head->get($field);
+    my $header    = Encode::decode("UTF-8",$self->TemplateObj->MIMEObj->head->get($field));
     my @addresses = Email::Address->parse($header);
 
     return (@addresses);
@@ -294,7 +276,7 @@ sub SendMessage {
     # ability to pass @_ to a 'post' routine.
     my ( $self, $MIMEObj ) = @_;
 
-    my $msgid = $MIMEObj->head->get('Message-ID');
+    my $msgid = Encode::decode( "UTF-8", $MIMEObj->head->get('Message-ID') );
     chomp $msgid;
 
     $self->ScripActionObj->{_Message_ID}++;
@@ -317,7 +299,7 @@ sub SendMessage {
 
     my $success = $msgid . " sent ";
     foreach (@EMAIL_RECIPIENT_HEADERS) {
-        my $recipients = $MIMEObj->head->get($_);
+        my $recipients = Encode::decode( "UTF-8", $MIMEObj->head->get($_) );
         $success .= " $_: " . $recipients if $recipients;
     }
 
@@ -335,24 +317,21 @@ sub SendMessage {
     return (1);
 }
 
-=head2 AddAttachments
+=head2 AttachableFromTransaction
 
-Takes any attachments to this transaction and attaches them to the message
-we're building.
+Function (not method) that takes an L<RT::Transaction> and returns an
+L<RT::Attachments> collection of attachments suitable for attaching to an
+email.
 
 =cut
 
-sub AddAttachments {
-    my $self = shift;
-
-    my $MIMEObj = $self->TemplateObj->MIMEObj;
-
-    $MIMEObj->head->delete('RT-Attach-Message');
+sub AttachableFromTransaction {
+    my $txn = shift;
 
-    my $attachments = RT::Attachments->new($RT::SystemUser);
+    my $attachments = RT::Attachments->new( RT->SystemUser );
     $attachments->Limit(
         FIELD => 'TransactionId',
-        VALUE => $self->TransactionObj->Id
+        VALUE => $txn->Id
     );
 
     # Don't attach anything blank
@@ -362,7 +341,7 @@ sub AddAttachments {
     # We want to make sure that we don't include the attachment that's
     # being used as the "Content" of this message" unless that attachment's
     # content type is not like text/...
-    my $transaction_content_obj = $self->TransactionObj->ContentObj;
+    my $transaction_content_obj = $txn->ContentObj;
 
     if (   $transaction_content_obj
         && $transaction_content_obj->ContentType =~ m{text/}i )
@@ -386,6 +365,25 @@ sub AddAttachments {
         }
     }
 
+    return $attachments;
+}
+
+=head2 AddAttachments
+
+Takes any attachments to this transaction and attaches them to the message
+we're building.
+
+=cut
+
+sub AddAttachments {
+    my $self = shift;
+
+    my $MIMEObj = $self->TemplateObj->MIMEObj;
+
+    $MIMEObj->head->delete('RT-Attach-Message');
+
+    my $attachments = AttachableFromTransaction($self->TransactionObj);
+
     # attach any of this transaction's attachments
     my $seen_attachment = 0;
     while ( my $attach = $attachments->Next ) {
@@ -399,7 +397,7 @@ sub AddAttachments {
 
 =head2 AddAttachment $attachment
 
-Takes one attachment object of L<RT::Attachmment> class and attaches it to the message
+Takes one attachment object of L<RT::Attachment> class and attaches it to the message
 we're building.
 
 =cut
@@ -409,11 +407,22 @@ sub AddAttachment {
     my $attach  = shift;
     my $MIMEObj = shift || $self->TemplateObj->MIMEObj;
 
+    # $attach->TransactionObj may not always be $self->TransactionObj
+    return unless $attach->Id
+              and $attach->TransactionObj->CurrentUserCanSee;
+
+    # ->attach expects just the disposition type; extract it if we have the header
+    # or default to "attachment"
+    my $disp = ($attach->GetHeader('Content-Disposition') || '')
+                    =~ /^\s*(inline|attachment)/i ? $1 : "attachment";
+
     $MIMEObj->attach(
-        Type     => $attach->ContentType,
-        Charset  => $attach->OriginalEncoding,
-        Data     => $attach->OriginalContent,
-        Filename => $self->MIMEEncodeString( $attach->Filename ),
+        Type        => $attach->ContentType,
+        Charset     => $attach->OriginalEncoding,
+        Data        => $attach->OriginalContent,
+        Disposition => $disp,
+        Filename    => $self->MIMEEncodeString( $attach->Filename ),
+        Id          => $attach->GetHeader('Content-ID'),
         'RT-Attachment:' => $self->TicketObj->Id . "/"
             . $self->TransactionObj->Id . "/"
             . $attach->id,
@@ -467,14 +476,13 @@ sub AddTicket {
     my $self = shift;
     my $tid  = shift;
 
-    # XXX: we need a current user here, but who is current user?
-    my $attachs   = RT::Attachments->new($RT::SystemUser);
+    my $attachs   = RT::Attachments->new( $self->TransactionObj->CreatorObj );
     my $txn_alias = $attachs->TransactionAlias;
-    $attachs->Limit( ALIAS => $txn_alias, FIELD => 'Type', VALUE => 'Create' );
     $attachs->Limit(
-        ALIAS => $txn_alias,
-        FIELD => 'Type',
-        VALUE => 'Correspond'
+        ALIAS    => $txn_alias,
+        FIELD    => 'Type',
+        OPERATOR => 'IN',
+        VALUE    => [qw(Create Correspond)],
     );
     $attachs->LimitByTicket($tid);
     $attachs->LimitNotEmpty;
@@ -539,7 +547,7 @@ sub RecordOutgoingMailTransaction {
         $type = 'EmailRecord';
     }
 
-    my $msgid = $MIMEObj->head->get('Message-ID');
+    my $msgid = Encode::decode( "UTF-8", $MIMEObj->head->get('Message-ID') );
     chomp $msgid;
 
     my ( $id, $msg ) = $transaction->Create(
@@ -609,24 +617,19 @@ sub SetRTSpecialHeaders {
         }
     }
 
-    if (my $precedence = RT->Config->Get('DefaultMailPrecedence')
-        and !$self->TemplateObj->MIMEObj->head->get("Precedence")
-    ) {
-        $self->SetHeader( 'Precedence', $precedence );
-    }
-
     $self->SetHeader( 'X-RT-Loop-Prevention', RT->Config->Get('rtname') );
-    $self->SetHeader( 'RT-Ticket',
+    $self->SetHeader( 'X-RT-Ticket',
         RT->Config->Get('rtname') . " #" . $self->TicketObj->id() );
-    $self->SetHeader( 'Managed-by',
+    $self->SetHeader( 'X-Managed-by',
         "RT $RT::VERSION (http://www.bestpractical.com/rt/)" );
 
 # XXX, TODO: use /ShowUser/ShowUserEntry(or something like that) when it would be
 #            refactored into user's method.
     if ( my $email = $self->TransactionObj->CreatorObj->EmailAddress
+         and ! defined $self->TemplateObj->MIMEObj->head->get("RT-Originator")
          and RT->Config->Get('UseOriginatorHeader')
     ) {
-        $self->SetHeader( 'RT-Originator', $email );
+        $self->SetHeader( 'X-RT-Originator', $email );
     }
 
 }
@@ -657,10 +660,10 @@ sub DeferDigestRecipients {
 
         # Have to get the list of addresses directly from the MIME header
         # at this point.
-        $RT::Logger->debug( $self->TemplateObj->MIMEObj->head->as_string );
+        $RT::Logger->debug( Encode::decode( "UTF-8", $self->TemplateObj->MIMEObj->head->as_string ) );
         foreach my $rcpt ( map { $_->address } $self->AddressesFromHeader($mailfield) ) {
             next unless $rcpt;
-            my $user_obj = RT::User->new($RT::SystemUser);
+            my $user_obj = RT::User->new(RT->SystemUser);
             $user_obj->LoadByEmail($rcpt);
             if  ( ! $user_obj->id ) {
                 # If there's an email address in here without an associated
@@ -729,26 +732,15 @@ sub RecordDeferredRecipients {
     return ($ret,$msg);
 }
 
-=head2 SquelchMailTo [@ADDRESSES]
+=head2 SquelchMailTo
 
-Mark ADDRESSES to be removed from list of the recipients. Returns list of the addresses.
-To empty list pass undefined argument.
-
-B<Note> that this method can be called as class method and works globaly. Don't forget to
-clean this list when blocking is not required anymore, pass undef to do this.
+Returns list of the addresses to squelch on this transaction.
 
 =cut
 
-{
-    my $squelch = [];
-
-    sub SquelchMailTo {
-        my $self = shift;
-        if (@_) {
-            $squelch = [ grep defined, @_ ];
-        }
-        return @$squelch;
-    }
+sub SquelchMailTo {
+    my $self = shift;
+    return map $_->Content, $self->TransactionObj->SquelchMailTo;
 }
 
 =head2 RemoveInappropriateRecipients
@@ -757,15 +749,29 @@ Remove addresses that are RT addresses or that are on this transaction's blackli
 
 =cut
 
+my %squelch_reasons = (
+    'not privileged'
+        => "because autogenerated messages are configured to only be sent to privileged users (RedistributeAutoGeneratedMessages)",
+    'squelch:attachment'
+        => "by RT-Squelch-Replies-To header in the incoming message",
+    'squelch:transaction'
+        => "by notification checkboxes for this transaction",
+    'squelch:ticket'
+        => "by notification checkboxes on this ticket's People page",
+);
+
+
 sub RemoveInappropriateRecipients {
     my $self = shift;
 
-    my @blacklist = ();
+    my %blacklist = ();
 
     # If there are no recipients, don't try to send the message.
     # If the transaction has content and has the header RT-Squelch-Replies-To
 
-    my $msgid = $self->TemplateObj->MIMEObj->head->get('Message-Id');
+    my $msgid = Encode::decode( "UTF-8", $self->TemplateObj->MIMEObj->head->get('Message-Id') );
+    chomp $msgid;
+
     if ( my $attachment = $self->TransactionObj->Attachments->First ) {
 
         if ( $attachment->GetHeader('RT-DetectedAutoGenerated') ) {
@@ -774,7 +780,9 @@ sub RemoveInappropriateRecipients {
             # caused by one of the watcher addresses being broken.
             # Default ("true") is to redistribute, for historical reasons.
 
-            if ( !RT->Config->Get('RedistributeAutoGeneratedMessages') ) {
+            my $redistribute = RT->Config->Get('RedistributeAutoGeneratedMessages');
+
+            if ( !$redistribute ) {
 
                 # Don't send to any watchers.
                 @{ $self->{$_} } = () for (@EMAIL_RECIPIENT_HEADERS);
@@ -782,16 +790,15 @@ sub RemoveInappropriateRecipients {
                         . " The incoming message was autogenerated. "
                         . "Not redistributing this message based on site configuration."
                 );
-            } elsif ( RT->Config->Get('RedistributeAutoGeneratedMessages') eq
-                'privileged' )
-            {
+            } elsif ( $redistribute eq 'privileged' ) {
 
                 # Only send to "privileged" watchers.
                 foreach my $type (@EMAIL_RECIPIENT_HEADERS) {
                     foreach my $addr ( @{ $self->{$type} } ) {
-                        my $user = RT::User->new($RT::SystemUser);
+                        my $user = RT::User->new(RT->SystemUser);
                         $user->LoadByEmail($addr);
-                        push @blacklist, $addr if ( !$user->Privileged );
+                        $blacklist{ $addr } ||= 'not privileged'
+                            unless $user->id && $user->Privileged;
                     }
                 }
                 $RT::Logger->info( $msgid
@@ -802,40 +809,88 @@ sub RemoveInappropriateRecipients {
         }
 
         if ( my $squelch = $attachment->GetHeader('RT-Squelch-Replies-To') ) {
-            push @blacklist, split( /,/, $squelch );
+            $blacklist{ $_->address } ||= 'squelch:attachment'
+                foreach Email::Address->parse( $squelch );
         }
     }
 
-# Let's grab the SquelchMailTo attribue and push those entries into the @blacklist
-    push @blacklist, map $_->Content, $self->TicketObj->SquelchMailTo;
-    push @blacklist, $self->SquelchMailTo;
+    # Let's grab the SquelchMailTo attributes and push those entries
+    # into the blacklisted
+    $blacklist{ $_->Content } ||= 'squelch:transaction'
+        foreach $self->TransactionObj->SquelchMailTo;
+    $blacklist{ $_->Content } ||= 'squelch:ticket'
+        foreach $self->TicketObj->SquelchMailTo;
+
+    # canonicalize emails
+    foreach my $address ( keys %blacklist ) {
+        my $reason = delete $blacklist{ $address };
+        $blacklist{ lc $_ } = $reason
+            foreach map RT::User->CanonicalizeEmailAddress( $_->address ),
+            Email::Address->parse( $address );
+    }
+
+    $self->RecipientFilter(
+        Callback => sub {
+            return unless RT::EmailParser->IsRTAddress( $_[0] );
+            return "$_[0] appears to point to this RT instance. Skipping";
+        },
+        All => 1,
+    );
 
-    # Cycle through the people we're sending to and pull out anyone on the
-    # system blacklist
+    $self->RecipientFilter(
+        Callback => sub {
+            return unless $blacklist{ lc $_[0] };
+            return "$_[0] is blacklisted $squelch_reasons{ $blacklist{ lc $_[0] } }. Skipping";
+        },
+    );
 
-    # Trim leading and trailing spaces. 
-    @blacklist = map { RT::User->CanonicalizeEmailAddress( $_->address ) } Email::Address->parse(join(', ', grep {defined} @blacklist));
 
-    foreach my $type (@EMAIL_RECIPIENT_HEADERS) {
+    # Cycle through the people we're sending to and pull out anyone that meets any of the callbacks
+    for my $type (@EMAIL_RECIPIENT_HEADERS) {
         my @addrs;
-        foreach my $addr ( @{ $self->{$type} } ) {
 
-         # Weed out any RT addresses. We really don't want to talk to ourselves!
-         # If we get a reply back, that means it's not an RT address
-            if ( !RT::EmailParser->CullRTAddresses($addr) ) {
-                $RT::Logger->info( $msgid . "$addr appears to point to this RT instance. Skipping" );
-                next;
+      ADDRESS:
+        for my $addr ( @{ $self->{$type} } ) {
+            for my $filter ( map {$_->{Callback}} @{$self->{RecipientFilter}} ) {
+                my $skip = $filter->($addr);
+                next unless $skip;
+                $RT::Logger->info( "$msgid $skip" );
+                next ADDRESS;
             }
-            if ( grep /^\Q$addr\E$/, @blacklist ) {
-                $RT::Logger->info( $msgid . "$addr was blacklisted for outbound mail on this transaction. Skipping");
-                next;
+            push @addrs, $addr;
+        }
+
+      NOSQUELCH_ADDRESS:
+        for my $addr ( @{ $self->{NoSquelch}{$type} } ) {
+            for my $filter ( map {$_->{Callback}} grep {$_->{All}} @{$self->{RecipientFilter}} ) {
+                my $skip = $filter->($addr);
+                next unless $skip;
+                $RT::Logger->info( "$msgid $skip" );
+                next NOSQUELCH_ADDRESS;
             }
             push @addrs, $addr;
         }
+
         @{ $self->{$type} } = @addrs;
     }
 }
 
+=head2 RecipientFilter Callback => SUB, [All => 1]
+
+Registers a filter to be applied to addresses by
+L<RemoveInappropriateRecipients>.  The C<Callback> will be called with
+one address at a time, and should return false if the address should
+receive mail, or a message explaining why it should not be.  Passing a
+true value for C<All> will cause the filter to also be applied to
+NoSquelch (one-time Cc and Bcc) recipients as well.
+
+=cut
+
+sub RecipientFilter {
+    my $self = shift;
+    push @{ $self->{RecipientFilter}}, {@_};
+}
+
 =head2 SetReturnAddress is_comment => BOOLEAN
 
 Calculate and set From and Reply-To headers based on the is_comment flag.
@@ -864,41 +919,77 @@ sub SetReturnAddress {
     }
 
     unless ( $self->TemplateObj->MIMEObj->head->get('From') ) {
-        if ( RT->Config->Get('UseFriendlyFromLine') ) {
-            my $friendly_name = $args{friendly_name};
+        $self->SetFrom( %args, From => $replyto );
+    }
 
-            unless ( $friendly_name ) {
-                $friendly_name = $self->TransactionObj->CreatorObj->FriendlyName;
-                if ( $friendly_name =~ /^"(.*)"$/ ) {    # a quoted string
-                    $friendly_name = $1;
-                }
-            }
+    unless ( $self->TemplateObj->MIMEObj->head->get('Reply-To') ) {
+        $self->SetHeader( 'Reply-To', "$replyto" );
+    }
 
-            $friendly_name =~ s/"/\\"/g;
-            $self->SetHeader(
-                'From',
-                sprintf(
-                    RT->Config->Get('FriendlyFromLineFormat'),
-                    $self->MIMEEncodeString(
-                        $friendly_name, RT->Config->Get('EmailOutputEncoding')
-                    ),
-                    $replyto
+}
+
+=head2 SetFrom ( From => emailaddress )
+
+Set the From: address for outgoing email
+
+=cut
+
+sub SetFrom {
+    my $self = shift;
+    my %args = @_;
+
+    my $from = $args{From};
+
+    if ( RT->Config->Get('UseFriendlyFromLine') ) {
+        my $friendly_name = $self->GetFriendlyName(%args);
+        $from = 
+            sprintf(
+                RT->Config->Get('FriendlyFromLineFormat'),
+                $self->MIMEEncodeString(
+                    $friendly_name, RT->Config->Get('EmailOutputEncoding')
                 ),
+                $args{From}
             );
-        } else {
-            $self->SetHeader( 'From', $replyto );
-        }
     }
 
-    unless ( $self->TemplateObj->MIMEObj->head->get('Reply-To') ) {
-        $self->SetHeader( 'Reply-To', "$replyto" );
+    $self->SetHeader( 'From', $from );
+
+    #also set Sender:, otherwise MTAs add a nonsensical value like rt@machine,
+    #and then Outlook prepends "rt@machine on behalf of" to the From: header
+    $self->SetHeader( 'Sender', $from );
+}
+
+=head2 GetFriendlyName
+
+Calculate the proper Friendly Name based on the creator of the transaction
+
+=cut
+
+sub GetFriendlyName {
+    my $self = shift;
+    my %args = (
+        is_comment => 0,
+        friendly_name => '',
+        @_
+    );
+    my $friendly_name = $args{friendly_name};
+
+    unless ( $friendly_name ) {
+        $friendly_name = $self->TransactionObj->CreatorObj->FriendlyName;
+        if ( $friendly_name =~ /^"(.*)"$/ ) {    # a quoted string
+            $friendly_name = $1;
+        }
     }
 
+    $friendly_name =~ s/"/\\"/g;
+    return $friendly_name;
+
 }
 
 =head2 SetHeader FIELD, VALUE
 
-Set the FIELD of the current MIME object into VALUE.
+Set the FIELD of the current MIME object into VALUE, which should be in
+characters, not bytes.  Returns the new header, in bytes.
 
 =cut
 
@@ -911,7 +1002,7 @@ sub SetHeader {
     chomp $field;
     my $head = $self->TemplateObj->MIMEObj->head;
     $head->fold_length( $field, 10000 );
-    $head->replace( $field, $val );
+    $head->replace( $field, Encode::encode( "UTF-8", $val ) );
     return $head->get($field);
 }
 
@@ -931,8 +1022,14 @@ sub SetSubject {
         return ();
     }
 
-    my $message = $self->TransactionObj->Attachments;
+    # don't use Transaction->Attachments because it caches
+    # and anything which later calls ->Attachments will be hurt
+    # by our RowsPerPage() call.  caching is hard.
+    my $message = RT::Attachments->new( $self->CurrentUser );
+    $message->Limit( FIELD => 'TransactionId', VALUE => $self->TransactionObj->id);
+    $message->OrderBy( FIELD => 'id', ORDER => 'ASC' );
     $message->RowsPerPage(1);
+
     if ( $self->{'Subject'} ) {
         $subject = $self->{'Subject'};
     } elsif ( my $first = $message->First ) {
@@ -960,11 +1057,12 @@ sub SetSubjectToken {
     my $self = shift;
 
     my $head = $self->TemplateObj->MIMEObj->head;
-    $head->replace(
-        Subject => RT::Interface::Email::AddSubjectTag(
-            Encode::decode_utf8( $head->get('Subject') ),
-            $self->TicketObj,
-        ),
+    $self->SetHeader(
+        Subject =>
+            RT::Interface::Email::AddSubjectTag(
+                Encode::decode( "UTF-8", $head->get('Subject') ),
+                $self->TicketObj,
+            ),
     );
 }
 
@@ -976,16 +1074,17 @@ Set References and In-Reply-To headers for this message.
 
 sub SetReferencesHeaders {
     my $self = shift;
-    my ( @in_reply_to, @references, @msgid );
 
-    if ( my $top = $self->TransactionObj->Message->First ) {
-        @in_reply_to = split( /\s+/m, $top->GetHeader('In-Reply-To') || '' );
-        @references  = split( /\s+/m, $top->GetHeader('References')  || '' );
-        @msgid       = split( /\s+/m, $top->GetHeader('Message-ID')  || '' );
-    } else {
+    my $top = $self->TransactionObj->Message->First;
+    unless ( $top ) {
+        $self->SetHeader( References => $self->PseudoReference );
         return (undef);
     }
 
+    my @in_reply_to = split( /\s+/m, $top->GetHeader('In-Reply-To') || '' );
+    my @references  = split( /\s+/m, $top->GetHeader('References')  || '' );
+    my @msgid       = split( /\s+/m, $top->GetHeader('Message-ID')  || '' );
+
     # There are two main cases -- this transaction was created with
     # the RT Web UI, and hence we want to *not* append its Message-ID
     # to the References and In-Reply-To.  OR it came from an outside
@@ -1045,18 +1144,14 @@ Returns a fake Message-ID: header for the ticket to allow a base level of thread
 =cut
 
 sub PseudoReference {
-
     my $self = shift;
-    my $pseudo_ref
-        = '<RT-Ticket-'
-        . $self->TicketObj->id . '@'
-        . RT->Config->Get('Organization') . '>';
-    return $pseudo_ref;
+    return RT::Interface::Email::PseudoReference( $self->TicketObj );
 }
 
 =head2 SetHeaderAsEncoding($field_name, $charset_encoding)
 
-This routine converts the field into specified charset encoding.
+This routine converts the field into specified charset encoding, then
+applies the MIME-Header transfer encoding.
 
 =cut
 
@@ -1066,13 +1161,8 @@ sub SetHeaderAsEncoding {
 
     my $head = $self->TemplateObj->MIMEObj->head;
 
-    if ( lc($field) eq 'from' and RT->Config->Get('SMTPFrom') ) {
-        $head->replace( $field, RT->Config->Get('SMTPFrom') );
-        return;
-    }
-
-    my $value = $head->get( $field );
-    $value = $self->MIMEEncodeString( $value, $enc );
+    my $value = Encode::decode("UTF-8", $head->get( $field ));
+    $value = $self->MIMEEncodeString( $value, $enc ); # Returns bytes
     $head->replace( $field, $value );
 
 }
@@ -1082,7 +1172,8 @@ sub SetHeaderAsEncoding {
 Takes a perl string and optional encoding pass it over
 L<RT::Interface::Email/EncodeToMIME>.
 
-Basicly encode a string using B encoding according to RFC2047.
+Basicly encode a string using B encoding according to RFC2047, returning
+bytes.
 
 =cut
 
@@ -1091,10 +1182,7 @@ sub MIMEEncodeString {
     return RT::Interface::Email::EncodeToMIME( String => $_[0], Charset => $_[1] );
 }
 
-eval "require RT::Action::SendEmail_Vendor";
-die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Vendor.pm} );
-eval "require RT::Action::SendEmail_Local";
-die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Local.pm} );
+RT::Base->_ImportOverlays();
 
 1;