Commit fedc6149 authored by Byron Jones's avatar Byron Jones

Bug 714724 - correctly encode emails as quoted-printable

r=LpSolit,a=sgreen
parent 48649030
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::MIME;
use strict;
use warnings;
use 5.10.1;
use parent qw(Email::MIME);
use Encode qw(encode);
sub new {
my ($class, $msg) = @_;
# Template-Toolkit trims trailing newlines, which is problematic when
# parsing headers.
$msg =~ s/\n*$/\n/;
# Because the encoding headers are not present in our email templates, we
# need to treat them as binary UTF-8 when parsing.
my ($in_header, $has_type, $has_encoding, $has_body) = (1);
foreach my $line (split(/\n/, $msg)) {
if ($line eq '') {
$in_header = 0;
next;
}
if (!$in_header) {
$has_body = 1;
last;
}
$has_type = 1 if $line =~ /^Content-Type:/i;
$has_encoding = 1 if $line =~ /^Content-Transfer-Encoding:/i;
}
if ($has_body) {
if (!$has_type) {
$msg = qq#Content-Type: text/plain; charset="UTF-8"\n# . $msg;
}
if (!$has_encoding) {
$msg = qq#Content-Transfer-Encoding: binary\n# . $msg;
}
}
if (utf8::is_utf8($msg)) {
utf8::encode($msg);
}
# RFC 2822 requires us to have CRLF for our line endings and
# Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF)
# directly because Perl translates "\n" depending on what platform
# you're running on. See http://perldoc.perl.org/perlport.html#Newlines
$msg =~ s/(?:\015+)?\012/\015\012/msg;
return $class->SUPER::new($msg);
}
sub as_string {
my $self = shift;
# We add this header to uniquely identify all email that we
# send as coming from this Bugzilla installation.
#
# We don't use correct_urlbase, because we want this URL to
# *always* be the same for this Bugzilla, in every email,
# even if the admin changes the "ssl_redirect" parameter some day.
$self->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
# We add this header to mark the mail as "auto-generated" and
# thus to hopefully avoid auto replies.
$self->header_set('Auto-Submitted', 'auto-generated');
# MIME-Version must be set otherwise some mailsystems ignore the charset
$self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version');
# Encode the headers correctly in quoted-printable
foreach my $header ($self->header_names) {
my @values = $self->header($header);
# We don't recode headers that happen multiple times.
next if scalar(@values) > 1;
if (my $value = $values[0]) {
utf8::decode($value) unless utf8::is_utf8($value);
# avoid excessive line wrapping done by Encode.
local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998;
my $encoded = encode('MIME-Q', $value);
$self->header_set($header, $encoded);
}
}
# Ensure the character-set and encoding is set correctly on single part
# emails. Multipart emails should have these already set when the parts
# are assembled.
if (scalar($self->parts) == 1) {
$self->charset_set('UTF-8');
$self->encoding_set('quoted-printable');
}
# Ensure we always return the encoded string
my $value = $self->SUPER::as_string();
if (utf8::is_utf8($value)) {
utf8::encode($value);
}
return $value;
}
1;
__END__
=head1 NAME
Bugzilla::MIME - Wrapper around Email::MIME for unifying MIME related
workarounds.
=head1 SYNOPSIS
use Bugzilla::MIME;
my $email = Bugzilla::MIME->new($message);
=head1 DESCRIPTION
Bugzilla::MIME subclasses Email::MIME and performs various fixes when parsing
and generating email.
......@@ -17,6 +17,7 @@ use parent qw(Exporter);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Hook;
use Bugzilla::MIME;
use Bugzilla::User;
use Bugzilla::Util;
......@@ -24,7 +25,6 @@ use Date::Format qw(time2str);
use Encode qw(encode);
use Encode::MIME::Header;
use Email::MIME;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP::Persistent;
use Bugzilla::Sender::Transport::Sendmail;
......@@ -34,17 +34,17 @@ sub generate_email {
my ($lang, $email_format, $msg_text, $msg_html, $msg_header);
if ($vars->{to_user}) {
$lang = $vars->{to_user}->setting('lang');
$email_format = $vars->{to_user}->setting('email_format');
$lang = $vars->{to_user}->setting('lang');
$email_format = $vars->{to_user}->setting('email_format');
} else {
# If there are users in the CC list who don't have an account,
# use the default language for email notifications.
$lang = Bugzilla::User->new()->setting('lang');
# However we cannot fall back to the default email_format, since
# it may be HTML, and many of the includes used in the HTML
# template require a valid user object. Instead we fall back to
# the plaintext template.
$email_format = 'text_only';
# If there are users in the CC list who don't have an account,
# use the default language for email notifications.
$lang = Bugzilla::User->new()->setting('lang');
# However we cannot fall back to the default email_format, since
# it may be HTML, and many of the includes used in the HTML
# template require a valid user object. Instead we fall back to
# the plaintext template.
$email_format = 'text_only';
}
my $template = Bugzilla->template_inner($lang);
......@@ -55,26 +55,29 @@ sub generate_email {
|| ThrowTemplateError($template->error());
my @parts = (
Email::MIME->create(
Bugzilla::MIME->create(
attributes => {
content_type => "text/plain",
content_type => 'text/plain',
charset => 'UTF-8',
encoding => 'quoted-printable',
},
body => $msg_text,
body_str => $msg_text,
)
);
if ($templates->{html} && $email_format eq 'html') {
$template->process($templates->{html}, $vars, \$msg_html)
|| ThrowTemplateError($template->error());
push @parts, Email::MIME->create(
push @parts, Bugzilla::MIME->create(
attributes => {
content_type => "text/html",
content_type => 'text/html',
charset => 'UTF-8',
encoding => 'quoted-printable',
},
body => $msg_html,
body_str => $msg_html,
);
}
# TT trims the trailing newline, and threadingmarker may be ignored.
my $email = new Email::MIME("$msg_header\n");
my $email = Bugzilla::MIME->new($msg_header);
if (scalar(@parts) == 1) {
$email->content_type_set($parts[0]->content_type);
} else {
......@@ -101,18 +104,7 @@ sub MessageToMTA {
my $dbh = Bugzilla->dbh;
my $email;
if (ref $msg) {
$email = $msg;
}
else {
# RFC 2822 requires us to have CRLF for our line endings and
# Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF)
# directly because Perl translates "\n" depending on what platform
# you're running on. See http://perldoc.perl.org/perlport.html#Newlines
$msg =~ s/(?:\015+)?\012/\015\012/msg;
$email = new Email::MIME($msg);
}
my $email = ref($msg) ? $msg : Bugzilla::MIME->new($msg);
# If we're called from within a transaction, we don't want to send the
# email immediately, in case the transaction is rolled back. Instead we
......@@ -165,37 +157,6 @@ sub MessageToMTA {
}
}
# We add this header to uniquely identify all email that we
# send as coming from this Bugzilla installation.
#
# We don't use correct_urlbase, because we want this URL to
# *always* be the same for this Bugzilla, in every email,
# even if the admin changes the "ssl_redirect" parameter some day.
$email->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
# We add this header to mark the mail as "auto-generated" and
# thus to hopefully avoid auto replies.
$email->header_set('Auto-Submitted', 'auto-generated');
# MIME-Version must be set otherwise some mailsystems ignore the charset
$email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');
# Encode the headers correctly in quoted-printable
foreach my $header ($email->header_names) {
my @values = $email->header($header);
# We don't recode headers that happen multiple times.
next if scalar(@values) > 1;
if (my $value = $values[0]) {
utf8::decode($value) unless utf8::is_utf8($value);
# avoid excessive line wrapping done by Encode.
local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998;
my $encoded = encode('MIME-Q', $value);
$email->header_set($header, $encoded);
}
}
my $from = $email->header('From');
my $hostname;
......@@ -240,27 +201,6 @@ sub MessageToMTA {
return if $email->header('to') eq '';
$email->walk_parts(sub {
my ($part) = @_;
return if $part->parts > 1; # Top-level
my $content_type = $part->content_type || '';
$content_type =~ /charset=['"](.+)['"]/;
# If no charset is defined or is the default us-ascii,
# then we encode the email to UTF-8.
# XXX - This is a hack to workaround bug 723944.
if (!$1 || $1 eq 'us-ascii') {
my $body = $part->body;
$part->charset_set('UTF-8');
# encoding_set works only with bytes, not with utf8 strings.
my $raw = $part->body_raw;
if (utf8::is_utf8($raw)) {
utf8::encode($raw);
$part->body_set($raw);
}
$part->encoding_set('quoted-printable') if !is_7bit_clean($body);
}
});
if ($method eq "Test") {
my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
open TESTFILE, '>>:encoding(UTF-8)', $filename;
......
......@@ -35,6 +35,7 @@ use constant SUB_WHITELIST => (
'Bugzilla::JobQueue' => qr/(?:^work_once|work_until_done|subprocess_worker)$/,
'Bugzilla::Search' => qr/^SPECIAL_PARSING$/,
'Bugzilla::Template' => qr/^field_name$/,
'Bugzilla::MIME' => qr/^as_string$/,
);
# These modules do not need to be documented, generally because they
......
......@@ -8,10 +8,6 @@
[%# INTERFACE:
# subject: subject line of message
# alternatives: array of hashes containing:
# type: MIME type
# content: verbatim content
# boundary: a string that has been generated to be a unique boundary
# recipient: user object for the intended recipient of the message
# from: Bugzilla system email address
#%]
......@@ -19,21 +15,5 @@
From: [% from %]
To: [% recipient.email %]
Subject: [[% terms.Bugzilla %]] [% subject %]
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="[% boundary %]"
X-Bugzilla-Type: whine
This is a MIME multipart message. It is possible that your mail program
doesn't quite handle these properly. Some or all of the information in this
message may be unreadable.
[% FOREACH part=alternatives %]
--[% boundary %]
Content-type: [% part.type +%]
[%+ part.content %]
[%+ END %]
--[% boundary %]--
......@@ -21,6 +21,7 @@ use Bugzilla::Constants;
use Bugzilla::Search;
use Bugzilla::User;
use Bugzilla::Mailer;
use Bugzilla::MIME;
use Bugzilla::Util;
use Bugzilla::Group;
......@@ -346,53 +347,20 @@ while (my $event = get_next_event) {
# - subject Subject line for the message
# - recipient user object for the recipient
# - author user object of the person who created the whine event
#
# In addition, mail adds two more fields to $args:
# - alternatives array of hashes defining mime multipart types and contents
# - boundary a MIME boundary generated using the process id and time
#
sub mail {
my $args = shift;
my $addressee = $args->{recipient};
# Don't send mail to someone whose bugmail notification is disabled.
return if $addressee->email_disabled;
my $template = Bugzilla->template_inner($addressee->setting('lang'));
my $msg = ''; # it's a temporary variable to hold the template output
$args->{'alternatives'} ||= [];
# put together the different multipart mime segments
return if $args->{recipient}->email_disabled;
$template->process("whine/mail.txt.tmpl", $args, \$msg)
or die($template->error());
push @{$args->{'alternatives'}},
$args->{to_user} = $args->{recipient};
MessageToMTA(generate_email(
$args,
{
'content' => $msg,
'type' => 'text/plain',
};
$msg = '';
$template->process("whine/mail.html.tmpl", $args, \$msg)
or die($template->error());
push @{$args->{'alternatives'}},
{
'content' => $msg,
'type' => 'text/html',
};
$msg = '';
# now produce a ready-to-mail mime-encoded message
$args->{'boundary'} = "----------" . $$ . "--" . time() . "-----";
$template->process("whine/multipart-mime.txt.tmpl", $args, \$msg)
or die($template->error());
MessageToMTA($msg);
delete $args->{'boundary'};
delete $args->{'alternatives'};
header => 'whine/header.txt.tmpl',
text => 'whine/mail.txt.tmpl',
html => 'whine/mail.html.tmpl',
}
));
}
# run_queries runs all of the queries associated with a schedule ID, adding
......
Markdown is supported
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