diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm index d12a1a6e7e..54ef78415f 100644 --- a/extensions/SecureMail/Extension.pm +++ b/extensions/SecureMail/Extension.pm @@ -499,15 +499,26 @@ sub _make_secure { body => _tct_encrypt($tct, $to_encrypt, $bug_id) ), ); - $email->parts_set(\@new_parts); - my $new_boundary = $email->{ct}{attributes}{boundary}; - - # Redo the old content type header with the new boundaries - # and other information needed for PGP - $email->header_set("Content-Type", - "multipart/encrypted; " - . "protocol=\"application/pgp-encrypted\"; " - . "boundary=\"$new_boundary\""); + # Keep the PGP/MIME payload as a standards-compliant two-part + # multipart/encrypted message. For fully secure bugmail, wrap it in a + # multipart/mixed message with a plaintext link so clients which do not + # expose the OpenPGP armour comment (notably Gmail) provide a usable + # route back to the bug. + my $encrypted_part = Email::MIME->create( + attributes => {content_type => 'multipart/encrypted'}, + parts => \@new_parts, + ); + _set_pgp_content_type($encrypted_part); + + if ($sanitize_subject && $bug_id) { + _wrap_pgp_bugmail($email, $encrypted_part, _bug_url($bug_id)); + } + else { + # Preserve the original top-level PGP/MIME structure for all other + # encrypted mail, such as private-comment notifications. + $email->parts_set(\@new_parts); + _set_pgp_content_type($email); + } } else { _fix_encoding($email); @@ -597,8 +608,7 @@ sub _make_secure { sub _tct_encrypt { my ($tct, $text, $bug_id) = @_; - my $comment = Bugzilla->localconfig->urlbase - . ($bug_id ? 'show_bug.cgi?id=' . $bug_id : ''); + my $comment = _bug_url($bug_id); my $encrypted; my $ok = eval { $encrypted = $tct->encrypt($text, $comment)->get; 1 }; if (!$ok) { @@ -614,6 +624,36 @@ sub _tct_encrypt { return $encrypted; } +sub _bug_url { + my ($bug_id) = @_; + return Bugzilla->localconfig->urlbase + . ($bug_id ? 'show_bug.cgi?id=' . $bug_id : ''); +} + +sub _wrap_pgp_bugmail { + my ($email, $encrypted_part, $bug_url) = @_; + + my $link_part = Email::MIME->create( + attributes => { + content_type => 'text/plain', + charset => 'UTF-8', + encoding => 'quoted-printable', + }, + body_str => "View this bug: $bug_url\n", + ); + $email->parts_set([$link_part, $encrypted_part]); + $email->content_type_set('multipart/mixed'); +} + +sub _set_pgp_content_type { + my ($email) = @_; + my $boundary = $email->{ct}{attributes}{boundary}; + $email->header_set("Content-Type", + "multipart/encrypted; " + . "protocol=\"application/pgp-encrypted\"; " + . "boundary=\"$boundary\""); +} + # Insert the subject into the part's body, as the subject of the message will # be sanitized. # XXX this incorrectly assumes all parts of the message are the body diff --git a/t/903-securemail-link.t b/t/903-securemail-link.t new file mode 100644 index 0000000000..211eb14768 --- /dev/null +++ b/t/903-securemail-link.t @@ -0,0 +1,81 @@ +# 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/. + +package main; + +use strict; +use warnings; +use 5.10.1; +use lib qw(. lib local/lib/perl5 extensions/SecureMail/lib); + +use Test::More; +use Email::MIME; + +BEGIN { + *Bugzilla::Extension::SecureMail::NAME = sub { 1 }; +} + +my $extension = './extensions/SecureMail/Extension.pm'; +require $extension; + +my $email = Email::MIME->create( + attributes => {content_type => 'multipart/alternative'}, + parts => [], +); +my $control_part = Email::MIME->create( + attributes => { + content_type => 'application/pgp-encrypted', + encoding => '7bit', + }, + body => "Version: 1\n", +); +my $data_part = Email::MIME->create( + attributes => { + content_type => 'application/octet-stream', + encoding => '7bit', + }, + body => 'encrypted data', +); +my $encrypted_part = Email::MIME->create( + attributes => {content_type => 'multipart/encrypted'}, + parts => [$control_part, $data_part], +); + +Bugzilla::Extension::SecureMail::_wrap_pgp_bugmail( + $email, + $encrypted_part, + 'https://bugzilla.example/show_bug.cgi?id=123', +); + +like($email->content_type, qr{\Amultipart/mixed(?:;|\z)}, + 'outer message is multipart/mixed'); +my @outer_parts = $email->parts; +is(scalar @outer_parts, 2, 'outer message contains the link and encrypted message'); +like($outer_parts[0]->content_type, qr{\Atext/plain(?:;|\z)}, + 'first part is plaintext'); +like($outer_parts[0]->body_str, + qr{\AView this bug: https://bugzilla\.example/show_bug\.cgi\?id=123\r?\n\z}, + 'plaintext part contains the bug URL'); +like($outer_parts[1]->content_type, qr{\Amultipart/encrypted(?:;|\z)}, + 'second part is the PGP/MIME message'); +my @encrypted_parts = $outer_parts[1]->parts; +is(scalar @encrypted_parts, 2, 'PGP/MIME message retains its two required parts'); +is($encrypted_parts[0]->content_type, 'application/pgp-encrypted', + 'first PGP/MIME part is the control information'); +is($encrypted_parts[1]->content_type, 'application/octet-stream', + 'second PGP/MIME part is encrypted data'); + +my $unwrapped_email = Email::MIME->create( + attributes => {content_type => 'multipart/alternative'}, + parts => [], +); +$unwrapped_email->parts_set([$control_part, $data_part]); +Bugzilla::Extension::SecureMail::_set_pgp_content_type($unwrapped_email); +my ($boundary) = $unwrapped_email->header('Content-Type') =~ /boundary="([^"]+)"/; +is($boundary, $unwrapped_email->{ct}{attributes}{boundary}, + 'unwrapped PGP/MIME header declares its own generated boundary'); +like($unwrapped_email->as_string, qr/--\Q$boundary\E/, + 'unwrapped PGP/MIME body uses its declared boundary'); + +done_testing;