From 20c4a66e3688773bd8836cbcfc9db1fb4513bd22 Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Thu, 3 Sep 2026 11:34:53 -0700 Subject: [PATCH 1/3] Bug 2050896 - Add secure bug link to PGP email body --- extensions/SecureMail/Extension.pm | 54 ++++++++++++++++++++---- t/903-securemail-link.t | 67 ++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 t/903-securemail-link.t diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm index d12a1a6e7e..6c60e8dee5 100644 --- a/extensions/SecureMail/Extension.pm +++ b/extensions/SecureMail/Extension.pm @@ -499,15 +499,33 @@ 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", + # 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, + ); + my $new_boundary = $encrypted_part->{ct}{attributes}{boundary}; + $encrypted_part->header_set("Content-Type", "multipart/encrypted; " . "protocol=\"application/pgp-encrypted\"; " . "boundary=\"$new_boundary\""); + + 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); + $email->header_set("Content-Type", + "multipart/encrypted; " + . "protocol=\"application/pgp-encrypted\"; " + . "boundary=\"$new_boundary\""); + } } else { _fix_encoding($email); @@ -597,8 +615,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 +631,27 @@ 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'); +} + # 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..0f2f2cd6c8 --- /dev/null +++ b/t/903-securemail-link.t @@ -0,0 +1,67 @@ +# 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/. + +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 { + package Bugzilla::Extension::SecureMail; + sub NAME { 1 } +} + +require './extensions/SecureMail/Extension.pm'; + +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'); + +done_testing; From 2b2165f7e08792bc68fd6efb5b61980a90ef122c Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Thu, 3 Sep 2026 12:50:21 -0700 Subject: [PATCH 2/3] Bug 2050896 - Preserve PGP MIME boundaries --- extensions/SecureMail/Extension.pm | 20 +++++++++++--------- t/903-securemail-link.t | 12 ++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm index 6c60e8dee5..54ef78415f 100644 --- a/extensions/SecureMail/Extension.pm +++ b/extensions/SecureMail/Extension.pm @@ -508,11 +508,7 @@ sub _make_secure { attributes => {content_type => 'multipart/encrypted'}, parts => \@new_parts, ); - my $new_boundary = $encrypted_part->{ct}{attributes}{boundary}; - $encrypted_part->header_set("Content-Type", - "multipart/encrypted; " - . "protocol=\"application/pgp-encrypted\"; " - . "boundary=\"$new_boundary\""); + _set_pgp_content_type($encrypted_part); if ($sanitize_subject && $bug_id) { _wrap_pgp_bugmail($email, $encrypted_part, _bug_url($bug_id)); @@ -521,10 +517,7 @@ sub _make_secure { # Preserve the original top-level PGP/MIME structure for all other # encrypted mail, such as private-comment notifications. $email->parts_set(\@new_parts); - $email->header_set("Content-Type", - "multipart/encrypted; " - . "protocol=\"application/pgp-encrypted\"; " - . "boundary=\"$new_boundary\""); + _set_pgp_content_type($email); } } else { @@ -652,6 +645,15 @@ sub _wrap_pgp_bugmail { $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 index 0f2f2cd6c8..b224caa575 100644 --- a/t/903-securemail-link.t +++ b/t/903-securemail-link.t @@ -64,4 +64,16 @@ is($encrypted_parts[0]->content_type, 'application/pgp-encrypted', 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; From 04f5854870d357e7c7023390c6dbdc7f7a2ae717 Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Thu, 3 Sep 2026 13:35:17 -0700 Subject: [PATCH 3/3] Bug 2050896 - Satisfy secure mail test lint --- t/903-securemail-link.t | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/t/903-securemail-link.t b/t/903-securemail-link.t index b224caa575..211eb14768 100644 --- a/t/903-securemail-link.t +++ b/t/903-securemail-link.t @@ -2,6 +2,8 @@ # 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; @@ -11,11 +13,11 @@ use Test::More; use Email::MIME; BEGIN { - package Bugzilla::Extension::SecureMail; - sub NAME { 1 } + *Bugzilla::Extension::SecureMail::NAME = sub { 1 }; } -require './extensions/SecureMail/Extension.pm'; +my $extension = './extensions/SecureMail/Extension.pm'; +require $extension; my $email = Email::MIME->create( attributes => {content_type => 'multipart/alternative'},