]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 714724 - correctly encode emails as quoted-printable
authorByron Jones <glob@mozilla.com>
Mon, 31 Aug 2015 04:20:49 +0000 (12:20 +0800)
committerByron Jones <glob@mozilla.com>
Mon, 31 Aug 2015 04:20:49 +0000 (12:20 +0800)
r=LpSolit,a=sgreen

Bugzilla/MIME.pm [new file with mode: 0644]
Bugzilla/Mailer.pm
t/011pod.t
template/en/default/whine/header.txt.tmpl [moved from template/en/default/whine/multipart-mime.txt.tmpl with 52% similarity]
whine.pl

diff --git a/Bugzilla/MIME.pm b/Bugzilla/MIME.pm
new file mode 100644 (file)
index 0000000..20e667d
--- /dev/null
@@ -0,0 +1,128 @@
+# 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.
index 69db9c55782451a4d345a79945e5f0a72d470840..593a1067a09a760295455da5f00a1e0ca7332798 100644 (file)
@@ -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;
index 9c092eeb1165ef825a11629edc0125ce6b95f010..193435d4e338c407fc29b539214bd9f583a27a68 100644 (file)
@@ -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
similarity index 52%
rename from template/en/default/whine/multipart-mime.txt.tmpl
rename to template/en/default/whine/header.txt.tmpl
index d28f4cea609fcd42465b8bfdcd2e4bad5bfedb51..4067964f2a180800ca38ca404d722a875c76dc0b 100644 (file)
@@ -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
   #%]
 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 %]--
index a7e3ee1cf0e24f5f3b90fd6afc5bca5a1b56bdcc..dfc405200dfc14f4579e37dec53b66a430c882e3 100755 (executable)
--- a/whine.pl
+++ b/whine.pl
@@ -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