← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/soyuz-notifications-unicode into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/soyuz-notifications-unicode into lp:launchpad with lp:~wgrant/launchpad/maintainably-fix-safe-fix-maintainer as a prerequisite.

Commit message:
Rewrite Soyuz notification Changed-By/Maintainer/Signed-By handling to be less confusing and avoid unnecessarily RFC2047-encoding Signed-By in the body.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1479206 in Launchpad itself: "Upload notification body's Signed-By can be RFC2047-encoded"
  https://bugs.launchpad.net/launchpad/+bug/1479206

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/soyuz-notifications-unicode/+merge/266186

Rewrite Soyuz notification Changed-By/Maintainer/Signed-By handling to be less confusing and avoid unnecessarily RFC2047-encoding Signed-By in the body.

rfc2047_encode_address, introduced in the previous branch, dies. rfc822_encode_address could probably disappear in favour of email.utils.formataddr, but they handle special characters in very different ways.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/soyuz-notifications-unicode into lp:launchpad.
=== modified file 'lib/lp/archiveuploader/tests/test_utils.py'
--- lib/lp/archiveuploader/tests/test_utils.py	2015-07-29 07:12:25 +0000
+++ lib/lp/archiveuploader/tests/test_utils.py	2015-07-29 07:12:25 +0000
@@ -147,77 +147,65 @@
         """
         from lp.archiveuploader.utils import (
             parse_maintainer_bytes,
-            rfc2047_encode_address,
             rfc822_encode_address,
             )
         cases = (
             ("No\xc3\xa8l K\xc3\xb6the <noel@xxxxxxxxxx>",
              u"No\xe8l K\xf6the <noel@xxxxxxxxxx>",
-             u"=?utf-8?b?Tm/DqGwgS8O2dGhl?= <noel@xxxxxxxxxx>",
              u"No\xe8l K\xf6the",
              u"noel@xxxxxxxxxx"),
 
             ("No\xe8l K\xf6the <noel@xxxxxxxxxx>",
              u"No\xe8l K\xf6the <noel@xxxxxxxxxx>",
-             u"=?utf-8?b?Tm/DqGwgS8O2dGhl?= <noel@xxxxxxxxxx>",
              u"No\xe8l K\xf6the",
              u"noel@xxxxxxxxxx"),
 
             ("James Troup <james@xxxxxxxxxx>",
              u"James Troup <james@xxxxxxxxxx>",
-             u"James Troup <james@xxxxxxxxxx>",
              u"James Troup",
              u"james@xxxxxxxxxx"),
 
             ("James J. Troup <james@xxxxxxxxxx>",
              u"james@xxxxxxxxxx (James J. Troup)",
-             u"james@xxxxxxxxxx (James J. Troup)",
              u"James J. Troup",
              u"james@xxxxxxxxxx"),
 
             ("James J, Troup <james@xxxxxxxxxx>",
              u"james@xxxxxxxxxx (James J, Troup)",
-             u"james@xxxxxxxxxx (James J, Troup)",
              u"James J, Troup",
              u"james@xxxxxxxxxx"),
 
             ("james@xxxxxxxxxx",
              u" <james@xxxxxxxxxx>",
-             u" <james@xxxxxxxxxx>",
              u"",
              u"james@xxxxxxxxxx"),
 
             ("<james@xxxxxxxxxx>",
              u" <james@xxxxxxxxxx>",
-             u" <james@xxxxxxxxxx>",
              u"",
              u"james@xxxxxxxxxx"),
 
             ("Cris van Pelt <\"Cris van Pelt\"@tribe.eu.org>",
              u"Cris van Pelt <\"Cris van Pelt\"@tribe.eu.org>",
-             u"Cris van Pelt <\"Cris van Pelt\"@tribe.eu.org>",
              u"Cris van Pelt",
              u"\"Cris van Pelt\"@tribe.eu.org"),
 
             ("Zak B. Elep <zakame@xxxxxxxxxx>",
              u"zakame@xxxxxxxxxx (Zak B. Elep)",
-             u"zakame@xxxxxxxxxx (Zak B. Elep)",
              u"Zak B. Elep",
              u"zakame@xxxxxxxxxx"),
 
             ("zakame@xxxxxxxxxx (Zak B. Elep)",
              u" <zakame@xxxxxxxxxx (Zak B. Elep)>",
-             u" <zakame@xxxxxxxxxx (Zak B. Elep)>",
              u"",
              u"zakame@xxxxxxxxxx (Zak B. Elep)"),
              )
 
         for case in cases:
             (name, email) = parse_maintainer_bytes(case[0], 'Maintainer')
-            self.assertEquals(case[3], name)
-            self.assertEquals(case[4], email)
+            self.assertEquals(case[2], name)
+            self.assertEquals(case[3], email)
             self.assertEquals(case[1], rfc822_encode_address(name, email))
-            self.assertEquals(case[2], rfc2047_encode_address(name, email))
 
     def testParseMaintainerRaises(self):
         """lp.archiveuploader.utils.parse_maintainer should raise on incorrect

=== modified file 'lib/lp/archiveuploader/utils.py'
--- lib/lp/archiveuploader/utils.py	2015-07-29 07:12:25 +0000
+++ lib/lp/archiveuploader/utils.py	2015-07-29 07:12:25 +0000
@@ -25,7 +25,6 @@
     're_valid_pkg_name',
     're_changes_file_name',
     're_extract_src_version',
-    'rfc2047_encode_address',
     'rfc822_encode_address',
     'UploadError',
     'UploadWarning',
@@ -33,7 +32,6 @@
 
 
 from collections import defaultdict
-from email.header import Header
 import os
 import re
 import signal
@@ -223,9 +221,11 @@
 
     name and email must be Unicode. If they contain non-ASCII
     characters, the result is not RFC822-compliant and you should use
-    rfc2047_encode_address instead.
+    something like format_address instead.
 
-    If the name field contains '.' or ',' the 'email (name)' format is used.
+    This is similar to email.utils.format_addr, except that it handles
+    special characters using the 'email (name)' format rather than
+    '"name" (email)'.
     """
     # If the maintainer's name contains a full stop then the whole field will
     # not work directly as an email address due to a misfeature in the syntax
@@ -237,22 +237,6 @@
         return u"%s <%s>" % (name, email)
 
 
-def rfc2047_encode_address(name, email):
-    """Return an RFC2047 encoding of a name and an email address.
-
-    name and email must be Unicode strings, and email must be
-    ASCII-only.
-
-    If the name field contains '.' or ',' the 'email (name)' format is used.
-    """
-    try:
-        email.encode('ascii')
-    except UnicodeDecodeError:
-        raise AssertionError("Email addresses must be ASCII.")
-    return rfc822_encode_address(
-        Header(name, 'utf-8', 998).encode().decode('ascii'), email)
-
-
 def extract_dpkg_source(dsc_filepath, target, vendor=None):
     """Extract a source package by dsc file path.
 

=== modified file 'lib/lp/soyuz/adapters/notification.py'
--- lib/lp/soyuz/adapters/notification.py	2015-07-29 07:12:25 +0000
+++ lib/lp/soyuz/adapters/notification.py	2015-07-29 07:12:25 +0000
@@ -12,7 +12,6 @@
 
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
-from email.utils import formataddr
 import os
 
 from zope.component import getUtility
@@ -23,7 +22,6 @@
 from lp.archiveuploader.utils import (
     parse_maintainer_bytes,
     ParseMaintError,
-    rfc2047_encode_address,
     rfc822_encode_address,
     )
 from lp.registry.interfaces.person import IPersonSet
@@ -231,11 +229,11 @@
 
     info = fetch_information(spr, bprs, changes)
     from_addr = info['changedby']
-    from_email = info['changedby_email']
-    if announce_from_person is not None:
-        if announce_from_person.preferredemail is not None:
-            from_addr = format_address_for_person(announce_from_person)
-            from_email = announce_from_person.preferredemail.email
+    if (announce_from_person is not None
+            and announce_from_person.preferredemail is not None):
+        from_addr = (
+            announce_from_person.displayname,
+            announce_from_person.preferredemail.email)
 
     # If we're sending an acceptance notification for a non-PPA upload,
     # announce if possible. Avoid announcing backports, binary-only
@@ -244,7 +242,7 @@
         and not archive.is_ppa
         and pocket != PackagePublishingPocket.BACKPORTS
         and not (pocket == PackagePublishingPocket.SECURITY and spr is None)
-        and not is_auto_sync_upload(spr, bprs, pocket, from_email)):
+        and not is_auto_sync_upload(spr, bprs, pocket, from_addr)):
         name = None
         bcc_addr = None
         if spr:
@@ -257,8 +255,9 @@
                 bcc_addr = email_base.format(package_name=name)
 
         build_and_send_mail(
-            'announcement', [str(distroseries.changeslist)], from_addr,
-            bcc_addr, previous_version=previous_version)
+            'announcement', [str(distroseries.changeslist)],
+            format_address(*from_addr) if from_addr else None, bcc_addr,
+            previous_version=previous_version)
 
 
 def assemble_body(blamer, spr, bprs, archive, distroseries, summary, changes,
@@ -286,9 +285,22 @@
     if spr:
         information['SPR_URL'] = canonical_url(
             distroseries.distribution.getSourcePackageRelease(spr))
-    changedby_displayname = info['changedby_displayname']
-    if changedby_displayname:
-        information['CHANGEDBY'] = '\nChanged-By: %s' % changedby_displayname
+
+    # Some syncs (e.g. from Debian) will involve packages whose
+    # changed-by person was auto-created in LP and hence does not have a
+    # preferred email address set.  We'll get a None here.
+    changedby_person = addr_to_person(info['changedby'])
+    if info['changedby']:
+        information['CHANGEDBY'] = (
+            '\nChanged-By: %s' % rfc822_encode_address(*info['changedby']))
+    if (blamer is not None and blamer != changedby_person
+            and blamer.preferredemail):
+        information['SIGNER'] = '\nSigned-By: %s' % rfc822_encode_address(
+            blamer.displayname, blamer.preferredemail.email)
+    if info['maintainer'] and info['maintainer'] != info['changedby']:
+        information['MAINTAINER'] = (
+            '\nMaintainer: %s' % rfc822_encode_address(*info['maintainer']))
+
     origin = changes.get('Origin')
     if origin:
         information['ORIGIN'] = '\nOrigin: %s' % origin
@@ -299,20 +311,6 @@
         information['ANNOUNCE'] = "Announcing to %s" % (
             distroseries.changeslist)
 
-    # Some syncs (e.g. from Debian) will involve packages whose
-    # changed-by person was auto-created in LP and hence does not have a
-    # preferred email address set.  We'll get a None here.
-    changedby_person = email_to_person(info['changedby_email'])
-
-    if blamer is not None and blamer != changedby_person:
-        signer_signature = person_to_email(blamer)
-        if signer_signature != info['changedby']:
-            information['SIGNER'] = '\nSigned-By: %s' % signer_signature
-    # Add maintainer if present and different from changed-by.
-    maintainer_displayname = info['maintainer_displayname']
-    if (maintainer_displayname and
-            maintainer_displayname != changedby_displayname):
-        information['MAINTAINER'] = '\nMaintainer: %s' % maintainer_displayname
     return get_template(archive, action) % information
 
 
@@ -463,8 +461,8 @@
     candidate_recipients = [blamer]
     info = fetch_information(spr, bprs, changes)
 
-    changer = email_to_person(info['changedby_email'])
-    maintainer = email_to_person(info['maintainer_email'])
+    changer = addr_to_person(info['changedby'])
+    maintainer = addr_to_person(info['maintainer'])
 
     if blamer is None and not archive.is_copy:
         debug(logger, "Changes file is unsigned; adding changer as recipient.")
@@ -557,55 +555,26 @@
     return summary
 
 
-def email_to_person(email):
-    """Return an `IPerson` given an email address (without a name).
+def addr_to_person(addr):
+    """Return an `IPerson` given a name and email address.
 
-    :param email: Potential email address.
+    :param addr: (name, email) tuple. The name is ignored.
     :return: `IPerson` with the given email address.  None if there
-        isn't one, or if `email` is None.
+        isn't one, or if `addr` is None.
     """
-    if not email:
+    if addr is None:
         return None
-    return getUtility(IPersonSet).getByEmail(email)
-
-
-def person_to_email(person):
-    """Return a string of full name <email address> given an IPerson."""
-    if person and person.preferredemail:
-        # This will use email.header to encode any non-ASCII characters.
-        return format_address_for_person(person)
-
-
-def fix_email(fullemail, field_name):
-    """Turn an email address from .changes into various useful forms.
-
-    The input address may be None, or anything that `parse_maintainer_bytes`
-    understands.
-
-    :return: A tuple of (RFC2047-compatible address, Unicode
-        RFC822-compatible address, email).
-    """
-    if not fullemail:
-        return None, None, None
-
-    try:
-        name, email = parse_maintainer_bytes(fullemail, field_name)
-        return (
-            rfc2047_encode_address(name, email).encode('utf-8'),
-            rfc822_encode_address(name, email),
-            email.encode('ascii'))
-    except ParseMaintError:
-        return None, None, None
-
-
-def is_auto_sync_upload(spr, bprs, pocket, changed_by_email):
+    return getUtility(IPersonSet).getByEmail(addr[1])
+
+
+def is_auto_sync_upload(spr, bprs, pocket, changed_by):
     """Return True if this is a (Debian) auto sync upload.
 
     Sync uploads are source-only, unsigned and not targeted to
     the security pocket. The Changed-By field is also the Katie
     user (archive@xxxxxxxxxx).
     """
-    changed_by = email_to_person(changed_by_email)
+    changed_by = addr_to_person(changed_by)
     return (
         spr and
         not bprs and
@@ -614,50 +583,40 @@
 
 
 def fetch_information(spr, bprs, changes, previous_version=None):
-    changedby = None
-    changedby_displayname = None
-    changedby_email = None
-    maintainer = None
-    maintainer_displayname = None
-    maintainer_email = None
+    changelog = date = changedby = maintainer = None
 
     if changes:
-        changesfile = ChangesFile.formatChangesComment(
+        changelog = ChangesFile.formatChangesComment(
             sanitize_string(changes.get('Changes')))
         date = changes.get('Date')
-        changedby, changedby_displayname, changedby_email = fix_email(
-            changes.get('Changed-By'), 'Changed-By')
-        maintainer, maintainer_displayname, maintainer_email = fix_email(
-            changes.get('Maintainer'), 'Maintainer')
+        try:
+            changedby = parse_maintainer_bytes(
+                changes.get('Changed-By'), 'Changed-By')
+        except ParseMaintError:
+            pass
+        try:
+            maintainer = parse_maintainer_bytes(
+                changes.get('Maintainer'), 'Maintainer')
+        except ParseMaintError:
+            pass
     elif spr or bprs:
         if not spr and bprs:
             spr = bprs[0].build.source_package_release
-        changesfile = spr.aggregate_changelog(previous_version)
+        changelog = spr.aggregate_changelog(previous_version)
         date = spr.dateuploaded
-        changedby = person_to_email(spr.creator)
-        maintainer = person_to_email(spr.maintainer)
-        if changedby:
-            addr = formataddr((spr.creator.displayname,
-                               spr.creator.preferredemail.email))
-            changedby_displayname = sanitize_string(addr)
-            changedby_email = spr.creator.preferredemail.email
-        if maintainer:
-            addr = formataddr((spr.maintainer.displayname,
-                               spr.maintainer.preferredemail.email))
-            maintainer_displayname = sanitize_string(addr)
-            maintainer_email = spr.maintainer.preferredemail.email
-    else:
-        changesfile = date = None
+        if spr.creator and spr.creator.preferredemail:
+            changedby = (
+                spr.creator.displayname, spr.creator.preferredemail.email)
+        if spr.maintainer and spr.maintainer.preferredemail:
+            maintainer = (
+                spr.maintainer.displayname,
+                spr.maintainer.preferredemail.email)
 
     return {
-        'changelog': changesfile,
+        'changelog': changelog,
         'date': date,
         'changedby': changedby,
-        'changedby_displayname': changedby_displayname,
-        'changedby_email': changedby_email,
         'maintainer': maintainer,
-        'maintainer_displayname': maintainer_displayname,
-        'maintainer_email': maintainer_email,
         }
 
 

=== modified file 'lib/lp/soyuz/adapters/tests/test_notification.py'
--- lib/lp/soyuz/adapters/tests/test_notification.py	2014-11-08 23:53:17 +0000
+++ lib/lp/soyuz/adapters/tests/test_notification.py	2015-07-29 07:12:25 +0000
@@ -5,7 +5,6 @@
 # Copyright 2011-2014 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from email.utils import formataddr
 from textwrap import dedent
 
 from storm.store import Store
@@ -24,7 +23,6 @@
     get_upload_notification_recipients,
     is_auto_sync_upload,
     notify,
-    person_to_email,
     reject_changes_file,
     )
 from lp.soyuz.enums import (
@@ -54,15 +52,14 @@
     def test_notify_from_unicode_names(self):
         # People with unicode in their names should appear correctly in the
         # email and not get smashed to ASCII or otherwise transliterated.
-        RANDOM_UNICODE = u"Loïc"
-        creator = self.factory.makePerson(displayname=RANDOM_UNICODE)
+        creator = self.factory.makePerson(displayname=u"Loïc")
         spr = self.factory.makeSourcePackageRelease(creator=creator)
         self.factory.makeSourcePackageReleaseFile(sourcepackagerelease=spr)
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
         pocket = PackagePublishingPocket.RELEASE
         distroseries = self.factory.makeDistroSeries()
         distroseries.changeslist = "blah@xxxxxxxxxxx"
-        blamer = self.factory.makePerson()
+        blamer = self.factory.makePerson(displayname=u"Stéphane")
         notify(
             blamer, spr, [], [], archive, distroseries, pocket,
             action='accepted')
@@ -70,7 +67,8 @@
         self.assertEqual(2, len(notifications))
         msg = notifications[1].get_payload(0)
         body = msg.get_payload(decode=True)
-        self.assertIn("Loïc", body)
+        self.assertIn("Changed-By: Loïc", body)
+        self.assertIn("Signed-By: Stéphane", body)
 
     def test_calculate_subject_customfile(self):
         lfa = self.factory.makeLibraryFileAlias()
@@ -196,14 +194,15 @@
         archive = self.factory.makeArchive(owner=person, name='ppa')
         pocket = self.factory.getAnyPocket()
         distroseries = self.factory.makeDistroSeries()
-        person = self.factory.makePerson()
+        person = self.factory.makePerson(
+            displayname=u'Blamer', email='blamer@xxxxxxxxxxx')
         notify(
             person, None, [bpr], [], archive, distroseries, pocket,
             summary_text="Rejected by archive administrator.",
             action='rejected')
         [notification] = pop_notifications()
         body = notification.get_payload()[0].get_payload()
-        self.assertEqual(person_to_email(person), notification['To'])
+        self.assertEqual('Blamer <blamer@xxxxxxxxxxx>', notification['To'])
         expected_body = dedent("""\
             Rejected:
             Rejected by archive administrator.
@@ -242,11 +241,9 @@
         fields = [
             info['changedby'],
             info['maintainer'],
-            info['changedby_displayname'],
-            info['maintainer_displayname'],
             ]
         for field in fields:
-            self.assertEqual('Foo Bar <foo.bar@xxxxxxxxxxx>', field)
+            self.assertEqual((u'Foo Bar', u'foo.bar@xxxxxxxxxxx'), field)
 
     def test_fetch_information_spr(self):
         creator = self.factory.makePerson(displayname=u"foø")
@@ -257,15 +254,9 @@
         self.assertEqual(info['date'], spr.dateuploaded)
         self.assertEqual(info['changelog'], spr.changelog_entry)
         self.assertEqual(
-            info['changedby'], format_address_for_person(spr.creator))
-        self.assertEqual(
-            info['maintainer'], format_address_for_person(spr.maintainer))
-        self.assertEqual(
-            u"foø <%s>" % spr.creator.preferredemail.email,
-            info['changedby_displayname'])
-        self.assertEqual(
-            u"bær <%s>" % spr.maintainer.preferredemail.email,
-            info['maintainer_displayname'])
+            (u"foø", spr.creator.preferredemail.email), info['changedby'])
+        self.assertEqual(
+            (u"bær", spr.maintainer.preferredemail.email), info['maintainer'])
 
     def test_fetch_information_bprs(self):
         bpr = self.factory.makeBinaryPackageRelease()
@@ -274,17 +265,11 @@
         self.assertEqual(info['date'], spr.dateuploaded)
         self.assertEqual(info['changelog'], spr.changelog_entry)
         self.assertEqual(
-            info['changedby'], format_address_for_person(spr.creator))
-        self.assertEqual(
-            info['maintainer'], format_address_for_person(spr.maintainer))
-        self.assertEqual(
-            info['changedby_displayname'],
-            formataddr((spr.creator.displayname,
-                        spr.creator.preferredemail.email)))
-        self.assertEqual(
-            info['maintainer_displayname'],
-            formataddr((spr.maintainer.displayname,
-                        spr.maintainer.preferredemail.email)))
+            (spr.creator.displayname, spr.creator.preferredemail.email),
+            info['changedby'])
+        self.assertEqual(
+            (spr.maintainer.displayname, spr.maintainer.preferredemail.email),
+            info['maintainer'])
 
     def test_calculate_subject_spr(self):
         spr = self.factory.makeSourcePackageRelease()
@@ -461,5 +446,5 @@
         # If changer has no preferred email address,
         # is_auto_sync_upload should still work.
         result = is_auto_sync_upload(
-            spr=None, bprs=None, pocket=None, changed_by_email=None)
+            spr=None, bprs=None, pocket=None, changed_by=None)
         self.assertFalse(result)

=== modified file 'lib/lp/soyuz/doc/soyuz-set-of-uploads.txt'
--- lib/lp/soyuz/doc/soyuz-set-of-uploads.txt	2015-07-08 19:20:29 +0000
+++ lib/lp/soyuz/doc/soyuz-set-of-uploads.txt	2015-07-29 07:12:25 +0000
@@ -337,7 +337,7 @@
     Date: Tue, 25 Apr 2006 10:36:14 -0300
     Changed-By: cprov@xxxxxxxxxx (Celso R. Providelo)
     Maintainer: Launchpad team <launchpad@xxxxxxxxxxxxxxxxxxx>
-    Signed-By: "Foo B. Bar" <foo.bar@xxxxxxxxxxxxx>
+    Signed-By: foo.bar@xxxxxxxxxxxxx (Foo B. Bar)
     http://launchpad.dev/ubuntutest/+source/bar/1.0-4
     <BLANKLINE>
     =3D=3D


Follow ups