← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-email-decode-bytes into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3-email-decode-bytes into launchpad:master.

Commit message:
Fix types in email payload tests

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/397707

`Message.get_payload(decode=True)` returns bytes, not text.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-email-decode-bytes into launchpad:master.
diff --git a/lib/lp/blueprints/doc/specification-notifications.txt b/lib/lp/blueprints/doc/specification-notifications.txt
index 097cf25..275dbd9 100644
--- a/lib/lp/blueprints/doc/specification-notifications.txt
+++ b/lib/lp/blueprints/doc/specification-notifications.txt
@@ -128,7 +128,7 @@ Now let's take a look at what the notification looks like:
     >>> status_notification['Subject']
     '[Blueprint svg-support] Support Native SVG Objects'
     >>> body = status_notification.get_payload(decode=True)
-    >>> print(body)
+    >>> print(body.decode('UTF-8'))
     Blueprint changed by Foo Bar:
     <BLANKLINE>
         Definition Status: Drafting => Pending Approval
@@ -168,7 +168,7 @@ Whiteboard change:
     >>> status_notification['Subject']
     '[Blueprint svg-support] Support Native SVG Objects'
     >>> body = status_notification.get_payload(decode=True)
-    >>> print(body)
+    >>> print(body.decode('UTF-8'))
     Blueprint changed by Foo Bar:
     <BLANKLINE>
     Whiteboard set to:
@@ -208,7 +208,7 @@ Definition status and whiteboard change:
     >>> status_notification['Subject']
     '[Blueprint svg-support] Support Native SVG Objects'
     >>> body = status_notification.get_payload(decode=True)
-    >>> print(body)
+    >>> print(body.decode('UTF-8'))
     Blueprint changed by Foo Bar:
     <BLANKLINE>
         Definition Status: Pending Approval => Approved
@@ -248,7 +248,7 @@ Change priority:
     >>> status_notification['Subject']
     '[Blueprint svg-support] Support Native SVG Objects'
     >>> body = status_notification.get_payload(decode=True)
-    >>> print(body)
+    >>> print(body.decode('UTF-8'))
     Blueprint changed by Foo Bar:
     <BLANKLINE>
         Priority: High => Essential
@@ -282,7 +282,7 @@ Change approver, assignee and drafter:
     >>> status_notification['Subject']
     '[Blueprint svg-support] Support Native SVG Objects'
     >>> body = status_notification.get_payload(decode=True)
-    >>> print(body)
+    >>> print(body.decode('UTF-8'))
     Blueprint changed by Foo Bar:
     <BLANKLINE>
         Approver: Mark Shuttleworth => (none)
diff --git a/lib/lp/blueprints/model/tests/test_specification.py b/lib/lp/blueprints/model/tests/test_specification.py
index 4264e9f..73a8834 100644
--- a/lib/lp/blueprints/model/tests/test_specification.py
+++ b/lib/lp/blueprints/model/tests/test_specification.py
@@ -236,7 +236,7 @@ class TestSpecificationWorkItemsNotifications(TestCaseWithFactory):
             new_work_item['status'].name)
         [email] = stub.test_emails
         # Actual message is part 2 of the email.
-        msg = email[2]
+        msg = email[2].decode('UTF-8')
         self.assertIn(rationale, msg)
 
     def test_workitems_deleted_notification_message(self):
@@ -255,7 +255,7 @@ class TestSpecificationWorkItemsNotifications(TestCaseWithFactory):
         rationale = '- %s: %s' % (wi.title, wi.status.name)
         [email] = stub.test_emails
         # Actual message is part 2 of the email.
-        msg = email[2]
+        msg = email[2].decode('UTF-8')
         self.assertIn(rationale, msg)
 
     def test_workitems_changed_notification_message(self):
@@ -294,7 +294,7 @@ class TestSpecificationWorkItemsNotifications(TestCaseWithFactory):
             new_work_item['title'], new_work_item['status'].name)
         [email] = stub.test_emails
         # Actual message is part 2 of the email.
-        msg = email[2]
+        msg = email[2].decode('UTF-8')
         self.assertIn(rationale_removed, msg)
         self.assertIn(rationale_added, msg)
 
diff --git a/lib/lp/bugs/doc/bugnotification-sending.txt b/lib/lp/bugs/doc/bugnotification-sending.txt
index e0ed9aa..48495ac 100644
--- a/lib/lp/bugs/doc/bugnotification-sending.txt
+++ b/lib/lp/bugs/doc/bugnotification-sending.txt
@@ -31,7 +31,7 @@ easier.
     ...     print_notification_headers(
     ...         email_notification, extra_headers=extra_headers)
     ...     print()
-    ...     print(email_notification.get_payload(decode=True))
+    ...     print(six.ensure_str(email_notification.get_payload(decode=True)))
     ...     print("-" * 70)
 
 We'll also import a helper function to help us with database users.
@@ -273,7 +273,7 @@ lp/bugs/tests/test_bugnotification.py), and not demonstrated here.
 Another thing worth noting is that there's a blank line before the
 signature, and the signature marker has a trailing space.
 
-    >>> message.get_payload(decode=True).splitlines()
+    >>> six.ensure_str(message.get_payload(decode=True)).splitlines()
     [...,
      '',
      '-- ',
@@ -1063,26 +1063,26 @@ It's important to note that the bug title and description are wrapped
 and indented correctly in verbose notifications.
 
     >>> message = collated_messages['conciseteam@xxxxxxxxxxx'][0]
-    >>> payload = message.get_payload(decode=True)
-    >>> print(payload.split('\n'))
+    >>> payload = six.ensure_str(message.get_payload(decode=True))
+    >>> print(payload.splitlines())
     [...
-     u'Title:',
-     u'  In the beginning, the universe was created. This has made a lot of',
-     u'  people very angry and has been widely regarded as a bad move',
+     'Title:',
+     '  In the beginning, the universe was created. This has made a lot of',
+     '  people very angry and has been widely regarded as a bad move',
      ...
-     u'Bug description:',
-     u'  This is a long description of the bug, which will be automatically',
-     u"  wrapped by the BugNotification machinery. Ain't technology great?"...]
+     'Bug description:',
+     '  This is a long description of the bug, which will be automatically',
+     "  wrapped by the BugNotification machinery. Ain't technology great?"...]
 
 The title is also wrapped and indented in normal notifications.
 
     >>> message = collated_messages['verboseteam@xxxxxxxxxxx'][0]
-    >>> payload = message.get_payload(decode=True)
-    >>> print(payload.strip().split('\n'))
+    >>> payload = six.ensure_str(message.get_payload(decode=True))
+    >>> print(payload.strip().splitlines())
     [...
-     u'Title:',
-     u'  In the beginning, the universe was created. This has made a lot of',
-     u'  people very angry and has been widely regarded as a bad move'...]
+     'Title:',
+     '  In the beginning, the universe was created. This has made a lot of',
+     '  people very angry and has been widely regarded as a bad move'...]
 
 Self-Generated Bug Notifications
 --------------------------------
diff --git a/lib/lp/bugs/doc/externalbugtracker-debbugs.txt b/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
index 4353e79..3a427dd 100644
--- a/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
@@ -319,7 +319,7 @@ description as the description.
     >>> print(report['Subject'])
     evolution: Multiple format string vulnerabilities in Evolution
 
-    >>> print(report.get_payload(decode=True))
+    >>> print(six.ensure_text(report.get_payload(decode=True)))
     Package: evolution
     Severity: grave
     Tags: security
diff --git a/lib/lp/bugs/doc/initial-bug-contacts.txt b/lib/lp/bugs/doc/initial-bug-contacts.txt
index 6e89ca6..ed95d04 100644
--- a/lib/lp/bugs/doc/initial-bug-contacts.txt
+++ b/lib/lp/bugs/doc/initial-bug-contacts.txt
@@ -198,7 +198,7 @@ of the email with the bug title and URL.
     >>> msg['Subject']
     '[Bug 1] [NEW] Firefox does not support SVG'
 
-    >>> print(msg.get_payload(decode=True))
+    >>> print(six.ensure_text(msg.get_payload(decode=True)))
     You have been subscribed to a public bug:
     <BLANKLINE>
     Firefox needs to support embedded SVG images, now that the standard has
diff --git a/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py b/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py
index 291d87f..c3063f4 100644
--- a/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py
+++ b/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py
@@ -70,7 +70,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
         message = self.builder.build(
             'from', self.bug.owner, 'body', 'subject', utc_now, filters=[])
         self.assertNotIn(
-            "Launchpad-Notification-Type", message.get_payload(decode=True))
+            b"Launchpad-Notification-Type", message.get_payload(decode=True))
 
     def test_mails_append_expanded_footer(self):
         # Recipients with expanded_notification_footers receive an expanded
@@ -81,7 +81,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
         message = self.builder.build(
             'from', self.bug.owner, 'body', 'subject', utc_now, filters=[])
         self.assertIn(
-            "\n-- \nLaunchpad-Notification-Type: bug\n",
+            b"\n-- \nLaunchpad-Notification-Type: bug\n",
             message.get_payload(decode=True))
 
     def test_private_team(self):
diff --git a/lib/lp/bugs/mail/tests/test_handler.py b/lib/lp/bugs/mail/tests/test_handler.py
index eeebc9f..a017d48 100644
--- a/lib/lp/bugs/mail/tests/test_handler.py
+++ b/lib/lp/bugs/mail/tests/test_handler.py
@@ -187,13 +187,13 @@ class TestMaloneHandler(TestCaseWithFactory):
         big_body_text = 'This is really big.' * 10000
         message = self.getFailureForMessage(
             'new@xxxxxxxxxxxxxxxxxxx', body=big_body_text)
-        self.assertIn("The description is too long.", message)
+        self.assertIn(b"The description is too long.", message)
 
     def test_bug_not_found(self):
         # Non-existent bug numbers result in an informative error.
         message = self.getFailureForMessage('1234@xxxxxxxxxxxxxxxxxxx')
         self.assertIn(
-            "There is no such bug in Launchpad: 1234", message)
+            b"There is no such bug in Launchpad: 1234", message)
 
     def test_accessible_private_bug(self):
         # Private bugs are accessible by their subscribers.
@@ -216,7 +216,7 @@ class TestMaloneHandler(TestCaseWithFactory):
                 True, self.factory.makePerson())
         message = self.getFailureForMessage('4@xxxxxxxxxxxxxxxxxxx')
         self.assertIn(
-            "There is no such bug in Launchpad: 4", message)
+            b"There is no such bug in Launchpad: 4", message)
 
 
 class MaloneHandlerProcessTestCase(TestCaseWithFactory):
diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py
index 7454ad4..9973981 100644
--- a/lib/lp/bugs/scripts/tests/test_bugnotification.py
+++ b/lib/lp/bugs/scripts/tests/test_bugnotification.py
@@ -690,7 +690,7 @@ class EmailNotificationTestBase(TestCaseWithFactory):
 
 class EmailNotificationsBugMixin:
 
-    change_class = change_name = old = new = alt = unexpected_text = None
+    change_class = change_name = old = new = alt = unexpected_bytes = None
 
     def change(self, old, new):
         self.bug.addChange(
@@ -708,7 +708,7 @@ class EmailNotificationsBugMixin:
         # A smoketest.
         self.change(self.old, self.new)
         message, body = next(self.get_messages())
-        self.assertThat(body, Contains(self.unexpected_text))
+        self.assertThat(body, Contains(self.unexpected_bytes))
 
     def test_undone_change_sends_no_emails(self):
         self.change(self.old, self.new)
@@ -720,7 +720,7 @@ class EmailNotificationsBugMixin:
         self.change(self.new, self.old)
         self.change_other()
         message, body = next(self.get_messages())
-        self.assertThat(body, Not(Contains(self.unexpected_text)))
+        self.assertThat(body, Not(Contains(self.unexpected_bytes)))
 
     def test_multiple_undone_changes_sends_no_emails(self):
         self.change(self.old, self.new)
@@ -763,7 +763,7 @@ class EmailNotificationsBugTaskMixin(EmailNotificationsBugMixin):
         self.change(self.old, self.new, index=0)
         self.change(self.new, self.old, index=1)
         message, body = next(self.get_messages())
-        self.assertThat(body, Contains(self.unexpected_text))
+        self.assertThat(body, Contains(self.unexpected_bytes))
 
 
 class EmailNotificationsAddedRemovedMixin:
@@ -805,7 +805,7 @@ class TestEmailNotificationsBugTitle(
     old = "Old summary"
     new = "New summary"
     alt = "Another summary"
-    unexpected_text = '** Summary changed:'
+    unexpected_bytes = b'** Summary changed:'
 
 
 class TestEmailNotificationsBugTags(
@@ -816,7 +816,7 @@ class TestEmailNotificationsBugTags(
     old = ['foo', 'bar', 'baz']
     new = ['foo', 'bar']
     alt = ['bing', 'shazam']
-    unexpected_text = '** Tags'
+    unexpected_bytes = b'** Tags'
 
     def test_undone_ordered_set_sends_no_email(self):
         # Tags use ordered sets to generate change descriptions, which we
@@ -831,7 +831,7 @@ class TestEmailNotificationsBugDuplicate(
 
     change_class = BugDuplicateChange
     change_name = "duplicateof"
-    unexpected_text = 'duplicate'
+    unexpected_bytes = b'duplicate'
 
     def _bug(self):
         with lp_dbuser():
@@ -850,7 +850,7 @@ class TestEmailNotificationsBugTaskStatus(
     old = BugTaskStatus.TRIAGED
     new = BugTaskStatus.INPROGRESS
     alt = BugTaskStatus.INVALID
-    unexpected_text = 'Status: '
+    unexpected_bytes = b'Status: '
 
 
 class TestEmailNotificationsBugWatch(
@@ -862,8 +862,8 @@ class TestEmailNotificationsBugWatch(
     # bugwatch, so they can be handled just as a simple bugtask attribute
     # change, like status.
 
-    added_message = '** Bug watch added:'
-    removed_message = '** Bug watch removed:'
+    added_message = b'** Bug watch added:'
+    removed_message = b'** Bug watch removed:'
 
     @cachedproperty
     def tracker(self):
@@ -897,8 +897,8 @@ class TestEmailNotificationsBugWatch(
 class TestEmailNotificationsBranch(
     EmailNotificationsAddedRemovedMixin, EmailNotificationTestBase):
 
-    added_message = '** Branch linked:'
-    removed_message = '** Branch unlinked:'
+    added_message = b'** Branch linked:'
+    removed_message = b'** Branch unlinked:'
 
     def _branch(self):
         with lp_dbuser():
@@ -923,8 +923,8 @@ class TestEmailNotificationsBranch(
 class TestEmailNotificationsCVE(
     EmailNotificationsAddedRemovedMixin, EmailNotificationTestBase):
 
-    added_message = '** CVE added:'
-    removed_message = '** CVE removed:'
+    added_message = b'** CVE added:'
+    removed_message = b'** CVE removed:'
 
     def _cve(self, sequence):
         with lp_dbuser():
@@ -949,8 +949,8 @@ class TestEmailNotificationsCVE(
 class TestEmailNotificationsAttachments(
     EmailNotificationsAddedRemovedMixin, EmailNotificationTestBase):
 
-    added_message = '** Attachment added:'
-    removed_message = '** Attachment removed:'
+    added_message = b'** Attachment added:'
+    removed_message = b'** Attachment removed:'
 
     def _attachment(self):
         with lp_dbuser():
@@ -1270,10 +1270,10 @@ class TestExpandedNotificationFooters(EmailNotificationTestBase):
             payload for message, payload in self.get_messages()
             if message["to"] == expected_to]
         self.assertThat(payload, MatchesRegex(
-            r'.*To manage notifications about this bug go to:\n'
-            r'http://.*\+subscriptions\n'
-            r'\n'
-            r'Launchpad-Notification-Type: bug\n', re.S))
+            br'.*To manage notifications about this bug go to:\n'
+            br'http://.*\+subscriptions\n'
+            br'\n'
+            br'Launchpad-Notification-Type: bug\n', re.S))
 
 
 class TestDeferredNotifications(TestCaseWithFactory):
diff --git a/lib/lp/code/doc/branch-merge-proposal-notifications.txt b/lib/lp/code/doc/branch-merge-proposal-notifications.txt
index 1cf3c56..70d15f0 100644
--- a/lib/lp/code/doc/branch-merge-proposal-notifications.txt
+++ b/lib/lp/code/doc/branch-merge-proposal-notifications.txt
@@ -140,7 +140,7 @@ An email is sent to subscribers of either branch and the default reviewer.
     Subscriber
     >>> print(notification['X-Launchpad-Message-For'])
     source-subscriber
-    >>> print(notification.get_payload(decode=True))
+    >>> print(six.ensure_text(notification.get_payload(decode=True)))
     Eric has proposed merging
     lp://dev/~person-name...into lp://dev/~person-name...
     --
@@ -187,7 +187,8 @@ in the email.
     source@xxxxxxxxxxx, Subscriber, source-subscriber
     target@xxxxxxxxxxx, Subscriber, target-subscriber
     >>> notification = notifications[0]
-    >>> print(notification.get_payload()[0].get_payload(decode=True))
+    >>> print(six.ensure_text(
+    ...     notification.get_payload()[0].get_payload(decode=True)))
     Eric has proposed merging
     lp://dev/~person-name...into lp://dev/~person-name...
     <BLANKLINE>
diff --git a/lib/lp/code/doc/branch-notifications.txt b/lib/lp/code/doc/branch-notifications.txt
index 5869074..00696fe 100644
--- a/lib/lp/code/doc/branch-notifications.txt
+++ b/lib/lp/code/doc/branch-notifications.txt
@@ -62,7 +62,8 @@ also sends the email to the list of recipients.
     Subscriber
     >>> print(branch_notification['X-Launchpad-Message-For'])
     name12
-    >>> notification_body = branch_notification.get_payload(decode=True)
+    >>> notification_body = six.ensure_text(
+    ...     branch_notification.get_payload(decode=True))
     >>> print(notification_body) #doctest: -NORMALIZE_WHITESPACE
     The contents.
     <BLANKLINE>
@@ -196,15 +197,17 @@ to allow email filtering.
     # A helper function to print out the To header and
     # email body
     >>> def print_to_and_body(email):
-    ...     attachment = ''
+    ...     attachment = b''
     ...     if email.is_multipart():
     ...         root = email.get_payload()
     ...         body = root[0].get_payload(decode=True)
     ...         if len(root) > 1:
-    ...             attachment = '\n' + root[1].get_payload(decode=True)
+    ...             attachment = b'\n' + root[1].get_payload(decode=True)
     ...     else:
     ...         body = email.get_payload(decode=True)
-    ...     print('To: %s\n%s%s' % (email['To'], body, attachment))
+    ...     print('To: %s\n%s%s' % (
+    ...         email['To'], six.ensure_text(body),
+    ...         six.ensure_text(attachment)))
 
 We need to create some sufficiently large diffs to compare against.
 
diff --git a/lib/lp/code/doc/codeimport.txt b/lib/lp/code/doc/codeimport.txt
index bed3223..354df5e 100644
--- a/lib/lp/code/doc/codeimport.txt
+++ b/lib/lp/code/doc/codeimport.txt
@@ -111,7 +111,7 @@ three members of the vcs-imports team.
     Operator @vcs-imports
     >>> print(message['X-Launchpad-Message-For'])
     vcs-imports
-    >>> print(message.get_payload(decode=True))
+    >>> print(six.ensure_text(message.get_payload(decode=True)))
     A new CVS code import has been requested by Code Import Person:
         http://code.launchpad.test/~import-person/widget/trunk-cvs
     from
diff --git a/lib/lp/code/mail/tests/test_branch.py b/lib/lp/code/mail/tests/test_branch.py
index 9d6bdb2..69ed7dd 100644
--- a/lib/lp/code/mail/tests/test_branch.py
+++ b/lib/lp/code/mail/tests/test_branch.py
@@ -309,7 +309,7 @@ class TestBranchMailerDiffMixin:
         ctrl = self.makeBobMailController(diff=u'hello \u03A3')
         self.assertEqual(1, len(ctrl.attachments))
         diff = ctrl.attachments[0]
-        self.assertEqual('hello \xce\xa3', diff.get_payload(decode=True))
+        self.assertEqual(b'hello \xce\xa3', diff.get_payload(decode=True))
         self.assertEqual('text/x-diff; charset="utf-8"', diff['Content-type'])
         self.assertEqual('inline; filename="revision-diff.txt"',
                          diff['Content-disposition'])
diff --git a/lib/lp/code/mail/tests/test_branchmergeproposal.py b/lib/lp/code/mail/tests/test_branchmergeproposal.py
index 0a0fd9a..4bfd3db 100644
--- a/lib/lp/code/mail/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/mail/tests/test_branchmergeproposal.py
@@ -9,6 +9,7 @@ from textwrap import dedent
 
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
+import six
 import transaction
 from zope.interface import providedBy
 
@@ -375,7 +376,8 @@ class TestMergeProposalMailing(TestCaseWithFactory):
             'text/x-diff; charset="utf-8"', attachment['Content-Type'])
         self.assertEqual('inline; filename="review-diff.txt"',
                          attachment['Content-Disposition'])
-        self.assertEqual(diff_text, attachment.get_payload(decode=True))
+        self.assertEqual(
+            diff_text.encode('UTF-8'), attachment.get_payload(decode=True))
 
     def test_generateEmail_no_diff_for_status_only(self):
         """If the subscription is for status only, don't attach diffs."""
@@ -404,7 +406,9 @@ class TestMergeProposalMailing(TestCaseWithFactory):
             'text/x-diff; charset="utf-8"', attachment['Content-Type'])
         self.assertEqual('inline; filename="review-diff.txt"',
                          attachment['Content-Disposition'])
-        self.assertEqual(diff_text[:25], attachment.get_payload(decode=True))
+        self.assertEqual(
+            diff_text.encode('UTF-8')[:25],
+            attachment.get_payload(decode=True))
         warning_text = (
             "The attached diff has been truncated due to its size.\n")
         self.assertTrue(warning_text in ctrl.body)
@@ -544,7 +548,8 @@ class TestMergeProposalMailing(TestCaseWithFactory):
                 'source': bmp.source_branch.bzr_identity,
                 'target': bmp.target_branch.bzr_identity,
                 'bmp': canonical_url(bmp)}
-        self.assertEqual(expected, email.get_payload(decode=True))
+        self.assertEqual(
+            expected, six.ensure_text(email.get_payload(decode=True)))
 
     def assertRecipientsMatches(self, recipients, mailer):
         """Assert that `mailer` will send to the people in `recipients`."""
@@ -676,7 +681,8 @@ class TestBranchMergeProposalRequestReview(TestCaseWithFactory):
                 'source': bmp.source_branch.bzr_identity,
                 'target': bmp.target_branch.bzr_identity,
                 'bmp': canonical_url(bmp)})
-        self.assertEqual(expected, sent_mail.get_payload(decode=True))
+        self.assertEqual(
+            expected, six.ensure_text(sent_mail.get_payload(decode=True)))
 
     def test_nominateReview_emails_team_address(self):
         # If a review request is made for a team, the members of the team are
diff --git a/lib/lp/code/mail/tests/test_codehandler.py b/lib/lp/code/mail/tests/test_codehandler.py
index 97feda0..197ee49 100644
--- a/lib/lp/code/mail/tests/test_codehandler.py
+++ b/lib/lp/code/mail/tests/test_codehandler.py
@@ -199,7 +199,7 @@ class TestCodeHandler(TestCaseWithFactory):
         # the message, and the second is the original message.
         message, original = notification.get_payload()
         self.assertIn(
-            "There is no merge proposal at mp+0@xxxxxxxxxxxxxxxxxxx\n",
+            b"There is no merge proposal at mp+0@xxxxxxxxxxxxxxxxxxx\n",
             message.get_payload(decode=True))
 
     def test_processBadVote(self):
@@ -239,7 +239,7 @@ class TestCodeHandler(TestCaseWithFactory):
         --\x20
         For more information about using Launchpad by email, see
         https://help.launchpad.net/EmailInterface
-        or send an email to help@xxxxxxxxxxxxx"""),
+        or send an email to help@xxxxxxxxxxxxx""").encode("UTF-8"),
                                 message.get_payload(decode=True))
         self.assertEqual(mail['From'], notification['To'])
 
@@ -278,8 +278,8 @@ class TestCodeHandler(TestCaseWithFactory):
         # The returned message is a multipart message, the first part is
         # the message, and the second is the original message.
         message, original = notification.get_payload()
-        self.assertTrue(
-            "You are not a reviewer for the branch" in
+        self.assertIn(
+            b"You are not a reviewer for the branch",
             message.get_payload(decode=True))
 
     def test_processVote(self):
@@ -424,9 +424,9 @@ class TestCodeHandler(TestCaseWithFactory):
             notification['Subject'], 'Error Creating Merge Proposal')
         self.assertEqual(
             notification.get_payload(decode=True),
-            'Your message did not contain a subject.  Launchpad code '
-            'reviews require all\nemails to contain subject lines.  '
-            'Please re-send your email including the\nsubject line.\n\n')
+            b'Your message did not contain a subject.  Launchpad code '
+            b'reviews require all\nemails to contain subject lines.  '
+            b'Please re-send your email including the\nsubject line.\n\n')
         self.assertEqual(notification['to'],
             mail['from'])
         self.assertEqual(0, bmp.all_comments.count())
diff --git a/lib/lp/code/mail/tests/test_codeimport.py b/lib/lp/code/mail/tests/test_codeimport.py
index a155823..e82f466 100644
--- a/lib/lp/code/mail/tests/test_codeimport.py
+++ b/lib/lp/code/mail/tests/test_codeimport.py
@@ -8,6 +8,7 @@ from __future__ import absolute_import, print_function, unicode_literals
 from email import message_from_string
 import textwrap
 
+import six
 import transaction
 
 from lp.code.enums import (
@@ -51,7 +52,8 @@ class TestNewCodeImports(TestCaseWithFactory):
             '    :pserver:anonymouse@xxxxxxxxxxxxxxx:/cvsroot, a_module\n'
             '\n'
             '-- \nYou are getting this email because you are a member of the '
-            'vcs-imports team.\n', msg.get_payload(decode=True))
+            'vcs-imports team.\n',
+            six.ensure_text(msg.get_payload(decode=True)))
 
     def test_svn_to_bzr_import(self):
         # Test the email for a new Subversion-to-Bazaar import.
@@ -74,7 +76,8 @@ class TestNewCodeImports(TestCaseWithFactory):
             '    svn://svn.example.com/fooix/trunk\n'
             '\n'
             '-- \nYou are getting this email because you are a member of the '
-            'vcs-imports team.\n', msg.get_payload(decode=True))
+            'vcs-imports team.\n',
+            six.ensure_text(msg.get_payload(decode=True)))
 
     def test_git_to_bzr_import(self):
         # Test the email for a new git-to-Bazaar import.
@@ -97,7 +100,8 @@ class TestNewCodeImports(TestCaseWithFactory):
             '    git://git.example.com/fooix.git\n'
             '\n'
             '-- \nYou are getting this email because you are a member of the '
-            'vcs-imports team.\n', msg.get_payload(decode=True))
+            'vcs-imports team.\n',
+            six.ensure_text(msg.get_payload(decode=True)))
 
     def test_git_to_git_import(self):
         # Test the email for a new git-to-git import.
@@ -122,7 +126,8 @@ class TestNewCodeImports(TestCaseWithFactory):
             '    git://git.example.com/fooix.git\n'
             '\n'
             '-- \nYou are getting this email because you are a member of the '
-            'vcs-imports team.\n', msg.get_payload(decode=True))
+            'vcs-imports team.\n',
+            six.ensure_text(msg.get_payload(decode=True)))
 
     def test_new_source_package_import(self):
         # Test the email for a new sourcepackage import.
@@ -150,7 +155,8 @@ class TestNewCodeImports(TestCaseWithFactory):
             '    git://git.example.com/fooix.git\n'
             '\n'
             '-- \nYou are getting this email because you are a member of the '
-            'vcs-imports team.\n', msg.get_payload(decode=True))
+            'vcs-imports team.\n',
+            six.ensure_text(msg.get_payload(decode=True)))
 
 
 class TestUpdatedCodeImports(TestCaseWithFactory):
@@ -174,7 +180,7 @@ class TestUpdatedCodeImports(TestCaseWithFactory):
                 'details': details,
                 'unique_name': unique_name,
                 },
-            msg.get_payload(decode=True))
+            six.ensure_text(msg.get_payload(decode=True)))
 
     def assertDifferentDetailsEmail(self, old_details, new_details,
                                     unique_name):
@@ -198,7 +204,7 @@ class TestUpdatedCodeImports(TestCaseWithFactory):
                 'new_details': new_details,
                 'unique_name': unique_name,
                 },
-            msg.get_payload(decode=True))
+            six.ensure_text(msg.get_payload(decode=True)))
 
     def test_cvs_to_bzr_import_same_details(self):
         code_import = self.factory.makeProductCodeImport(
diff --git a/lib/lp/code/mail/tests/test_codereviewcomment.py b/lib/lp/code/mail/tests/test_codereviewcomment.py
index 712a55f..7d4ec13 100644
--- a/lib/lp/code/mail/tests/test_codereviewcomment.py
+++ b/lib/lp/code/mail/tests/test_codereviewcomment.py
@@ -351,7 +351,7 @@ class TestCodeReviewComment(TestCaseWithFactory):
             person.preferredemail.email, person).makeMessage()
         attachment = message.get_payload()[1]
         self.assertEqual(
-            'This is a diff.', attachment.get_payload(decode=True))
+            b'This is a diff.', attachment.get_payload(decode=True))
 
     def makeCommentAndParticipants(self):
         """Create a merge proposal and comment.
diff --git a/lib/lp/code/model/tests/test_branchjob.py b/lib/lp/code/model/tests/test_branchjob.py
index 821e3a1..2abb78a 100644
--- a/lib/lp/code/model/tests/test_branchjob.py
+++ b/lib/lp/code/model/tests/test_branchjob.py
@@ -18,6 +18,7 @@ from breezy.revision import NULL_REVISION
 from breezy.transport import get_transport
 from fixtures import MockPatch
 import pytz
+import six
 from storm.locals import Store
 import transaction
 from zope.component import getUtility
@@ -332,7 +333,8 @@ class TestBranchUpgradeJob(TestCaseWithFactory):
         (mail,) = pop_notifications()
         self.assertEqual(
             'Launchpad error while upgrading a branch', mail['subject'])
-        self.assertIn('Not a branch', mail.get_payload(decode=True))
+        self.assertIn(
+            'Not a branch', six.ensure_text(mail.get_payload(decode=True)))
 
 
 class TestRevisionMailJob(TestCaseWithFactory):
@@ -384,7 +386,7 @@ class TestRevisionMailJob(TestCaseWithFactory):
                 'url': canonical_url(branch),
                 'identity': branch.bzr_identity,
                 },
-            mail.get_payload(decode=True))
+            six.ensure_text(mail.get_payload(decode=True)))
 
     def test_revno_string(self):
         """Ensure that revnos can be strings."""
diff --git a/lib/lp/code/model/tests/test_branchmergeproposaljobs.py b/lib/lp/code/model/tests/test_branchmergeproposaljobs.py
index c2bed7f..7236b18 100644
--- a/lib/lp/code/model/tests/test_branchmergeproposaljobs.py
+++ b/lib/lp/code/model/tests/test_branchmergeproposaljobs.py
@@ -348,7 +348,7 @@ class TestUpdatePreviewDiffJob(DiffTestCase):
             'The source branch of http://code.launchpad.test/~%s/%s/%s/'
             '+merge/%d has no revisions.' % (
                 branch.owner.name, branch.target.name, branch.name, bmp.id),
-            email.get_payload(decode=True))
+            six.ensure_text(email.get_payload(decode=True)))
 
     def test_run_branches_pending_writes(self):
         """If the branches are being written, we retry but don't complain."""
@@ -738,7 +738,7 @@ class TestReviewRequestedEmailJob(TestCaseWithFactory):
         (notification,) = pop_notifications()
         self.assertIn(
             'You have been requested to review the proposed merge',
-            notification.get_payload(decode=True))
+            six.ensure_text(notification.get_payload(decode=True)))
 
 
 class TestMergeProposalUpdatedEmailJob(TestCaseWithFactory):
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index ce52929..df40171 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -1297,7 +1297,7 @@ class TestGitRepositoryModificationNotifications(TestCaseWithFactory):
         for from_addr, to_addrs, message in stub.test_emails:
             body = email.message_from_string(message).get_payload(decode=True)
             for to_addr in to_addrs:
-                bodies_by_recipient[to_addr] = body
+                bodies_by_recipient[to_addr] = six.ensure_text(body)
         # Both the owner and the unprivileged subscriber receive email.
         self.assertContentEqual(
             [owner_address, subscriber_address], bodies_by_recipient.keys())
@@ -2876,7 +2876,7 @@ class TestGitRepositoryDetectMerges(TestCaseWithFactory):
         notifications = pop_notifications()
         self.assertIn(
             "Work in progress => Merged",
-            notifications[0].get_payload(decode=True))
+            notifications[0].get_payload(decode=True).decode("UTF-8"))
         self.assertEqual(
             config.canonical.noreply_from_address, notifications[0]["From"])
         recipients = set(msg["x-envelope-to"] for msg in notifications)
diff --git a/lib/lp/codehosting/scanner/tests/test_email.py b/lib/lp/codehosting/scanner/tests/test_email.py
index 3148e8b..26ab55f 100644
--- a/lib/lp/codehosting/scanner/tests/test_email.py
+++ b/lib/lp/codehosting/scanner/tests/test_email.py
@@ -133,7 +133,7 @@ class TestBzrSyncEmail(BzrSyncTestCase):
         self.assertEqual(len(stub.test_emails), 2)
         [recommit_email, uncommit_email] = stub.test_emails
         uncommit_email_body = uncommit_email[2]
-        expected = '1 revision was removed from the branch.'
+        expected = b'1 revision was removed from the branch.'
         self.assertIn(expected, uncommit_email_body)
         subject = (
             'Subject: [Branch %s] Test branch' % self.db_branch.unique_name)
@@ -145,11 +145,11 @@ class TestBzrSyncEmail(BzrSyncTestCase):
         subject = '[Branch %s] Rev 1: second' % self.db_branch.unique_name
         self.assertEmailHeadersEqual(subject, recommit_email_msg['Subject'])
         body_bits = [
-            'revno: 1',
-            'committer: %s' % author,
-            'branch nick: %s' % self.bzr_branch.nick,
-            'message:\n  second',
-            'added:\n  hello.txt',
+            b'revno: 1',
+            ('committer: %s' % author).encode('UTF-8'),
+            ('branch nick: %s' % self.bzr_branch.nick).encode('UTF-8'),
+            b'message:\n  second',
+            b'added:\n  hello.txt',
             ]
         for bit in body_bits:
             self.assertIn(bit, recommit_email_body)
diff --git a/lib/lp/codehosting/scanner/tests/test_mergedetection.py b/lib/lp/codehosting/scanner/tests/test_mergedetection.py
index 34202df..3868865 100644
--- a/lib/lp/codehosting/scanner/tests/test_mergedetection.py
+++ b/lib/lp/codehosting/scanner/tests/test_mergedetection.py
@@ -11,6 +11,7 @@ import logging
 
 from breezy.revision import NULL_REVISION
 from lazr.lifecycle.event import ObjectModifiedEvent
+import six
 import transaction
 from zope.component import getUtility
 from zope.event import notify
@@ -298,8 +299,9 @@ class TestBranchMergeDetectionHandler(TestCaseWithFactory):
         derived_job = job.makeDerived()
         derived_job.run()
         notifications = pop_notifications()
-        self.assertIn('Work in progress => Merged',
-                      notifications[0].get_payload(decode=True))
+        self.assertIn(
+            'Work in progress => Merged',
+            six.ensure_text(notifications[0].get_payload(decode=True)))
         self.assertEqual(
             config.canonical.noreply_from_address, notifications[0]['From'])
         recipients = set(msg['x-envelope-to'] for msg in notifications)
diff --git a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
index 950706d..cb24bd7 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
@@ -62,7 +62,7 @@ followed by ASCII armored encrypted confirmation instructions.  Ensure that
 the clear text instructions contain the expected URLs pointing to more help.
 
     >>> cipher_body = msg.get_payload(decode=True)
-    >>> print(cipher_body)
+    >>> print(six.ensure_text(cipher_body))
     Hello,
     <BLANKLINE>
     This message contains the instructions for confirming registration of an
@@ -182,13 +182,13 @@ Sample Person checks their email.
 The email is not encrypted, since Sample Person didn't claim the
 ability to decrypt text with this key.
 
-    >>> '-----BEGIN PGP MESSAGE-----' in body
+    >>> b'-----BEGIN PGP MESSAGE-----' in body
     False
 
 The email does contain some information about the key, and a token URL
 Sample Person should visit to verify their ownership of the key.
 
-    >>> print(body)
+    >>> print(six.ensure_text(body))
     <BLANKLINE>
     Hello,
     ...
@@ -210,7 +210,8 @@ text which includes the date the token was generated (to avoid replay
 attacks). To make this testable, we set the creation date of this
 token to a fixed value:
 
-    >>> nothing, token_value = token_url.split('http://launchpad.test/token/')
+    >>> token_value = token_url.split(
+    ...     'http://launchpad.test/token/')[1].encode('ASCII')
 
     >>> import datetime, hashlib, pytz
     >>> from lp.services.verification.model.logintoken import LoginToken
@@ -622,7 +623,7 @@ Test if the advertisement email was sent:
     >>> from lp.services.mail import stub
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
     >>> msg = email.message_from_string(raw_msg)
-    >>> print(msg.get_payload(decode=True))
+    >>> print(six.ensure_text(msg.get_payload(decode=True)))
     <BLANKLINE>
     ...
     User: 'Mark Shuttleworth'
diff --git a/lib/lp/registry/tests/test_codeofconduct.py b/lib/lp/registry/tests/test_codeofconduct.py
index 5472c05..b505254 100644
--- a/lib/lp/registry/tests/test_codeofconduct.py
+++ b/lib/lp/registry/tests/test_codeofconduct.py
@@ -191,4 +191,4 @@ class TestSignedCodeOfConductSet(TestCaseWithFactory):
                     'user': user.display_name,
                     'fingerprint': gpgkey.fingerprint,
                     },
-            notification.get_payload(decode=True))
+            notification.get_payload(decode=True).decode("UTF-8"))
diff --git a/lib/lp/services/job/tests/test_runner.py b/lib/lp/services/job/tests/test_runner.py
index 8981b66..36842b9 100644
--- a/lib/lp/services/job/tests/test_runner.py
+++ b/lib/lp/services/job/tests/test_runner.py
@@ -290,9 +290,10 @@ class TestJobRunner(StatsMixin, TestCaseWithFactory):
             'Launchpad encountered an internal error during the following'
             ' operation: appending a string to a list.  It was logged with id'
             ' %s.  Sorry for the inconvenience.' % oops['id'],
-            notification.get_payload(decode=True))
-        self.assertNotIn('Fake exception.  Foobar, I say!',
-                         notification.get_payload(decode=True))
+            notification.get_payload(decode=True).decode('UTF-8'))
+        self.assertNotIn(
+            'Fake exception.  Foobar, I say!',
+            notification.get_payload(decode=True).decode('UTF-8'))
         self.assertEqual('Launchpad internal error', notification['subject'])
 
     def test_runAll_mails_user_errors(self):
@@ -317,7 +318,7 @@ class TestJobRunner(StatsMixin, TestCaseWithFactory):
         self.assertEqual([], self.oopses)
         notifications = pop_notifications()
         self.assertEqual(1, len(notifications))
-        body = notifications[0].get_payload(decode=True)
+        body = notifications[0].get_payload(decode=True).decode('UTF-8')
         self.assertEqual(
             'Launchpad encountered an error during the following operation:'
             ' appending a string to a list.  Fake exception.  Foobar, I say!',
diff --git a/lib/lp/services/mail/doc/sending-mail.txt b/lib/lp/services/mail/doc/sending-mail.txt
index 88bf4c9..428f07a 100644
--- a/lib/lp/services/mail/doc/sending-mail.txt
+++ b/lib/lp/services/mail/doc/sending-mail.txt
@@ -27,8 +27,8 @@ Now let's look at the sent email:
     'foo.bar@xxxxxxxxxxxxx'
     >>> msg['Subject']
     'Subject'
-    >>> msg.get_payload(decode=True)
-    'Content'
+    >>> print(six.ensure_text(msg.get_payload(decode=True)))
+    Content
     >>> # Make sure bulk headers are set for vacation programs
     >>> msg['Precedence']
     'bulk'
@@ -57,8 +57,8 @@ the person's name is encoded properly.
     'Foo Bar <foo.bar@xxxxxxxxxxxxx>'
     >>> msg['Subject']
     'Subject'
-    >>> msg.get_payload(decode=True)
-    'Content'
+    >>> print(six.ensure_text(msg.get_payload(decode=True)))
+    Content
     >>> msg['Precedence']
     'bulk'
 
@@ -249,8 +249,8 @@ that the precedence header was not added.
     'feedback@xxxxxxxxxxxxx'
     >>> msg['Subject']
     'Forgot password'
-    >>> msg.get_payload(decode=True)
-    'Content'
+    >>> print(six.ensure_text(msg.get_payload(decode=True)))
+    Content
     >>> print(msg['Precedence'])
     None
 
diff --git a/lib/lp/services/mail/tests/test_basemailer.py b/lib/lp/services/mail/tests/test_basemailer.py
index ac0fd62..6e6cb05 100644
--- a/lib/lp/services/mail/tests/test_basemailer.py
+++ b/lib/lp/services/mail/tests/test_basemailer.py
@@ -233,15 +233,15 @@ class TestBaseMailer(TestCaseWithFactory):
         good_parts = good.get_payload()
         self.assertEqual(3, len(good_parts))
         self.assertEqual(
-            'attachment1', good_parts[1].get_payload(decode=True))
+            b'attachment1', good_parts[1].get_payload(decode=True))
         self.assertEqual(
-            'attachment2', good_parts[2].get_payload(decode=True))
+            b'attachment2', good_parts[2].get_payload(decode=True))
         # The bad email has the normal attachments stripped off and replaced
         # with the text.
         bad_parts = bad.get_payload()
         self.assertEqual(2, len(bad_parts))
         self.assertEqual(
-            'Excessively large attachments removed.',
+            b'Excessively large attachments removed.',
             bad_parts[1].get_payload(decode=True))
         # And no OOPS is logged.
         self.assertEqual(0, len(self.oopses))
diff --git a/lib/lp/services/mail/tests/test_incoming.py b/lib/lp/services/mail/tests/test_incoming.py
index 3d2e625..a9d615a 100644
--- a/lib/lp/services/mail/tests/test_incoming.py
+++ b/lib/lp/services/mail/tests/test_incoming.py
@@ -7,6 +7,7 @@ import logging
 import os
 import unittest
 
+import six
 from testtools.matchers import (
     Equals,
     Is,
@@ -110,7 +111,8 @@ class IncomingTestCase(TestCaseWithFactory):
         handleMail()
         self.assertEqual([], self.oopses)
         [notification] = pop_notifications()
-        body = notification.get_payload()[0].get_payload(decode=True)
+        body = six.ensure_text(
+            notification.get_payload()[0].get_payload(decode=True))
         self.assertIn(
             "An error occurred while processing a mail you sent to "
             "Launchpad's email\ninterface.\n\n\n"
@@ -143,7 +145,8 @@ class IncomingTestCase(TestCaseWithFactory):
         handleMail()
         self.assertEqual([], self.oopses)
         [notification] = pop_notifications()
-        body = notification.get_payload()[0].get_payload(decode=True)
+        body = six.ensure_text(
+            notification.get_payload()[0].get_payload(decode=True))
         self.assertIn(
             "An error occurred while processing a mail you sent to "
             "Launchpad's email\ninterface.\n\n\n"
@@ -176,7 +179,8 @@ class IncomingTestCase(TestCaseWithFactory):
         handleMail()
         self.assertEqual([], self.oopses)
         [notification] = pop_notifications()
-        body = notification.get_payload()[0].get_payload(decode=True)
+        body = six.ensure_text(
+            notification.get_payload()[0].get_payload(decode=True))
         self.assertIn(
             "An error occurred while processing a mail you sent to "
             "Launchpad's email\ninterface.\n\n\n"
@@ -202,7 +206,8 @@ class IncomingTestCase(TestCaseWithFactory):
         handleMail()
         self.assertEqual([], self.oopses)
         [notification] = pop_notifications()
-        body = notification.get_payload()[0].get_payload(decode=True)
+        body = six.ensure_text(
+            notification.get_payload()[0].get_payload(decode=True))
         self.assertIn("The mail you sent to Launchpad is too long.", body)
         self.assertIn("was 55 MB and the limit is 10 MB.", body)
 
diff --git a/lib/lp/services/verification/tests/test_logintoken.py b/lib/lp/services/verification/tests/test_logintoken.py
index 14cedac..dcde2fa 100644
--- a/lib/lp/services/verification/tests/test_logintoken.py
+++ b/lib/lp/services/verification/tests/test_logintoken.py
@@ -8,6 +8,7 @@ __metaclass__ = type
 import doctest
 from textwrap import dedent
 
+import six
 from testtools.matchers import DocTestMatches
 from zope.component import getUtility
 
@@ -77,4 +78,6 @@ class TestLoginToken(TestCaseWithFactory):
             """)
         expected_matcher = DocTestMatches(
             expected_message, doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE)
-        self.assertThat(message.get_payload(decode=True), expected_matcher)
+        self.assertThat(
+            six.ensure_text(message.get_payload(decode=True)),
+            expected_matcher)
diff --git a/lib/lp/snappy/tests/test_snapbuild.py b/lib/lp/snappy/tests/test_snapbuild.py
index 77bdac0..e5dda05 100644
--- a/lib/lp/snappy/tests/test_snapbuild.py
+++ b/lib/lp/snappy/tests/test_snapbuild.py
@@ -15,6 +15,7 @@ from datetime import (
 from fixtures import FakeLogger
 from pymacaroons import Macaroon
 import pytz
+import six
 from six.moves.urllib.request import urlopen
 from testtools.matchers import (
     ContainsDict,
@@ -461,7 +462,8 @@ class TestSnapBuild(TestCaseWithFactory):
             notification["X-Launchpad-Notification-Type"])
         self.assertEqual(
             "FAILEDTOBUILD", notification["X-Launchpad-Build-State"])
-        body, footer = notification.get_payload(decode=True).split("\n-- \n")
+        body, footer = six.ensure_text(
+            notification.get_payload(decode=True)).split("\n-- \n")
         self.assertEqual(expected_body % (build.log_url, ""), body)
         self.assertEqual(
             "http://launchpad.test/~person/+snap/snap-1/+build/%d\n";
diff --git a/lib/lp/snappy/tests/test_snapbuildjob.py b/lib/lp/snappy/tests/test_snapbuildjob.py
index 90bb9db..87a5528 100644
--- a/lib/lp/snappy/tests/test_snapbuildjob.py
+++ b/lib/lp/snappy/tests/test_snapbuildjob.py
@@ -10,6 +10,7 @@ __metaclass__ = type
 from datetime import timedelta
 
 from fixtures import FakeLogger
+import six
 from testtools.matchers import (
     Equals,
     Is,
@@ -243,7 +244,8 @@ class TestSnapStoreUploadJob(TestCaseWithFactory):
         self.assertEqual(
             "snap-build-upload-unauthorized",
             notification["X-Launchpad-Notification-Type"])
-        body, footer = notification.get_payload(decode=True).split("\n-- \n")
+        body, footer = six.ensure_text(
+            notification.get_payload(decode=True)).split("\n-- \n")
         self.assertIn(
             "http://launchpad.test/~requester-team/+snap/test-snap/+authorize";,
             body)
@@ -335,7 +337,8 @@ class TestSnapStoreUploadJob(TestCaseWithFactory):
         self.assertEqual(
             "snap-build-upload-refresh-failed",
             notification["X-Launchpad-Notification-Type"])
-        body, footer = notification.get_payload(decode=True).split("\n-- \n")
+        body, footer = six.ensure_text(
+            notification.get_payload(decode=True)).split("\n-- \n")
         self.assertIn(
             "http://launchpad.test/~requester-team/+snap/test-snap/+authorize";,
             body)
@@ -385,7 +388,8 @@ class TestSnapStoreUploadJob(TestCaseWithFactory):
         self.assertEqual(
             "snap-build-upload-failed",
             notification["X-Launchpad-Notification-Type"])
-        body, footer = notification.get_payload(decode=True).split("\n-- \n")
+        body, footer = six.ensure_text(
+            notification.get_payload(decode=True)).split("\n-- \n")
         self.assertIn("Failed to upload", body)
         build_url = (
             "http://launchpad.test/~requester-team/+snap/test-snap/+build/%d"; %
@@ -488,7 +492,8 @@ class TestSnapStoreUploadJob(TestCaseWithFactory):
         self.assertEqual(
             "snap-build-upload-scan-failed",
             notification["X-Launchpad-Notification-Type"])
-        body, footer = notification.get_payload(decode=True).split("\n-- \n")
+        body, footer = six.ensure_text(
+            notification.get_payload(decode=True)).split("\n-- \n")
         self.assertIn("Scan failed.", body)
         self.assertEqual(
             "http://launchpad.test/~requester-team/+snap/test-snap/+build/%d\n";
diff --git a/lib/lp/snappy/tests/test_snapjob.py b/lib/lp/snappy/tests/test_snapjob.py
index 00130b5..7eea223 100644
--- a/lib/lp/snappy/tests/test_snapjob.py
+++ b/lib/lp/snappy/tests/test_snapjob.py
@@ -9,6 +9,7 @@ __metaclass__ = type
 
 from textwrap import dedent
 
+import six
 from testtools.matchers import (
     AfterPreprocessing,
     ContainsDict,
@@ -201,7 +202,7 @@ class TestSnapRequestBuildsJob(TestCaseWithFactory):
         self.assertEqual(
             "Launchpad encountered an error during the following operation: "
             "requesting builds of %s.  Nonsense on stilts" % snap.name,
-            notification.get_payload(decode=True))
+            six.ensure_text(notification.get_payload(decode=True)))
         self.assertThat(job, MatchesStructure(
             job=MatchesStructure.byEquality(status=JobStatus.FAILED),
             date_created=Equals(expected_date_created),
@@ -235,7 +236,7 @@ class TestSnapRequestBuildsJob(TestCaseWithFactory):
             "Launchpad encountered an error during the following operation: "
             "requesting builds of %s.  No such base: "
             "'nonexistent'." % snap.name,
-            notification.get_payload(decode=True))
+            six.ensure_text(notification.get_payload(decode=True)))
         self.assertThat(job, MatchesStructure(
             job=MatchesStructure.byEquality(status=JobStatus.FAILED),
             date_created=Equals(expected_date_created),
diff --git a/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt b/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt
index 18e1b08..787a70e 100644
--- a/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt
+++ b/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt
@@ -78,7 +78,8 @@ Note that the generated notification contain the 'extra_info' content:
   >>> build_notification['X-Creator-Recipient']
   'mark@xxxxxxxxxxx'
 
-  >>> notification_body = build_notification.get_payload(decode=True)
+  >>> notification_body = six.ensure_text(
+  ...     build_notification.get_payload(decode=True))
   >>> print(notification_body) #doctest: -NORMALIZE_WHITESPACE
   <BLANKLINE>
    * Source Package: cdrkit
diff --git a/lib/lp/soyuz/mail/tests/test_packageupload.py b/lib/lp/soyuz/mail/tests/test_packageupload.py
index 4b1bd3a..7881462 100644
--- a/lib/lp/soyuz/mail/tests/test_packageupload.py
+++ b/lib/lp/soyuz/mail/tests/test_packageupload.py
@@ -7,6 +7,7 @@
 
 from textwrap import dedent
 
+import six
 from testtools.matchers import (
     Contains,
     ContainsDict,
@@ -66,9 +67,9 @@ class TestNotificationRequiringLibrarian(TestCaseWithFactory):
         notifications = pop_notifications()
         self.assertEqual(2, len(notifications))
         msg = notifications[1].get_payload(0)
-        body = msg.get_payload(decode=True)
-        self.assertIn("Changed-By: Loïc", body)
-        self.assertIn("Signed-By: Stéphane", body)
+        body = six.ensure_text(msg.get_payload(decode=True))
+        self.assertIn(u"Changed-By: Loïc", body)
+        self.assertIn(u"Signed-By: Stéphane", body)
 
     def test_calculate_subject_customfile(self):
         lfa = self.factory.makeLibraryFileAlias()
@@ -225,7 +226,7 @@ class TestNotificationRequiringLibrarian(TestCaseWithFactory):
             summary_text="Rejected by archive administrator.")
         mailer.sendAll()
         [notification] = pop_notifications()
-        body = notification.get_payload(decode=True)
+        body = six.ensure_text(notification.get_payload(decode=True))
         self.assertEqual('Blamer <blamer@xxxxxxxxxxx>', notification['To'])
         expected_body = dedent("""\
             Rejected:
diff --git a/lib/lp/soyuz/scripts/tests/test_copypackage.py b/lib/lp/soyuz/scripts/tests/test_copypackage.py
index 6a99db9..8fdf14e 100644
--- a/lib/lp/soyuz/scripts/tests/test_copypackage.py
+++ b/lib/lp/soyuz/scripts/tests/test_copypackage.py
@@ -7,6 +7,7 @@ import datetime
 from textwrap import dedent
 
 import pytz
+import six
 from testtools.content import text_content
 from testtools.matchers import (
     Equals,
@@ -1454,7 +1455,7 @@ class TestDoDirectCopy(BaseDoCopyTests, TestCaseWithFactory):
         [notification] = pop_notifications()
         self.assertEqual(
             target_archive.reference, notification['X-Launchpad-Archive'])
-        body = notification.get_payload(decode=True)
+        body = six.ensure_text(notification.get_payload(decode=True))
         expected = dedent("""\
             Accepted:
              OK: foo_1.0-2.dsc
diff --git a/lib/lp/soyuz/tests/test_build_notify.py b/lib/lp/soyuz/tests/test_build_notify.py
index 3c27aec..2823531 100644
--- a/lib/lp/soyuz/tests/test_build_notify.py
+++ b/lib/lp/soyuz/tests/test_build_notify.py
@@ -183,7 +183,7 @@ class TestBuildNotify(TestCaseWithFactory):
             build.archive.reference, build.status.title, duration, build_log,
             builder, source, "-- ", build.title, canonical_url(build)))
         expected_body += "\n" + REASONS[reason] + "\n"
-        self.assertEqual(expected_body, body)
+        self.assertEqual(expected_body.encode("UTF-8"), body)
 
     def _assert_mails_are_correct(self, build, reasons, ppa=False):
         notifications = pop_notifications()
diff --git a/lib/lp/soyuz/tests/test_livefsbuild.py b/lib/lp/soyuz/tests/test_livefsbuild.py
index e7436f1..8d71dd7 100644
--- a/lib/lp/soyuz/tests/test_livefsbuild.py
+++ b/lib/lp/soyuz/tests/test_livefsbuild.py
@@ -340,7 +340,8 @@ class TestLiveFSBuild(TestCaseWithFactory):
             notification["X-Launchpad-Notification-Type"])
         self.assertEqual(
             "FAILEDTOBUILD", notification["X-Launchpad-Build-State"])
-        body, footer = notification.get_payload(decode=True).split("\n-- \n")
+        body, footer = notification.get_payload(decode=True).decode(
+            "UTF-8").split("\n-- \n")
         self.assertEqual(expected_body % (build.log_url, ""), body)
         self.assertEqual(
             "http://launchpad.test/~person/+livefs/distro/unstable/livefs-1/";
diff --git a/lib/lp/testing/mail_helpers.py b/lib/lp/testing/mail_helpers.py
index 1b405e3..1bed4dc 100644
--- a/lib/lp/testing/mail_helpers.py
+++ b/lib/lp/testing/mail_helpers.py
@@ -9,6 +9,7 @@ __metaclass__ = type
 
 import operator
 
+import six
 import transaction
 from zope.component import getUtility
 
@@ -119,7 +120,7 @@ def print_emails(include_reply_to=False, group_similar=False,
             print('%s: %s' % (
                 notification_type_header, message[notification_type_header]))
         print('Subject:', message['Subject'])
-        print(body)
+        print(six.ensure_text(body))
         print("-" * 40)
 
 
diff --git a/lib/lp/translations/doc/pofile-verify-stats.txt b/lib/lp/translations/doc/pofile-verify-stats.txt
index 71fb3f4..d2299da 100644
--- a/lib/lp/translations/doc/pofile-verify-stats.txt
+++ b/lib/lp/translations/doc/pofile-verify-stats.txt
@@ -105,7 +105,7 @@ The Translations administrators also receive an email about the error.
     >>> to_addrs
     ['launchpad-error-reports@xxxxxxxxxxxxxxxxxxx']
     >>> in_header = True
-    >>> for line in body.splitlines():
+    >>> for line in body.decode('UTF-8').splitlines():
     ...     if in_header:
     ...         in_header = (line != '')
     ...     else: