Commit 41d084f1 authored by Alex Vandiver's avatar Alex Vandiver
Browse files

Ensure all MIME::Entity headers are UTF-8 encoded bytes

Placing wide characters into MIME::Entity objects can lead to
double-encoding, as discovered most recently in d469cacc.  Explicitly
decode all headers as UTF-8 when retrieving them with ->get(), and
encode them as UTF-8 before updating them with ->set() or ->replace().
This also applies to headers passed to ->build().  The only exceptions
to this are fixed strings in the source (which, in the absence of "use
utf8", are always bytes).

While the majority of these headers will never have wide characters in
them, always decoding and encoding ensures the proper disipline to
guarantee that strings with the "UTF8" flag do not get placed in a
header, which can cause double-encoding.
parent 6d9bd63c
......@@ -257,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);
......@@ -276,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}++;
......@@ -299,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;
}
......@@ -531,7 +531,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(
......@@ -643,7 +643,7 @@ 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);
......@@ -752,7 +752,7 @@ sub RemoveInappropriateRecipients {
# 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 ) {
......@@ -1140,8 +1140,8 @@ sub SetHeaderAsEncoding {
my $head = $self->TemplateObj->MIMEObj->head;
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 );
}
......@@ -1151,7 +1151,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
......
......@@ -110,7 +110,7 @@ sub Prepare {
my $txn_attachment = $self->TransactionObj->Attachments->First;
for my $header (qw/From To Cc Bcc/) {
if ( $txn_attachment->GetHeader( $header ) ) {
$mime->head->replace( $header => $txn_attachment->GetHeader($header) );
$mime->head->replace( $header => Encode::encode( "UTF-8", $txn_attachment->GetHeader($header) ) );
}
}
......
......@@ -130,13 +130,12 @@ sub Create {
my $head = $Attachment->head;
# Get the subject
my $Subject = $head->get( 'subject', 0 );
my $Subject = Encode::decode( 'UTF-8', $head->get( 'subject' ) );
$Subject = '' unless defined $Subject;
chomp $Subject;
utf8::decode( $Subject ) unless utf8::is_utf8( $Subject );
#Get the Message-ID
my $MessageId = $head->get( 'Message-ID', 0 );
my $MessageId = Encode::decode( "UTF-8", $head->get( 'Message-ID' ) );
defined($MessageId) or $MessageId = '';
chomp ($MessageId);
$MessageId =~ s/^<(.*?)>$/$1/o;
......@@ -158,7 +157,7 @@ sub Create {
# MIME::Head doesn't support perl strings well and can return
# octets which later will be double encoded in low-level code
utf8::decode( $head ) unless utf8::is_utf8( $head );
$head = Encode::decode( 'UTF-8', $head );
# If a message has no bodyhandle, that means that it has subparts (or appears to)
# and we should act accordingly.
......
......@@ -437,15 +437,17 @@ sub SignEncrypt {
$args{'Signer'} =
$self->UseKeyForSigning
|| do {
my $addr = (Email::Address->parse( $entity->head->get( 'From' ) ))[0];
$addr? $addr->address : undef
my ($addr) = map {Email::Address->parse( Encode::decode( "UTF-8", $_ ) )}
$entity->head->get( 'From' );
$addr ? $addr->address : undef
};
}
if ( $args{'Encrypt'} && !$args{'Recipients'} ) {
my %seen;
$args{'Recipients'} = [
grep $_ && !$seen{ $_ }++, map $_->address,
map Email::Address->parse( $entity->head->get( $_ ) ),
map Email::Address->parse( Encode::decode("UTF-8", $_ ) ),
map $entity->head->get( $_ ),
qw(To Cc Bcc)
];
}
......
......@@ -494,7 +494,8 @@ sub SignEncryptRFC3156 {
}
if ( $args{'Encrypt'} ) {
my @recipients = map $_->address,
map Email::Address->parse( $entity->head->get( $_ ) ),
map Email::Address->parse( Encode::decode( "UTF-8", $_ ) ),
map $entity->head->get( $_ ),
qw(To Cc Bcc);
my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
......
......@@ -220,7 +220,7 @@ sub SignEncrypt {
if ( $args{'Encrypt'} ) {
my %seen;
$args{'Recipients'} = [
grep !$seen{$_}++, map $_->address, map Email::Address->parse($_),
grep !$seen{$_}++, map $_->address, map Email::Address->parse(Encode::decode("UTF-8",$_)),
grep defined && length, map $entity->head->get($_), qw(To Cc Bcc)
];
}
......@@ -742,7 +742,8 @@ sub CheckIfProtected {
if ( $security_type eq 'encrypted' ) {
my $top = $args{'TopEntity'}->head;
$res{'Recipients'} = [grep defined && length, map $top->get($_), 'To', 'Cc'];
$res{'Recipients'} = [map {Encode::decode("UTF-8", $_)}
grep defined && length, map $top->get($_), 'To', 'Cc'];
}
return %res;
......
......@@ -299,8 +299,8 @@ sub ParseCcAddressesFromHead {
my (@Addresses);
my @ToObjs = Email::Address->parse( $self->Head->get('To') );
my @CcObjs = Email::Address->parse( $self->Head->get('Cc') );
my @ToObjs = Email::Address->parse( Encode::decode( "UTF-8", $self->Head->get('To') ) );
my @CcObjs = Email::Address->parse( Encode::decode( "UTF-8", $self->Head->get('Cc') ) );
foreach my $AddrObj ( @ToObjs, @CcObjs ) {
my $Address = $AddrObj->address;
......
......@@ -282,7 +282,7 @@ sub SetMIMEEntityToEncoding {
);
# If this is a textual entity, we'd need to preserve its original encoding
$head->replace( "X-RT-Original-Encoding" => $charset )
$head->replace( "X-RT-Original-Encoding" => Encode::encode( "UTF-8", $charset ) )
if $head->mime_attr('content-type.charset') or IsTextualContentType($head->mime_type);
return unless IsTextualContentType($head->mime_type);
......@@ -294,7 +294,7 @@ sub SetMIMEEntityToEncoding {
$RT::Logger->debug( "Converting '$charset' to '$enc' for "
. $head->mime_type . " - "
. ( $head->get('subject') || 'Subjectless message' ) );
. ( Encode::decode("UTF-8",$head->get('subject')) || 'Subjectless message' ) );
# NOTE:: see the comments at the end of the sub.
Encode::_utf8_off($string);
......
......@@ -110,7 +110,7 @@ sub CheckForLoops {
my $head = shift;
# If this instance of RT sent it our, we don't want to take it in
my $RTLoop = $head->get("X-RT-Loop-Prevention") || "";
my $RTLoop = Encode::decode( "UTF-8", $head->get("X-RT-Loop-Prevention") || "" );
chomp ($RTLoop); # remove that newline
if ( $RTLoop eq RT->Config->Get('rtname') ) {
return 1;
......@@ -248,16 +248,17 @@ sub MailError {
# the colons are necessary to make ->build include non-standard headers
my %entity_args = (
Type => "multipart/mixed",
From => $args{'From'},
Bcc => $args{'Bcc'},
To => $args{'To'},
Subject => $args{'Subject'},
'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'),
From => Encode::encode( "UTF-8", $args{'From'} ),
Bcc => Encode::encode( "UTF-8", $args{'Bcc'} ),
To => Encode::encode( "UTF-8", $args{'To'} ),
Subject => EncodeToMIME( String => $args{'Subject'} ),
'X-RT-Loop-Prevention:' => Encode::encode( "UTF-8", RT->Config->Get('rtname') ),
);
# only set precedence if the sysadmin wants us to
if (defined(RT->Config->Get('DefaultErrorMailPrecedence'))) {
$entity_args{'Precedence:'} = RT->Config->Get('DefaultErrorMailPrecedence');
$entity_args{'Precedence:'} =
Encode::encode( "UTF-8", RT->Config->Get('DefaultErrorMailPrecedence') );
}
my $entity = MIME::Entity->build(%entity_args);
......@@ -366,7 +367,7 @@ sub SendEmail {
return 0;
}
my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
chomp $msgid;
# If we don't have any recipients to send to, don't send a message;
......@@ -386,7 +387,7 @@ sub SendEmail {
if (my $precedence = RT->Config->Get('DefaultMailPrecedence')
and !$args{'Entity'}->head->get("Precedence")
) {
$args{'Entity'}->head->replace( 'Precedence', $precedence );
$args{'Entity'}->head->replace( 'Precedence', Encode::encode("UTF-8",$precedence) );
}
if ( $TransactionObj && !$TicketObj
......@@ -400,7 +401,7 @@ sub SendEmail {
require RT::Date;
my $date = RT::Date->new( RT->SystemUser );
$date->SetToNow;
$head->replace( 'Date', $date->RFC2822( Timezone => 'server' ) );
$head->replace( 'Date', Encode::encode("UTF-8",$date->RFC2822( Timezone => 'server' ) ) );
}
unless ( $head->get('MIME-Version') ) {
# We should never have to set the MIME-Version header
......@@ -590,7 +591,7 @@ sub SendEmailUsingTemplate {
$mail->head->replace( $_ => Encode::encode_utf8( $args{ $_ } ) )
foreach grep defined $args{$_}, qw(To Cc Bcc From);
$mail->head->replace( $_ => $args{ExtraHeaders}{$_} )
$mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
foreach keys %{ $args{ExtraHeaders} };
SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
......@@ -673,7 +674,7 @@ sub SignEncrypt {
);
return 1 unless $args{'Sign'} || $args{'Encrypt'};
my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
chomp $msgid;
$RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
......@@ -914,7 +915,8 @@ sub ParseCcAddressesFromHead {
return
grep $_ ne $current_address && !RT::EmailParser->IsRTAddress( $_ ),
map lc $user->CanonicalizeEmailAddress( $_->address ),
map RT::EmailParser->CleanupAddresses( Email::Address->parse( $args{'Head'}->get( $_ ) ) ),
map RT::EmailParser->CleanupAddresses( Email::Address->parse(
Encode::decode( "UTF-8", $args{'Head'}->get( $_ ) ) ) ),
qw(To Cc);
}
......@@ -940,7 +942,7 @@ sub ParseSenderAddressFromHead {
#Figure out who's sending this message.
foreach my $header ( @sender_headers ) {
my $addr_line = $head->get($header) || next;
my $addr_line = Encode::decode( "UTF-8", $head->get($header) ) || next;
my ($addr, $name) = ParseAddressFromHeader( $addr_line );
# only return if the address is not empty
return ($addr, $name, @errors) if $addr;
......@@ -968,7 +970,7 @@ sub ParseErrorsToAddressFromHead {
foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) {
# If there's a header of that name
my $headerobj = $head->get($header);
my $headerobj = Encode::decode( "UTF-8", $head->get($header) );
if ($headerobj) {
my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
......@@ -1013,9 +1015,9 @@ sub DeleteRecipientsFromHead {
my %skip = map { lc $_ => 1 } @_;
foreach my $field ( qw(To Cc Bcc) ) {
$head->replace( $field =>
$head->replace( $field => Encode::encode( "UTF-8",
join ', ', map $_->format, grep !$skip{ lc $_->address },
Email::Address->parse( $head->get( $field ) )
Email::Address->parse( Encode::decode( "UTF-8", $head->get( $field ) ) ) )
);
}
}
......@@ -1048,7 +1050,7 @@ sub SetInReplyTo {
my $get_header = sub {
my @res;
if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
@res = $args{'InReplyTo'}->head->get( shift );
@res = map {Encode::decode("UTF-8", $_)} $args{'InReplyTo'}->head->get( shift );
} else {
@res = $args{'InReplyTo'}->GetHeader( shift ) || '';
}
......@@ -1096,7 +1098,7 @@ mail gateway code before Ticket creation.
sub ExtractTicketId {
my $entity = shift;
my $subject = $entity->head->get('Subject') || '';
my $subject = Encode::decode( "UTF-8", $entity->head->get('Subject') || '' );
chomp $subject;
return ParseTicketId( $subject );
}
......@@ -1319,14 +1321,14 @@ sub Gateway {
my $head = $Message->head;
my $ErrorsTo = ParseErrorsToAddressFromHead( $head );
my $Sender = (ParseSenderAddressFromHead( $head ))[0];
my $From = $head->get("From");
my $From = Encode::decode( "UTF-8", $head->get("From") );
chomp $From if defined $From;
my $MessageId = $head->get('Message-ID')
my $MessageId = Encode::decode( "UTF-8", $head->get('Message-ID') )
|| "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
#Pull apart the subject line
my $Subject = $head->get('Subject') || '';
my $Subject = Encode::decode( "UTF-8", $head->get('Subject') || '');
chomp $Subject;
# Lets check for mail loops of various sorts.
......@@ -1349,7 +1351,7 @@ sub Gateway {
$args{'ticket'} ||= ExtractTicketId( $Message );
# ExtractTicketId may have been overridden, and edited the Subject
my $NewSubject = $Message->head->get('Subject');
my $NewSubject = Encode::decode( "UTF-8", $Message->head->get('Subject') );
chomp $NewSubject;
$SystemTicket = RT::Ticket->new( RT->SystemUser );
......@@ -1593,7 +1595,7 @@ sub _RunUnsafeAction {
@_
);
my $From = $args{Message}->head->get("From");
my $From = Encode::decode( "UTF-8", $args{Message}->head->get("From") );
if ( $args{'Action'} =~ /^take$/i ) {
my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
......@@ -1749,7 +1751,7 @@ sub _HandleMachineGeneratedMail {
# to the scrip. We might want to notify nobody. Or just
# the RT Owner. Or maybe all Privileged watchers.
my ( $Sender, $junk ) = ParseSenderAddressFromHead($head);
$head->replace( 'RT-Squelch-Replies-To', $Sender );
$head->replace( 'RT-Squelch-Replies-To', Encode::encode("UTF-8", $Sender ) );
$head->replace( 'RT-DetectedAutoGenerated', 'true' );
}
return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
......
......@@ -175,7 +175,7 @@ sub GetCurrentUser {
foreach my $protocol ( @check_protocols ) {
my @status = grep defined && length,
$part->head->get( "X-RT-$protocol-Status" );
map Encode::decode( "UTF-8", $_), $part->head->get( "X-RT-$protocol-Status" );
next unless @status;
push @found, $protocol;
......@@ -186,20 +186,20 @@ sub GetCurrentUser {
}
if ( $_->{Operation} eq 'Verify' && $_->{Status} eq 'DONE' ) {
$part->head->replace(
'X-RT-Incoming-Signature' => $_->{UserString}
'X-RT-Incoming-Signature' => Encode::encode( "UTF-8", $_->{UserString} )
);
}
}
}
$part->head->replace(
'X-RT-Incoming-Encryption' =>
'X-RT-Incoming-Encryption' =>
$decrypted ? 'Success' : 'Not encrypted'
);
}
my %seen;
$args{'Message'}->head->replace( 'X-RT-Privacy' => $_ )
$args{'Message'}->head->replace( 'X-RT-Privacy' => Encode::encode( "UTF-8", $_ ) )
foreach grep !$seen{$_}++, @found;
return 1;
......
......@@ -2494,16 +2494,16 @@ sub MakeMIMEEntity {
my $uploadinfo = $cgi_object->uploadInfo($filehandle);
my $filename = "$filehandle";
my $filename = Encode::decode("UTF-8","$filehandle");
$filename =~ s{^.*[\\/]}{};
$Message->attach(
Type => $uploadinfo->{'Content-Type'},
Filename => $filename,
Filename => Encode::encode("UTF-8",$filename),
Data => \@content, # Bytes, as read directly from the file, above
);
if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) {
$Message->head->replace( 'Subject' => $filename );
$Message->head->replace( 'Subject' => Encode::encode( "UTF-8", $filename ) );
}
# Attachment parts really shouldn't get a Message-ID or "interface"
......
......@@ -867,8 +867,8 @@ sub create_ticket {
if ( my $content = delete $args{'Content'} ) {
$args{'MIMEObj'} = MIME::Entity->build(
From => $args{'Requestor'},
Subject => $args{'Subject'},
From => Encode::encode( "UTF-8", $args{'Requestor'} ),
Subject => RT::Interface::Email::EncodeToMIME( String => $args{'Subject'} ),
Type => "text/plain",
Charset => "UTF-8",
Data => Encode::encode( "UTF-8", $content ),
......
......@@ -1604,7 +1604,7 @@ sub _RecordNote {
# internal Message-ID now, so all emails sent because of this
# message have a common Message-ID
my $org = RT->Config->Get('Organization');
my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
$args{'MIMEObj'}->head->replace(
'RT-Message-ID' => Encode::encode_utf8(
......@@ -1616,7 +1616,7 @@ sub _RecordNote {
#Record the correspondence (write the transaction)
my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
Type => $args{'NoteType'},
Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
TimeTaken => $args{'TimeTaken'},
MIMEObj => $args{'MIMEObj'},
CommitScrips => $args{'CommitScrips'},
......@@ -3058,7 +3058,6 @@ sub Forward {
unless grep {length $args{$_}} qw/To Cc Bcc/;
my $mime = MIME::Entity->build(
Subject => $args{Subject},
Type => $args{ContentType},
Data => Encode::encode( "UTF-8", $args{Content} ),
);
......
......@@ -125,7 +125,7 @@ sub mime_recommended_filename {
$head = $head->head if $head->isa('MIME::Entity');
for my $attr_name (qw( content-disposition.filename content-type.name )) {
my $value = $head->mime_attr($attr_name);
my $value = Encode::decode("UTF-8",$head->mime_attr($attr_name));
if ( defined $value && $value =~ /\S/ ) {
return $value;
}
......
......@@ -171,8 +171,10 @@ sub send_digest {
}
# Set our sender and recipient.
$digest_template->MIMEObj->head->replace( 'From', RT::Config->Get('CorrespondAddress') );
$digest_template->MIMEObj->head->replace( 'To', $to );
$digest_template->MIMEObj->head->replace(
'From', Encode::encode( "UTF-8", RT::Config->Get('CorrespondAddress') ) );
$digest_template->MIMEObj->head->replace(
'To', Encode::encode( "UTF-8", $to ) );
if ($print) {
$digest_template->MIMEObj->print;
......
......@@ -193,8 +193,8 @@ else {
$v{MIMEObj} =
MIME::Entity->build(
Type => "multipart/mixed",
From => $session{CurrentUser}->EmailAddress,
Subject => $v{Subject},
From => Encode::encode( "UTF-8", $session{CurrentUser}->EmailAddress ),
Subject => Encode::encode( "UTF-8", $v{Subject}),
'X-RT-Interface' => 'REST',
);
$v{MIMEObj}->attach(
......
......@@ -88,7 +88,7 @@ my %squelched = ProcessTransactionSquelching( \%ARGS );
</ul>
% }
% if (RT->Config->Get('PreviewScripMessages')) {
<textarea cols="80" rows="5"><%$scrip->ActionObj->Action->TemplateObj->MIMEObj->as_string%></textarea>
<textarea cols="80" rows="5"><% Encode::decode( "UTF-8", $scrip->ActionObj->Action->TemplateObj->MIMEObj->as_string )%></textarea>
% }
<br />
% }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment