← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-more-message-bytes into launchpad:master

 

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

Commit message:
Use message_from_bytes/message_as_bytes where appropriate

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

This fixes a number of test failures on Python 3, where we generally want to handle raw email messages as bytes.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-more-message-bytes into launchpad:master.
diff --git a/lib/lp/blueprints/doc/specification-notifications.txt b/lib/lp/blueprints/doc/specification-notifications.txt
index 275dbd9..7eb04ab 100644
--- a/lib/lp/blueprints/doc/specification-notifications.txt
+++ b/lib/lp/blueprints/doc/specification-notifications.txt
@@ -116,9 +116,9 @@ are now subscribed.
 
 Now let's take a look at what the notification looks like:
 
-    >>> import email
+    >>> from lp.services.compat import message_from_bytes
     >>> notifications = [
-    ...     email.message_from_string(raw_message)
+    ...     message_from_bytes(raw_message)
     ...     for from_addr, to_addrs, raw_message in sorted(stub.test_emails)]
     >>> status_notification = notifications[0]
     >>> status_notification['To']
@@ -158,7 +158,7 @@ Whiteboard change:
     >>> transaction.commit()
 
     >>> notifications = [
-    ...     email.message_from_string(raw_message)
+    ...     message_from_bytes(raw_message)
     ...     for from_addr, to_addrs, raw_message in sorted(stub.test_emails)]
     >>> status_notification = notifications[0]
     >>> status_notification['To']
@@ -198,7 +198,7 @@ Definition status and whiteboard change:
     >>> transaction.commit()
 
     >>> notifications = [
-    ...     email.message_from_string(raw_message)
+    ...     message_from_bytes(raw_message)
     ...     for from_addr, to_addrs, raw_message in sorted(stub.test_emails)]
     >>> status_notification = notifications[0]
     >>> status_notification['To']
@@ -238,7 +238,7 @@ Change priority:
     >>> transaction.commit()
 
     >>> notifications = [
-    ...     email.message_from_string(raw_message)
+    ...     message_from_bytes(raw_message)
     ...     for from_addr, to_addrs, raw_message in sorted(stub.test_emails)]
     >>> status_notification = notifications[0]
     >>> status_notification['To']
@@ -272,7 +272,7 @@ Change approver, assignee and drafter:
     >>> transaction.commit()
 
     >>> notifications = [
-    ...     email.message_from_string(raw_message)
+    ...     message_from_bytes(raw_message)
     ...     for from_addr, to_addrs, raw_message in sorted(stub.test_emails)]
     >>> status_notification = notifications[0]
     >>> status_notification['To']
diff --git a/lib/lp/bugs/doc/externalbugtracker-debbugs.txt b/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
index f81a0d2..9d97e70 100644
--- a/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
@@ -306,10 +306,10 @@ report.
     >>> print(report['From'])
     Moritz Muehlenhoff <jmm@xxxxxxxxxx>
 
-    >>> name, email = external_debbugs.getBugReporter('322535')
+    >>> name, address = external_debbugs.getBugReporter('322535')
     >>> print(name)
     Moritz Muehlenhoff
-    >>> print(email)
+    >>> print(address)
     jmm@xxxxxxxxxx
 
 The getBugSummaryAndDescription method reads the bug report from the
@@ -418,10 +418,10 @@ same comment.
 If we query the DebBugs database directly we'll see that there are two
 copies of the same comment.
 
-    >>> import email
+    >>> from lp.services.compat import message_from_bytes
     >>> debian_bug = external_debbugs._findBug(bug_watch.remotebug)
     >>> for comment in debian_bug.comments:
-    ...     comment_email = email.message_from_string(comment)
+    ...     comment_email = message_from_bytes(comment)
     ...     print(comment_email['message-id'])
     <20040309081430.98BF411EE67@tux>
     <20040309081430.98BF411EE67@tux>
diff --git a/lib/lp/bugs/doc/initial-bug-contacts.txt b/lib/lp/bugs/doc/initial-bug-contacts.txt
index ed95d04..68e66c0 100644
--- a/lib/lp/bugs/doc/initial-bug-contacts.txt
+++ b/lib/lp/bugs/doc/initial-bug-contacts.txt
@@ -169,8 +169,8 @@ notification). The email has the X-Launchpad-Message-Rationale header to
 track why daf received the email. The rational is repeated in the footer
 of the email with the bug title and URL.
 
-    >>> import email
     >>> from operator import itemgetter
+    >>> from lp.services.compat import message_from_bytes
 
     >>> test_emails = list(stub.test_emails)
     >>> test_emails.sort(key=itemgetter(1))
@@ -185,7 +185,7 @@ of the email with the bug title and URL.
     >>> print(to_addr)
     ['daf@xxxxxxxxxxxxx']
 
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg['References'] == (
     ...        bug_one_in_ubuntu_firefox.bug.initial_message.rfc822msgid)
     True
diff --git a/lib/lp/bugs/externalbugtracker/debbugs.py b/lib/lp/bugs/externalbugtracker/debbugs.py
index 404b647..360c2b5 100644
--- a/lib/lp/bugs/externalbugtracker/debbugs.py
+++ b/lib/lp/bugs/externalbugtracker/debbugs.py
@@ -10,7 +10,6 @@ __all__ = [
     ]
 
 from datetime import datetime
-import email
 from email.utils import (
     mktime_tz,
     parseaddr,
@@ -42,6 +41,7 @@ from lp.bugs.interfaces.externalbugtracker import (
     UNKNOWN_REMOTE_IMPORTANCE,
     )
 from lp.bugs.scripts import debbugs
+from lp.services.compat import message_from_bytes
 from lp.services.config import config
 from lp.services.database.isolation import ensure_no_transaction
 from lp.services.mail.sendmail import simple_sendmail
@@ -242,7 +242,7 @@ class DebBugs(ExternalBugTracker):
 
         comment_ids = []
         for comment in debian_bug.comments:
-            parsed_comment = email.message_from_string(comment)
+            parsed_comment = message_from_bytes(comment)
 
             # It's possible for the same message to appear several times
             # in a DebBugs comment log, since each control command in a
@@ -271,7 +271,7 @@ class DebBugs(ExternalBugTracker):
         self._loadLog(debian_bug)
 
         for comment in debian_bug.comments:
-            parsed_comment = email.message_from_string(comment)
+            parsed_comment = message_from_bytes(comment)
             if parsed_comment['message-id'] == comment_id:
                 return parseaddr(parsed_comment['from'])
 
@@ -324,7 +324,7 @@ class DebBugs(ExternalBugTracker):
         self._loadLog(debian_bug)
 
         for comment in debian_bug.comments:
-            parsed_comment = email.message_from_string(comment)
+            parsed_comment = message_from_bytes(comment)
             if parsed_comment['message-id'] == comment_id:
                 msg_date = self._getDateForComment(parsed_comment)
                 message = getUtility(IMessageSet).fromEmail(comment, poster,
diff --git a/lib/lp/bugs/tests/bugs-emailinterface.txt b/lib/lp/bugs/tests/bugs-emailinterface.txt
index 271e77d..45a0a11 100644
--- a/lib/lp/bugs/tests/bugs-emailinterface.txt
+++ b/lib/lp/bugs/tests/bugs-emailinterface.txt
@@ -37,7 +37,7 @@ But if you want you can use any of the available commands as well.
 
 Let's take an example where we file a bug on Firefox:
 
-    >>> submit_mail = """From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: A bug in Firefox
@@ -71,10 +71,10 @@ Now if we pass the message to the Malone handler, we can see that the
 bug got submitted correctly:
 
     >>> from lp.bugs.mail.handler import MaloneHandler
+    >>> from lp.services.compat import message_from_bytes
     >>> handler = MaloneHandler()
     >>> def construct_email(raw_mail):
-    ...     msg = email.message_from_string(
-    ...         raw_mail, _class=MockSignedMessage)
+    ...     msg = message_from_bytes(raw_mail, _class=MockSignedMessage)
     ...     if 'Message-Id' not in msg:
     ...         msg['Message-Id'] = factory.makeUniqueRFC822MsgId()
     ...     return msg
@@ -155,7 +155,7 @@ If we would file a bug on Ubuntu instead, we would submit a mail like
 this:
 
     >>> login(sampledata.USER_EMAIL)
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: A bug in Ubuntu's Mozilla package
@@ -204,7 +204,7 @@ It's possible to file a bug on more than product/package at once:
     ...         distroseries=debian.currentseries,
     ...         sourcepackagename=evolution_dsp.sourcepackagename)
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Affects many packages
@@ -231,7 +231,7 @@ It's possible to file a bug on more than product/package at once:
 If the subject is folded (i.e spans more than one line), it will be
 unfolded before the bug subject is assigned.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:43 BST 2005
     ... Subject: A folded
@@ -255,7 +255,7 @@ emails to this address in order to add new comments to the bug. Note
 that we can interleave commands in the comment as well. If the comment
 includes commands, the email has to be OpenPGP-signed.
 
-    >>> comment_mail = """From: test@xxxxxxxxxxxxx
+    >>> comment_mail = b"""From: test@xxxxxxxxxxxxx
     ... To: 1@malone-domain
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Message-Id: <yada-yada-test1>
@@ -296,7 +296,7 @@ For that you can send mails to edit@malone-domain.
     >>> bug_five = bugset.get(5)
     >>> bug_four_comments = bug_four.messages.count()
     >>> bug_five_comments = bug_five.messages.count()
-    >>> edit_mail = """From: test@xxxxxxxxxxxxx
+    >>> edit_mail = b"""From: test@xxxxxxxxxxxxx
     ... To: edit@malone-domain
     ... Date: Fri Jun 17 10:10:23 BST 2005
     ... Subject: Not important
@@ -331,7 +331,7 @@ The email handler requires that a bug be specified to be changed. If no
 bug is specified, no edits occur and a message is sent to the user telling
 them what happened.
 
-    >>> edit_mail = """From: test@xxxxxxxxxxxxx
+    >>> edit_mail = b"""From: test@xxxxxxxxxxxxx
     ... To: edit@malone-domain
     ... Date: Fri Jun 17 10:10:23 BST 2005
     ... Subject: Not important
@@ -356,7 +356,7 @@ And the person sending the email has received an error message.
     ...     if not stub.test_emails:
     ...         raise AssertionError("No emails queued!")
     ...     from_addr, to_addrs, raw_message = stub.test_emails[-1]
-    ...     sent_msg = email.message_from_string(raw_message)
+    ...     sent_msg = message_from_bytes(raw_message)
     ...     error_mail, original_mail = sent_msg.get_payload()
     ...     print("Subject: %s" % sent_msg['Subject'])
     ...     print("To: %s" % ', '.join(to_addrs))
@@ -410,7 +410,7 @@ principal with that.
 
 Now we send a comment containing commands.
 
-    >>> comment_mail = """From: test@xxxxxxxxxxxxx
+    >>> comment_mail = b"""From: test@xxxxxxxxxxxxx
     ... To: 1@malone-domain
     ... Date: Fri Dec 17 10:20:23 BST 2005
     ... Message-Id: <yada-yada-test2>
@@ -457,8 +457,7 @@ The same will happen if we send the same email without signing it:
     >>> class MockUnsignedMessage(email.message.Message):
     ...     signedMessage = None
     ...     signature = None
-    >>> msg = email.message_from_string(
-    ...     comment_mail, _class=MockUnsignedMessage)
+    >>> msg = message_from_bytes(comment_mail, _class=MockUnsignedMessage)
     >>> handler.process(
     ...     msg, msg['To'],
     ...     )
@@ -479,7 +478,7 @@ The same will happen if we send the same email without signing it:
 If we don't include any commands in the comment, it will be added
 to the bug:
 
-    >>> comment_mail = """From: test@xxxxxxxxxxxxx
+    >>> comment_mail = b"""From: test@xxxxxxxxxxxxx
     ... To: 1@malone-domain
     ... Date: Fri Dec 17 10:20:23 BST 2005
     ... Message-Id: <yada-yada-test3>
@@ -510,13 +509,14 @@ us to play with. First we define a function to easily submit commands
 to edit bug 4:
 
     >>> def construct_command_email(bug, *commands):
-    ...     edit_mail = ("From: test@xxxxxxxxxxxxx\n"
-    ...                  "To: edit@malone-domain\n"
-    ...                  "Date: Fri Jun 17 10:10:23 BST 2005\n"
-    ...                  "Subject: Not important\n"
-    ...                  "\n"
-    ...                  " bug %d\n" % bug.id)
-    ...     edit_mail += ' ' + '\n '.join(commands)
+    ...     edit_mail = (b"From: test@xxxxxxxxxxxxx\n"
+    ...                  b"To: edit@malone-domain\n"
+    ...                  b"Date: Fri Jun 17 10:10:23 BST 2005\n"
+    ...                  b"Subject: Not important\n"
+    ...                  b"\n"
+    ...                  b" bug %d\n" % bug.id)
+    ...     edit_mail += b' ' + b'\n '.join(
+    ...         six.ensure_binary(command) for command in commands)
     ...     return construct_email(edit_mail)
 
     >>> def submit_command_email(msg):
@@ -1843,7 +1843,7 @@ signing the mail:
 A submit without specifying on what we want to file the bug on:
 
     >>> login(sampledata.USER_EMAIL)
-    >>> submit_mail_no_bugtask = """From: test@xxxxxxxxxxxxx
+    >>> submit_mail_no_bugtask = b"""From: test@xxxxxxxxxxxxx
     ... To: new@malone
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: A bug without a product or distribution
@@ -1861,7 +1861,7 @@ A submit without specifying on what we want to file the bug on:
 
 Submit a bug on a distribution that doesn't exist:
 
-    >>> submit_mail_distro_not_found = """From: test@xxxxxxxxxxxxx
+    >>> submit_mail_distro_not_found = b"""From: test@xxxxxxxxxxxxx
     ... To: new@malone
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: A bug with a non existing distribution
@@ -1884,7 +1884,7 @@ Submit a bug on a distribution that doesn't exist:
 
 An empty unsigned mail to new@malone:
 
-    >>> submit_empty = """From: test@xxxxxxxxxxxxx
+    >>> submit_empty = b"""From: test@xxxxxxxxxxxxx
     ... To: new@malone
     ... Date: Fri Jun 17 10:20:27 BST 2005
     ... Subject: An empty mail
@@ -1905,7 +1905,7 @@ If we submit an email with no affects command, it is rejected.
 
     >>> from lp.bugs.model.bug import Bug
     >>> before_count = Bug.select().count()
-    >>> submit_mail = """From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: A bug with no affects
@@ -1938,7 +1938,7 @@ XXX: Gavin Panella 2009-07-24 bug=404010: The need for this test
 arises from the implementation of MaloneHandler.
 
     >>> before_count = Bug.select().count()
-    >>> submit_mail = """From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: A bug with no affects
@@ -1971,7 +1971,7 @@ XXX: Gavin Panella 2009-07-24 bug=404010: The need for this test
 arises from the implementation of MaloneHandler.
 
     >>> before_count = Bug.select().count()
-    >>> submit_mail = """\
+    >>> submit_mail = b"""\
     ... From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
@@ -2004,7 +2004,7 @@ reached. For example, unsubscribing oneself from a private bug then
 linking a CVE.
 
     >>> before_count = Bug.select().count()
-    >>> submit_mail = """\
+    >>> submit_mail = b"""\
     ... From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
@@ -2033,7 +2033,7 @@ Let's take a closer look at send_process_error_notification(), which is
 used to send the error messages. It needs the message that caused the
 error, so let's create one.
 
-    >>> test_msg = email.message_from_string("""From: foo.bar@xxxxxxxxxxxxx
+    >>> test_msg = message_from_bytes(b"""From: foo.bar@xxxxxxxxxxxxx
     ... To: bugs@xxxxxxxxxxxxx
     ... Message-Id: <original@msg>
     ... Subject: Original Message Subject
@@ -2056,7 +2056,7 @@ The To and Subject headers got set to the values we provided:
 
     >>> transaction.commit()
     >>> from_addr, to_addrs, raw_message = stub.test_emails[-1]
-    >>> sent_msg = email.message_from_string(raw_message)
+    >>> sent_msg = message_from_bytes(raw_message)
     >>> sent_msg['To']
     'test@xxxxxxxxxxxxx'
     >>> sent_msg['Subject']
@@ -2114,7 +2114,7 @@ the original message.
     ...     max_return_size=max_return_size)
     >>> transaction.commit()
     >>> from_addr, to_addrs, raw_message = stub.test_emails[-1]
-    >>> sent_msg = email.message_from_string(raw_message)
+    >>> sent_msg = message_from_bytes(raw_message)
     >>> failure_msg, original_msg = sent_msg.get_payload()
     >>> msg = original_msg.get_payload()[0]
 
@@ -2134,7 +2134,7 @@ that action we used to have a bug (See bug #126943).
 First, we create a new firefox bug.
 
     >>> login(sampledata.USER_EMAIL)
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2006
     ... Subject: Another bug in Firefox
@@ -2187,7 +2187,7 @@ Sample Person sends an email with several commands. First comes an
 that doesn't exist (and so is guaranteed to result in a failure) and
 finally, the status of the selected bug task is set to 'confirmed'.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = ("""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: %s@xxxxxxxxxxxxxxxxxx
     ... Date: Thu Apr 3 11:53:23 BST 2008
     ... Subject: A new bug in Firefox
@@ -2196,7 +2196,7 @@ finally, the status of the selected bug task is set to 'confirmed'.
     ...  affects firefox
     ...  subscribe nonexistentuser
     ...  status confirmed
-    ... """ % ff_bug.id
+    ... """ % ff_bug.id).encode('ASCII')
     >>> process_email(submit_mail)
 
 The 'affects' and 'status' commands were processed successfully - the
@@ -2211,7 +2211,7 @@ The 'subscribe' command failed, and the user is being notified of the
 failure in an email.
 
     >>> from_addr, to_addrs, raw_message = stub.test_emails[-1]
-    >>> sent_msg = email.message_from_string(raw_message)
+    >>> sent_msg = message_from_bytes(raw_message)
     >>> failure_msg, original_msg = sent_msg.get_payload()
     >>> print(failure_msg.get_payload(decode=True).decode('UTF-8'))
     An error occurred while processing a mail you sent to Launchpad's email
@@ -2235,7 +2235,7 @@ fail, and 'status triaged' which is OK. 'security' commands cause the
 entire email to not to be processed, though.
 
     >>> transaction.commit()
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = ("""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: %s@xxxxxxxxxxxxxxxxxx
     ... Date: Thu Apr 3 11:53:23 BST 2008
     ... Subject: A new bug in Firefox
@@ -2244,7 +2244,7 @@ entire email to not to be processed, though.
     ...  affects firefox
     ...  status triaged
     ...  security maybe
-    ... """ % ff_bug.id
+    ... """ % ff_bug.id).encode('ASCII')
     >>> process_email(submit_mail)
 
 The status hasn't changed.
@@ -2259,7 +2259,7 @@ And the sender receives an email to let them know about the failing
 'security' command.
 
     >>> from_addr, to_addrs, raw_message = stub.test_emails[-1]
-    >>> sent_msg = email.message_from_string(raw_message)
+    >>> sent_msg = message_from_bytes(raw_message)
     >>> failure_msg, original_msg = sent_msg.get_payload()
     >>> print(failure_msg.get_payload(decode=True).decode('UTF-8'))
     An error occurred while processing a mail you sent to Launchpad's email
@@ -2294,7 +2294,7 @@ We send an email with four commands: 'affects', to choose the target,
 'importance', to set the importance to high, 'done', to stop reading,
 and 'status', which will be ignored.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = ("""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: %s@xxxxxxxxxxxxxxxxxx
     ... Date: Thu Apr 3 11:53:23 BST 2008
     ... Subject: A new bug in Firefox
@@ -2304,7 +2304,7 @@ and 'status', which will be ignored.
     ...  importance high
     ...  done
     ...  status triaged
-    ... """ % ff_bug.id
+    ... """ % ff_bug.id).encode('UTF-8')
     >>> process_email(submit_mail)
 
 The target (Firefox) is selected and the importance set, but the status
@@ -2326,7 +2326,7 @@ Requesting help
 It's possible to ask for the help document for the email interface via
 email too. Just send an email to `help@xxxxxxxxxxxxxxxxxx`.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: help@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2006
     ... Subject: help
@@ -2347,7 +2347,7 @@ email too. Just send an email to `help@xxxxxxxxxxxxxxxxxx`.
 
 Only mail coming from verified Launchpad users is answered.
 
-    >>> submit_mail = """From: Not a User <nobody@xxxxxxxxxxx>
+    >>> submit_mail = b"""From: Not a User <nobody@xxxxxxxxxxx>
     ... To: help@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2006
     ... Subject: help
@@ -2394,7 +2394,7 @@ criteria described below).
     ...         print(attachment.type.name)
     ...         print(lib.read())
     >>> login('test@xxxxxxxxxxxxx')
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2425,7 +2425,7 @@ criteria described below).
 
 An email may contain more than one attachment; all of them are stored.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2483,7 +2483,7 @@ If a text/html attachment does not have a filename, it is not stored.
 This is the HTML representation of the main text, it is not an
 attachment.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2514,7 +2514,7 @@ If the content-disposition header of a message part begins with
 "attachment" it is stored as a bug attachment, even if the
 content-disposition header does not provide a filename.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2544,7 +2544,7 @@ it is stored as a bug attachment, if the header additionally provides
 a filename. This ensures that a message part containing debug information
 and the content type text/plain is stored as a bug attchment.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2576,7 +2576,7 @@ and the content type text/plain is stored as a bug attchment.
 If the content-disposition header of a message part begins with "inline",
 but has no filename, it is not stored as a bug attachment.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2603,7 +2603,7 @@ but has no filename, it is not stored as a bug attachment.
 If an attachment has no content disposition header, it is not stored
 as a bug attachment.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2633,7 +2633,7 @@ If an attachment has one of the content types application/applefile
 application/pkcs7-signature, application/x-pkcs7-signature,
 text/x-vcard, application/ms-tnef, it is not stored.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2683,7 +2683,7 @@ text/x-vcard, application/ms-tnef, it is not stored.
     >>> print_attachments(get_latest_added_bug().attachments)
     No attachments
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2722,7 +2722,7 @@ text/x-vcard, application/ms-tnef, it is not stored.
 
 Attachments sent in replies to existing bugs are stored too.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: 1@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2754,7 +2754,7 @@ Attachments sent in replies to existing bugs are stored too.
 If an attachment has the content type text/x-diff or text/x-patch,
 it is considered to contain a patch.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2795,7 +2795,7 @@ it is considered to contain a patch.
 
 Mail attachments without a filename are named "unnamed".
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2837,7 +2837,7 @@ Mail attachments without a filename are named "unnamed".
 If an email has two attachments with the same filename, the names are
 not changed.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2871,7 +2871,7 @@ not changed.
 
 Base64 encoded attachments are decoded before being stored.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -2903,7 +2903,7 @@ Some mail clients append a filename to the content type of attachments.
 The content type of the PGP signature is properly detected and thus no bug
 attachment is created.
 
-    >>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
+    >>> submit_mail = b"""From: Sample Person <test@xxxxxxxxxxxxx>
     ... To: new@xxxxxxxxxxxxxxxxxx
     ... Date: Fri Jun 17 10:20:23 BST 2005
     ... Subject: Another bug in Firefox
@@ -3038,7 +3038,7 @@ been submitted.
     >>> comment_date = datetime(
     ...     2008, 5, 20, 11, 24, 12, tzinfo=pytz.timezone('Europe/Prague'))
 
-    >>> reply_mail = """From: test@xxxxxxxxxxxxx
+    >>> reply_mail = ("""From: test@xxxxxxxxxxxxx
     ... To: %(bug_id)s@malone-domain
     ... Date: %(date)s
     ... Message-Id: <1234567890@xxxxxxxxxxxxx>
@@ -3051,7 +3051,7 @@ been submitted.
     ...     'bug_id': bug_with_watch.id,
     ...     'date': comment_date.strftime('%a %b %d %H:%M:%S %Z %Y'),
     ...     'rfc822msgid': str(message.rfc822msgid),
-    ...     }
+    ...     }).encode('ASCII')
 
     >>> process_email(reply_mail)
     >>> transaction.commit()
@@ -3087,7 +3087,7 @@ to the bug and will not have its bugwatch field set.
     >>> comment_date = datetime(
     ...     2008, 5, 21, 11, 9, 12, tzinfo=pytz.timezone('Europe/Prague'))
 
-    >>> initial_mail = b"""From: test@xxxxxxxxxxxxx
+    >>> initial_mail = ("""From: test@xxxxxxxxxxxxx
     ... To: %(bug_id)s@malone-domain
     ... Date: %(date)s
     ... Message-Id: <912876543@xxxxxxxxxxxxx>
@@ -3097,14 +3097,14 @@ to the bug and will not have its bugwatch field set.
     ... """ % {
     ...     'bug_id': bug_with_watch.id,
     ...     'date': comment_date.strftime('%a %b %d %H:%M:%S %Z %Y'),
-    ...     }
+    ...     }).encode('ASCII')
     >>> message = getUtility(IMessageSet).fromEmail(
     ...     initial_mail, no_priv)
 
     >>> comment_date = datetime(
     ...     2008, 5, 21, 12, 52, 12, tzinfo=pytz.timezone('Europe/Prague'))
 
-    >>> reply_mail = """From: test@xxxxxxxxxxxxx
+    >>> reply_mail = ("""From: test@xxxxxxxxxxxxx
     ... To: %(bug_id)s@malone-domain
     ... Date: %(date)s
     ... Message-Id: <asu90ik1234567890@xxxxxxxxxxxxx>
@@ -3115,7 +3115,7 @@ to the bug and will not have its bugwatch field set.
     ... """ % {
     ...     'bug_id': bug_with_watch.id,
     ...     'date': comment_date.strftime('%a %b %d %H:%M:%S %Z %Y'),
-    ...     }
+    ...     }).encode('ASCII')
 
     >>> process_email(reply_mail)
     >>> transaction.commit()
diff --git a/lib/lp/code/doc/codeimport.txt b/lib/lp/code/doc/codeimport.txt
index 3ecc50d..9f2c088 100644
--- a/lib/lp/code/doc/codeimport.txt
+++ b/lib/lp/code/doc/codeimport.txt
@@ -103,8 +103,8 @@ three members of the vcs-imports team.
     >>> vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
     >>> len(get_contact_email_addresses(vcs_imports))
     3
-    >>> import email
-    >>> message = email.message_from_string(stub.test_emails[0][2])
+    >>> from lp.services.compat import message_from_bytes
+    >>> message = message_from_bytes(stub.test_emails[0][2])
     >>> print(message['subject'])
     New code import: ~import-person/widget/trunk-cvs
     >>> print(message['X-Launchpad-Message-Rationale'])
diff --git a/lib/lp/code/mail/tests/test_codeimport.py b/lib/lp/code/mail/tests/test_codeimport.py
index e82f466..b37f538 100644
--- a/lib/lp/code/mail/tests/test_codeimport.py
+++ b/lib/lp/code/mail/tests/test_codeimport.py
@@ -5,7 +5,6 @@
 
 from __future__ import absolute_import, print_function, unicode_literals
 
-from email import message_from_string
 import textwrap
 
 import six
@@ -17,6 +16,7 @@ from lp.code.enums import (
     TargetRevisionControlSystems,
     )
 from lp.code.tests.helpers import GitHostingFixture
+from lp.services.compat import message_from_bytes
 from lp.services.mail import stub
 from lp.testing import (
     login_celebrity,
@@ -42,7 +42,7 @@ class TestNewCodeImports(TestCaseWithFactory):
             cvs_module='a_module', branch_name='import',
             product=fooix, registrant=eric)
         transaction.commit()
-        msg = message_from_string(stub.test_emails[0][2])
+        msg = message_from_bytes(stub.test_emails[0][2])
         self.assertEqual('code-import', msg['X-Launchpad-Notification-Type'])
         self.assertEqual('~eric/fooix/import', msg['X-Launchpad-Branch'])
         self.assertEqual(
@@ -66,7 +66,7 @@ class TestNewCodeImports(TestCaseWithFactory):
             branch_name='trunk', product=fooix, registrant=eric,
             rcs_type=RevisionControlSystems.BZR_SVN)
         transaction.commit()
-        msg = message_from_string(stub.test_emails[0][2])
+        msg = message_from_bytes(stub.test_emails[0][2])
         self.assertEqual('code-import', msg['X-Launchpad-Notification-Type'])
         self.assertEqual('~eric/fooix/trunk', msg['X-Launchpad-Branch'])
         self.assertEqual(
@@ -89,7 +89,7 @@ class TestNewCodeImports(TestCaseWithFactory):
             git_repo_url='git://git.example.com/fooix.git',
             branch_name='master', product=fooix, registrant=eric)
         transaction.commit()
-        msg = message_from_string(stub.test_emails[0][2])
+        msg = message_from_bytes(stub.test_emails[0][2])
         self.assertEqual('code-import', msg['X-Launchpad-Notification-Type'])
         self.assertEqual('~eric/fooix/master', msg['X-Launchpad-Branch'])
         self.assertEqual(
@@ -115,7 +115,7 @@ class TestNewCodeImports(TestCaseWithFactory):
             branch_name=u'master', product=fooix, registrant=eric,
             target_rcs_type=TargetRevisionControlSystems.GIT)
         transaction.commit()
-        msg = message_from_string(stub.test_emails[0][2])
+        msg = message_from_bytes(stub.test_emails[0][2])
         self.assertEqual('code-import', msg['X-Launchpad-Notification-Type'])
         self.assertEqual('~eric/fooix/+git/master', msg['X-Launchpad-Branch'])
         self.assertEqual(
@@ -143,7 +143,7 @@ class TestNewCodeImports(TestCaseWithFactory):
             git_repo_url='git://git.example.com/fooix.git',
             branch_name='master', sourcepackage=fooix, registrant=eric)
         transaction.commit()
-        msg = message_from_string(stub.test_emails[0][2])
+        msg = message_from_bytes(stub.test_emails[0][2])
         self.assertEqual('code-import', msg['X-Launchpad-Notification-Type'])
         self.assertEqual(
             '~eric/foobuntu/manic/fooix/master', msg['X-Launchpad-Branch'])
@@ -165,7 +165,7 @@ class TestUpdatedCodeImports(TestCaseWithFactory):
     layer = DatabaseFunctionalLayer
 
     def assertSameDetailsEmail(self, details, unique_name):
-        msg = message_from_string(stub.test_emails[0][2])
+        msg = message_from_bytes(stub.test_emails[0][2])
         self.assertEqual(
             'code-import-updated', msg['X-Launchpad-Notification-Type'])
         self.assertEqual(unique_name, msg['X-Launchpad-Branch'])
@@ -184,7 +184,7 @@ class TestUpdatedCodeImports(TestCaseWithFactory):
 
     def assertDifferentDetailsEmail(self, old_details, new_details,
                                     unique_name):
-        msg = message_from_string(stub.test_emails[0][2])
+        msg = message_from_bytes(stub.test_emails[0][2])
         self.assertEqual(
             'code-import-updated', msg['X-Launchpad-Notification-Type'])
         self.assertEqual(unique_name, msg['X-Launchpad-Branch'])
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index df40171..c8e5bcf 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -15,7 +15,6 @@ from datetime import (
     datetime,
     timedelta,
     )
-import email
 from functools import partial
 import hashlib
 import json
@@ -147,6 +146,7 @@ from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
 from lp.registry.interfaces.personproduct import IPersonProductFactory
 from lp.registry.tests.test_accesspolicy import get_policies_for_artifact
 from lp.services.authserver.xmlrpc import AuthServerAPIView
+from lp.services.compat import message_from_bytes
 from lp.services.config import config
 from lp.services.database.constants import UTC_NOW
 from lp.services.database.interfaces import IStore
@@ -1295,7 +1295,7 @@ class TestGitRepositoryModificationNotifications(TestCaseWithFactory):
                 getUtility(IGitRepositoryModifiedMailJobSource)).runAll()
         bodies_by_recipient = {}
         for from_addr, to_addrs, message in stub.test_emails:
-            body = email.message_from_string(message).get_payload(decode=True)
+            body = message_from_bytes(message).get_payload(decode=True)
             for to_addr in to_addrs:
                 bodies_by_recipient[to_addr] = six.ensure_text(body)
         # Both the owner and the unprivileged subscriber receive email.
diff --git a/lib/lp/codehosting/scanner/tests/test_email.py b/lib/lp/codehosting/scanner/tests/test_email.py
index 26ab55f..5fb3a8b 100644
--- a/lib/lp/codehosting/scanner/tests/test_email.py
+++ b/lib/lp/codehosting/scanner/tests/test_email.py
@@ -7,7 +7,6 @@ from __future__ import absolute_import, print_function
 
 __metaclass__ = type
 
-import email
 import os
 
 from breezy.uncommit import uncommit
@@ -28,6 +27,7 @@ from lp.codehosting.scanner import events
 from lp.codehosting.scanner.bzrsync import BzrSync
 from lp.codehosting.scanner.tests.test_bzrsync import BzrSyncTestCase
 from lp.registry.interfaces.person import IPersonSet
+from lp.services.compat import message_from_bytes
 from lp.services.config import config
 from lp.services.features.testing import FeatureFixture
 from lp.services.job.runner import JobRunner
@@ -74,7 +74,7 @@ class TestBzrSyncEmail(BzrSyncTestCase):
         self.assertEqual(len(stub.test_emails), 1)
         [initial_email] = stub.test_emails
         expected = 'First scan of the branch detected 0 revisions'
-        message = email.message_from_string(initial_email[2])
+        message = message_from_bytes(initial_email[2])
         email_body = message.get_payload()
         self.assertIn(expected, email_body)
         self.assertEmailHeadersEqual(
@@ -89,7 +89,7 @@ class TestBzrSyncEmail(BzrSyncTestCase):
         [initial_email] = stub.test_emails
         expected = ('First scan of the branch detected 1 revision'
                     ' in the revision history of the=\n branch.')
-        message = email.message_from_string(initial_email[2])
+        message = message_from_bytes(initial_email[2])
         email_body = message.get_payload()
         self.assertIn(expected, email_body)
         self.assertEmailHeadersEqual(
@@ -107,7 +107,7 @@ class TestBzrSyncEmail(BzrSyncTestCase):
         self.assertEqual(len(stub.test_emails), 1)
         [uncommit_email] = stub.test_emails
         expected = '1 revision was removed from the branch.'
-        message = email.message_from_string(uncommit_email[2])
+        message = message_from_bytes(uncommit_email[2])
         email_body = message.get_payload()
         self.assertIn(expected, email_body)
         self.assertEmailHeadersEqual(
@@ -139,7 +139,7 @@ class TestBzrSyncEmail(BzrSyncTestCase):
             'Subject: [Branch %s] Test branch' % self.db_branch.unique_name)
         self.assertIn(expected, uncommit_email_body)
 
-        recommit_email_msg = email.message_from_string(recommit_email[2])
+        recommit_email_msg = message_from_bytes(recommit_email[2])
         recommit_email_body = recommit_email_msg.get_payload()[0].get_payload(
             decode=True)
         subject = '[Branch %s] Rev 1: second' % self.db_branch.unique_name
diff --git a/lib/lp/registry/browser/tests/test_person.py b/lib/lp/registry/browser/tests/test_person.py
index 68222c4..1bd1f8f 100644
--- a/lib/lp/registry/browser/tests/test_person.py
+++ b/lib/lp/registry/browser/tests/test_person.py
@@ -7,7 +7,6 @@ from __future__ import unicode_literals
 __metaclass__ = type
 
 import doctest
-import email
 from operator import attrgetter
 import re
 from textwrap import dedent
@@ -65,6 +64,7 @@ from lp.registry.interfaces.teammembership import (
 from lp.registry.model.karma import KarmaCategory
 from lp.registry.model.milestone import milestone_sort_key
 from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache
+from lp.services.compat import message_from_bytes
 from lp.services.config import config
 from lp.services.database.interfaces import IStore
 from lp.services.features.testing import FeatureFixture
@@ -829,7 +829,7 @@ class TestPersonEditView(TestPersonRenameFormMixin, TestCaseWithFactory):
         messages = [msg for from_addr, to_addr, msg in stub.test_emails]
         raw_msg = None
         for orig_msg in messages:
-            msg = email.message_from_string(orig_msg)
+            msg = message_from_bytes(orig_msg)
             if msg.get('to') == added_email:
                 raw_msg = orig_msg
         token_url = get_token_url_from_email(raw_msg)
diff --git a/lib/lp/registry/doc/distribution-mirror.txt b/lib/lp/registry/doc/distribution-mirror.txt
index cb4a97c..05e9857 100644
--- a/lib/lp/registry/doc/distribution-mirror.txt
+++ b/lib/lp/registry/doc/distribution-mirror.txt
@@ -359,18 +359,18 @@ up on the public mirror listings.
     >>> import transaction
     >>> transaction.commit()
 
-    >>> import email
     >>> from operator import itemgetter
+    >>> from lp.services.compat import message_from_bytes
     >>> from lp.services.mail import stub
     >>> len(stub.test_emails)
     2
     >>> stub.test_emails.sort(key=itemgetter(1))  # sort by to_addr
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg['To']
     'mark@xxxxxxxxxxx,karl@xxxxxxxxxxxxx'
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg['To']
     'mark@xxxxxxxxxxx'
     >>> valid_mirror.enabled
@@ -400,7 +400,7 @@ single notification to the distribution's mirror admins.
     >>> len(stub.test_emails)
     1
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg['To']
     'mark@xxxxxxxxxxx,karl@xxxxxxxxxxxxx'
 
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 e1ba1e0..206cfcf 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
@@ -3,11 +3,11 @@
 
 == Setup ==
 
-    >>> import email
-    >>> from lp.testing.keyserver import KeyServerTac
-    >>> from lp.services.mail import stub
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.services.compat import message_from_bytes
+    >>> from lp.services.mail import stub
+    >>> from lp.testing.keyserver import KeyServerTac
     >>> from lp.testing.pages import setupBrowserFreshLogin
 
 Set up the stub KeyServer:
@@ -53,7 +53,7 @@ text part that provides useful information to users who -- for whatever reason
 -- cannot decrypt the token url.  Start by grabbing the confirmation message.
 
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_msg)
+    >>> msg = message_from_bytes(raw_msg)
     >>> msg.get_content_type()
     'text/plain'
 
@@ -174,7 +174,7 @@ their Launchpad account.
 Sample Person checks their email.
 
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_msg)
+    >>> msg = message_from_bytes(raw_msg)
     >>> msg.get_content_type()
     'text/plain'
     >>> body = msg.get_payload(decode=True)
@@ -505,7 +505,7 @@ Get the token from the body of the email sent.
     >>> import email, re
     >>> from lp.services.mail import stub
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_msg)
+    >>> msg = message_from_bytes(raw_msg)
     >>> cipher_body = msg.get_payload(decode=1)
     >>> body = decrypt_content(cipher_body, six.ensure_str('test'))
     >>> link = get_token_url_from_bytes(body)
@@ -623,7 +623,7 @@ Test if the advertisement email was sent:
     >>> import email
     >>> from lp.services.mail import stub
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_msg)
+    >>> msg = message_from_bytes(raw_msg)
     >>> print(six.ensure_text(msg.get_payload(decode=True)))
     <BLANKLINE>
     ...
diff --git a/lib/lp/registry/stories/mailinglists/hosted-email-address.txt b/lib/lp/registry/stories/mailinglists/hosted-email-address.txt
index f9cebb1..aed8964 100644
--- a/lib/lp/registry/stories/mailinglists/hosted-email-address.txt
+++ b/lib/lp/registry/stories/mailinglists/hosted-email-address.txt
@@ -45,8 +45,8 @@ Launchpad sends that address a confirmation message.
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
     >>> stub.test_emails
     []
-    >>> import email
-    >>> msg = email.message_from_string(raw_msg)
+    >>> from lp.services.compat import message_from_bytes
+    >>> msg = message_from_bytes(raw_msg)
     >>> print(msg['From'])
     Launchpad Email Validator <noreply@xxxxxxxxxxxxx>
     >>> print(msg['Subject'])
diff --git a/lib/lp/registry/stories/person/xx-add-sshkey.txt b/lib/lp/registry/stories/person/xx-add-sshkey.txt
index 0e80b55..dda542d 100644
--- a/lib/lp/registry/stories/person/xx-add-sshkey.txt
+++ b/lib/lp/registry/stories/person/xx-add-sshkey.txt
@@ -191,7 +191,7 @@ this case we'll raise an UnexpectedFormData.
 If he removes a key then he will get a security warning email notification
 that the key has been removed.
 
-    >>> import email
+    >>> from lp.services.compat import message_from_bytes
     >>> from lp.services.mail import stub
     >>> stub.test_emails = []
     >>> browser.open('http://launchpad.test/~salgado/+editsshkeys')
@@ -199,7 +199,7 @@ that the key has been removed.
     >>> from_addr, to_addr, msg = stub.test_emails.pop()
     >>> to_addr
     ['guilherme.salgado@xxxxxxxxxxxxx']
-    >>> payload = email.message_from_string(msg).get_payload()
+    >>> payload = message_from_bytes(msg).get_payload()
     >>> assert payload.startswith('The SSH Key')
 
 
diff --git a/lib/lp/registry/tests/test_mailinglistapi.py b/lib/lp/registry/tests/test_mailinglistapi.py
index 52ac452..43178f6 100644
--- a/lib/lp/registry/tests/test_mailinglistapi.py
+++ b/lib/lp/registry/tests/test_mailinglistapi.py
@@ -38,6 +38,10 @@ from lp.registry.xmlrpc.mailinglist import (
     ENABLED,
     MailingListAPIView,
     )
+from lp.services.compat import (
+    message_as_bytes,
+    message_from_bytes,
+    )
 from lp.services.config import config
 from lp.services.identity.interfaces.account import AccountStatus
 from lp.services.identity.interfaces.emailaddress import (
@@ -462,7 +466,8 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
         # and notifies a team admins to moderate it.
         team, sender, message = self.makeMailingListAndHeldMessage()
         pop_notifications()
-        info = self.mailinglist_api.holdMessage('team', message.as_string())
+        info = self.mailinglist_api.holdMessage(
+            'team', message_as_bytes(message))
         notifications = pop_notifications()
         found = self.message_approval_set.getMessageByMessageID('<first-post>')
         self.assertIs(True, info)
@@ -480,7 +485,8 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
         # Users can send messages to private teams (did they guess the name)?
         team, sender, message = self.makeMailingListAndHeldMessage(
             private=True)
-        info = self.mailinglist_api.holdMessage('team', message.as_string())
+        info = self.mailinglist_api.holdMessage(
+            'team', message_as_bytes(message))
         found = self.message_approval_set.getMessageByMessageID('<first-post>')
         self.assertIs(True, info)
         self.assertIsNot(None, found)
@@ -489,7 +495,7 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
         # Non-ascii messages headers are re-encoded for moderators.
         team, sender, message = self.makeMailingListAndHeldMessage()
         with person_logged_in(sender):
-            message = message_from_string(dedent("""\
+            message = message_from_bytes(dedent("""\
                 From: \xa9 me <me@xxxxxx>
                 To: team@xxxxxxxxxxxxxxxxxxxx
                 Subject: \xa9 gremlins
@@ -498,7 +504,7 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
                 I put \xa9 in the body.
                 """).encode('ISO-8859-1'))
         info = self.mailinglist_api.holdMessage(
-            'team', xmlrpc_client.Binary(message.as_string()))
+            'team', xmlrpc_client.Binary(message_as_bytes(message)))
         transaction.commit()
         found = self.message_approval_set.getMessageByMessageID('<\\xa9-me>')
         self.assertIs(True, info)
@@ -521,7 +527,7 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
         # List moderators can approve messages.
         team, sender, message = self.makeMailingListAndHeldMessage()
         pop_notifications()
-        self.mailinglist_api.holdMessage('team', message.as_string())
+        self.mailinglist_api.holdMessage('team', message_as_bytes(message))
         found = self.message_approval_set.getMessageByMessageID('<first-post>')
         found.approve(team.teamowner)
         self.assertEqual(PostedMessageStatus.APPROVAL_PENDING, found.status)
@@ -534,7 +540,7 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
         # List moderators can reject messages.
         team, sender, message = self.makeMailingListAndHeldMessage()
         pop_notifications()
-        self.mailinglist_api.holdMessage('team', message.as_string())
+        self.mailinglist_api.holdMessage('team', message_as_bytes(message))
         found = self.message_approval_set.getMessageByMessageID('<first-post>')
         found.reject(team.teamowner)
         self.assertEqual(PostedMessageStatus.REJECTION_PENDING, found.status)
@@ -547,7 +553,7 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
         # List moderators can discard messages.
         team, sender, message = self.makeMailingListAndHeldMessage()
         pop_notifications()
-        self.mailinglist_api.holdMessage('team', message.as_string())
+        self.mailinglist_api.holdMessage('team', message_as_bytes(message))
         found = self.message_approval_set.getMessageByMessageID('<first-post>')
         found.discard(team.teamowner)
         self.assertEqual(PostedMessageStatus.DISCARD_PENDING, found.status)
diff --git a/lib/lp/registry/tests/test_xmlrpc.py b/lib/lp/registry/tests/test_xmlrpc.py
index 79bf7f8..11da72f 100644
--- a/lib/lp/registry/tests/test_xmlrpc.py
+++ b/lib/lp/registry/tests/test_xmlrpc.py
@@ -23,6 +23,7 @@ from lp.registry.interfaces.mailinglist import (
     PostedMessageStatus,
     )
 from lp.registry.interfaces.person import PersonalStanding
+from lp.services.compat import message_as_bytes
 from lp.services.config import config
 from lp.testing import (
     admin_logged_in,
@@ -180,7 +181,7 @@ class TestMailingListXMLRPCMessage(TestCaseWithFactory):
         # and notifies a team admins to moderate it.
         team, sender, message = self.makeMailingListAndHeldMessage()
         pop_notifications()
-        info = self.rpc_proxy.holdMessage('team', message.as_string())
+        info = self.rpc_proxy.holdMessage('team', message_as_bytes(message))
         notifications = pop_notifications()
         found = getUtility(IMessageApprovalSet).getMessageByMessageID(
             '<first-post>')
@@ -199,7 +200,7 @@ class TestMailingListXMLRPCMessage(TestCaseWithFactory):
         # List moderators can approve messages.
         team, sender, message = self.makeMailingListAndHeldMessage()
         pop_notifications()
-        self.rpc_proxy.holdMessage('team', message.as_string())
+        self.rpc_proxy.holdMessage('team', message_as_bytes(message))
         found = getUtility(IMessageApprovalSet).getMessageByMessageID(
             '<first-post>')
         found.approve(team.teamowner)
diff --git a/lib/lp/services/mail/doc/mailbox.txt b/lib/lp/services/mail/doc/mailbox.txt
index c3274cf..40702a0 100644
--- a/lib/lp/services/mail/doc/mailbox.txt
+++ b/lib/lp/services/mail/doc/mailbox.txt
@@ -74,8 +74,8 @@ before:
     >>> len(mails)
     2
     >>> id, raw_mail = mails[0]
-    >>> import email
-    >>> mail = email.message_from_string(raw_mail)
+    >>> from lp.services.compat import message_from_bytes
+    >>> mail = message_from_bytes(raw_mail)
     >>> print(mail['Message-ID'])
     <test1>
 
@@ -90,7 +90,7 @@ Since we didn't delete the mail, it's still in there:
     >>> len(mails)
     2
     >>> id, raw_mail = mails[0]
-    >>> mail = email.message_from_string(raw_mail)
+    >>> mail = message_from_bytes(raw_mail)
     >>> print(mail['Message-ID'])
     <test1>
 
diff --git a/lib/lp/services/mail/doc/sending-mail.txt b/lib/lp/services/mail/doc/sending-mail.txt
index 428f07a..ecf5c50 100644
--- a/lib/lp/services/mail/doc/sending-mail.txt
+++ b/lib/lp/services/mail/doc/sending-mail.txt
@@ -17,10 +17,10 @@ The mail get sent when the transaction gets commited:
 
 Now let's look at the sent email:
 
-    >>> import email
+    >>> from lp.services.compat import message_from_bytes
     >>> from lp.services.mail import stub
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg['To']
     'test@xxxxxxxxxxxxx'
     >>> msg['From']
@@ -50,7 +50,7 @@ the person's name is encoded properly.
 
     >>> transaction.commit()
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg['To']
     'test@xxxxxxxxxxxxx'
     >>> msg['From']
@@ -84,7 +84,7 @@ simple_sendmail_from_person uses the Person's preferred email address:
     >>> transaction.commit()
     >>> found = False
     >>> for from_addr, to_addr, raw_message in stub.test_emails:
-    ...     msg = email.message_from_string(raw_message)
+    ...     msg = message_from_bytes(raw_message)
     ...     if msg['From'] == 'Sample Person <testing@xxxxxxxxxxxxx>':
     ...         found = True
     >>> assert found
@@ -107,7 +107,7 @@ the header will appear more than once in the output message.
     >>> transaction.commit()
 
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg["X-Foo"]
     'test'
     >>> msg.get_all("X-Bar")
@@ -127,7 +127,7 @@ only.
 Now let's look at the sent email again.
 
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
 
     >>> from email.header import decode_header
     >>> subject_str, charset = decode_header(msg['Subject'])[0]
@@ -152,7 +152,7 @@ contain non-ASCII characters:
     >>> transaction.commit()
 
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
 
     >>> from email.utils import parseaddr
     >>> from_name_encoded, from_addr = parseaddr(msg['From'])
@@ -183,7 +183,7 @@ surrounded by quotes and quoted if necessary:
     >>> transaction.commit()
 
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> print(msg['From'])
     "Foo \[Baz\] \" Bar" <foo.bar@xxxxxxxxxxxxx>
 
@@ -242,7 +242,7 @@ The message is the same as the one from the simple_sendmail test except
 that the precedence header was not added.
 
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
-    >>> msg = email.message_from_string(raw_message)
+    >>> msg = message_from_bytes(raw_message)
     >>> msg['To']
     'test@xxxxxxxxxxxxx'
     >>> msg['From']
@@ -262,12 +262,13 @@ simple_sendmail creates a Message instance, and sends it via another
 function, sendmail. sendmail() can also be used directly if you want to
 send more complicated emails, like emails with attachments.
 
+    >>> from email.mime.text import MIMEText
     >>> from lp.services.mail.sendmail import sendmail
 
 Let's send a mail using that function. We only create a simple message
 to test with, though.
 
-    >>> msg = email.mime.text.MIMEText("Some content")
+    >>> msg = MIMEText("Some content")
     >>> msg['From'] = 'foo.bar@xxxxxxxxxxxxx'
     >>> msg['To'] = 'test@xxxxxxxxxxxxx'
     >>> msg['Subject'] = "test"
@@ -279,7 +280,7 @@ provide better bounce handling.
 
     >>> from lp.services.config import config
     >>> from_addr, to_add, raw_message = stub.test_emails.pop()
-    >>> sent_msg = email.message_from_string(raw_message)
+    >>> sent_msg = message_from_bytes(raw_message)
     >>> sent_msg['Return-Path'] == config.canonical.bounce_address
     True
     >>> sent_msg['Errors-To'] == config.canonical.bounce_address
@@ -298,7 +299,7 @@ It's possible to set Return-Path manually if needed.
     >>> transaction.commit()
 
     >>> from_addr, to_add, raw_message = stub.test_emails.pop()
-    >>> sent_msg = email.message_from_string(raw_message)
+    >>> sent_msg = message_from_bytes(raw_message)
     >>> sent_msg['Return-Path']
     '<>'
 
@@ -306,7 +307,7 @@ If we want to bounce messages, we can manually specify which addresses
 the mail should be sent to. When we do this, the 'To' and 'CC' headers
 are ignored.
 
-    >>> msg = email.mime.text.MIMEText("Some content")
+    >>> msg = MIMEText("Some content")
     >>> msg['From'] = 'foo.bar@xxxxxxxxxxxxx'
     >>> msg['To'] = 'test@xxxxxxxxxxxxx'
     >>> msg['CC'] = 'foo.bar@xxxxxxxxxxxxx'
@@ -319,7 +320,7 @@ are ignored.
     ...     print(to_addr)
     no-priv@xxxxxxxxxxxxx
 
-    >>> sent_msg = email.message_from_string(raw_message)
+    >>> sent_msg = message_from_bytes(raw_message)
     >>> sent_msg['To']
     'test@xxxxxxxxxxxxx'
     >>> sent_msg['CC']
@@ -329,7 +330,7 @@ Since sendmail() gets the addresses to send to from the email header,
 it needs to take care of unfolding the headers, so that they don't
 contain any line breaks.
 
-    >>> folded_message = email.message_from_string("""Subject: required
+    >>> folded_message = message_from_bytes(b"""Subject: required
     ... From: Not used
     ...  <from.address@xxxxxxxxxxx>
     ... To: To Address
diff --git a/lib/lp/services/mail/tests/test_stub.py b/lib/lp/services/mail/tests/test_stub.py
index 029fd61..fedb7d2 100644
--- a/lib/lp/services/mail/tests/test_stub.py
+++ b/lib/lp/services/mail/tests/test_stub.py
@@ -17,9 +17,9 @@ def test_simple_sendmail():
     r"""
     Send an email (faked by TestMailer - no actual email is sent)
 
-    >>> import email
     >>> from email.mime.text import MIMEText
     >>> import transaction
+    >>> from lp.services.compat import message_from_bytes
     >>> from lp.services.mail import stub
     >>> from lp.services.mail.sendmail import simple_sendmail
 
@@ -66,7 +66,7 @@ def test_simple_sendmail():
 
     >>> sorted_test_emails = sorted(list(stub.test_emails))
     >>> for from_addr, to_addrs, raw_message in sorted_test_emails:
-    ...     print(from_addr, to_addrs, 'nobody@xxxxxxxxxxx' in raw_message)
+    ...     print(from_addr, to_addrs, b'nobody@xxxxxxxxxxx' in raw_message)
     bounces@xxxxxxxxxxxxx ['nobody2@xxxxxxxxxxx'] True
     bounces@xxxxxxxxxxxxx ['nobody2@xxxxxxxxxxx'] False
 
@@ -78,7 +78,7 @@ def test_simple_sendmail():
 
     The message should be a sane RFC2822 document
 
-    >>> message = email.message_from_string(raw_message)
+    >>> message = message_from_bytes(raw_message)
     >>> message['From']
     'nobody@xxxxxxxxxxx'
     >>> message['To']
diff --git a/lib/lp/services/verification/doc/logintoken.txt b/lib/lp/services/verification/doc/logintoken.txt
index 14e6335..24e114f 100644
--- a/lib/lp/services/verification/doc/logintoken.txt
+++ b/lib/lp/services/verification/doc/logintoken.txt
@@ -46,9 +46,9 @@ Let's create a new LoginToken to confirm an email address for foobar.
 The email does not have a precedence header because the user implicitly
 requested it to complete their task.
 
-    >>> import email
+    >>> from lp.services.compat import message_from_bytes
 
-    >>> msg = email.message_from_string(found_msg)
+    >>> msg = message_from_bytes(found_msg)
     >>> print msg['Precedence']
     None
 
diff --git a/lib/lp/translations/doc/translations-export-to-branch.txt b/lib/lp/translations/doc/translations-export-to-branch.txt
index abdce05..5e07c5b 100644
--- a/lib/lp/translations/doc/translations-export-to-branch.txt
+++ b/lib/lp/translations/doc/translations-export-to-branch.txt
@@ -223,8 +223,8 @@ The Launchpad UI allows users to register branches in the Launchpad
 database without populating them in bzr.  Exporting to such a branch
 won't work, so we email a notification to the branch owner.
 
-    >>> from email import message_from_string
     >>> from lp.codehosting.vfs import get_rw_server
+    >>> from lp.services.compat import message_from_bytes
     >>> from lp.services.mail import stub
     >>> from lp.testing.factory import (
     ...     remove_security_proxy_and_shout_at_engineer)
@@ -255,7 +255,7 @@ won't work, so we email a notification to the branch owner.
     >>> transaction.commit()
 
     >>> sender, recipients, body = stub.test_emails.pop()
-    >>> message = message_from_string(body)
+    >>> message = message_from_bytes(body)
     >>> print(message['Subject'])
     Launchpad: translations branch has not been set up.