launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19231
[Merge] lp:~cjwatson/launchpad/upload-mail into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/upload-mail into lp:launchpad.
Commit message:
Convert package upload notifications to BaseMailer.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #117155 in Launchpad itself: "soyuz emails are vague about why they are being sent to a given user"
https://bugs.launchpad.net/launchpad/+bug/117155
Bug #127917 in Launchpad itself: "soyuz emails lack X-LP-Message-Rationale"
https://bugs.launchpad.net/launchpad/+bug/127917
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/upload-mail/+merge/269066
Convert package upload notifications to BaseMailer.
The main effect of this is that each recipient now receives a separate mail. This allows us to set X-Launchpad-Message-Rationale headers and use a much less vague footer.
Other minor effects:
* There's now an "X-Launchpad-Notification-Type: package-upload" header.
* Rejection mails without any spr, bprs, or customfiles (e.g. rejections from archiveuploader) are a little more uniform: as well as the above header changes, the last word changes from "rejected" to "(Rejected)".
* PPA upload notifications have a proper footer separator ("-- " rather than "--").
* PPA upload notifications are no longer unnecessarily multipart.
The changes list required some special handling, because it doesn't correspond to a Person. I added a StubPerson to the notification recipient set edifice to cope with this.
X-LP-M-R: Requester and "... because you made this upload" are intentionally a little vague, because at that point it could be either the signer of a direct upload or the requester of a sync.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/upload-mail into lp:launchpad.
=== modified file 'lib/lp/archiveuploader/tests/nascentupload-announcements.txt'
--- lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2015-07-08 16:52:34 +0000
+++ lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2015-08-25 14:09:28 +0000
@@ -101,9 +101,8 @@
DEBUG above if files already exist in other distroseries.
...
DEBUG --
- DEBUG You are receiving this email because you are the uploader,
- maintainer or
- DEBUG signer of the above package.
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
There is only one email generated:
@@ -143,7 +142,7 @@
DEBUG bar diff from 1.0-1 to 1.0-1 requested
DEBUG Setting it to ACCEPTED
...
- DEBUG Sending rejection email.
+ DEBUG Sent a mail:
...
DEBUG Rejected:
DEBUG The source bar - 1.0-1 is already accepted in ubuntu/hoary
@@ -206,8 +205,8 @@
<BLANKLINE>
-- =
<BLANKLINE>
- You are receiving this email because you are the uploader, maintainer or
- signer of the above package.
+ You are receiving this email because you are the most recent person
+ listed in this package's changelog.
<BLANKLINE>
In order to facilitate automated processing of announcement emails, the
@@ -215,10 +214,10 @@
>>> attachment = notification.get_payload()[1]
>>> print attachment.as_string() # doctest: -NORMALIZE_WHITESPACE
+ Content-Disposition: attachment; filename="changesfile"
+ MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
- MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
- Content-Disposition: attachment; filename="changesfile"
<BLANKLINE>
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
@@ -296,10 +295,8 @@
However, PPA upload notifications do not contain an attachment with the
original changesfile.
- >>> attachment = notification.get_payload()[1]
- Traceback (most recent call last):
- ...
- IndexError: list index out of range
+ >>> notification.is_multipart()
+ False
See further tests upon PPA upload notifications on
archiveuploader/tests/test_ppauploadprocessor.
@@ -348,8 +345,7 @@
>>> lang_pack.logger = FakeLogger()
>>> result = lang_pack.do_accept()
DEBUG Creating queue entry
- DEBUG Skipping acceptance and announcement, it is a language-package
- upload.
+ DEBUG Skipping acceptance and announcement for language packs.
>>> lang_pack.queue_root.status.name
'NEW'
@@ -381,8 +377,7 @@
>>> result = lang_pack.do_accept()
DEBUG Creating queue entry
...
- DEBUG Skipping acceptance and announcement, it is a language-package
- upload.
+ DEBUG Skipping acceptance and announcement for language packs.
>>> lang_pack.queue_root.status.name
'DONE'
@@ -412,22 +407,21 @@
DEBUG Creating queue entry
DEBUG language-pack-pt diff from 1.0-2 to 1.0-3 requested
DEBUG Setting it to UNAPPROVED
- DEBUG Skipping acceptance and announcement, it is a language-package
- upload.
+ DEBUG Skipping acceptance and announcement for language packs.
>>> lang_pack.queue_root.status.name
'UNAPPROVED'
UNAPPROVED message was also skipped for an upload targeted to
-'translation' section:
-
+'translation' section:
>>> transaction.commit()
>>> len(stub.test_emails)
0
-An UNAPPROVED binary upload via insecure will send one email saying that
-the upload is waiting for approval:
+An UNAPPROVED binary upload via insecure will send emails (in this case, one
+to the signer and one to the changer) saying that the upload is waiting for
+approval:
>>> bar_src = NascentUpload.from_changesfile_path(
... datadir('suite/bar_1.0-2/bar_1.0-2_source.changes'),
@@ -439,16 +433,21 @@
DEBUG Creating queue entry
...
- >>> [notification] = pop_notifications()
-
- >>> notification['X-Katie']
- 'Launchpad actually'
-
- >>> print_addrlist(notification['To'])
+ >>> changer_notification, signer_notification = pop_notifications()
+
+ >>> changer_notification['X-Katie']
+ 'Launchpad actually'
+ >>> signer_notification['X-Katie']
+ 'Launchpad actually'
+
+ >>> print_addrlist(changer_notification['To'])
Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+ >>> print_addrlist(signer_notification['To'])
Foo Bar <foo.bar@xxxxxxxxxxxxx>
- >>> notification['Subject']
+ >>> changer_notification['Subject']
+ '[ubuntu/hoary-updates] bar 1.0-2 (Waiting for approval)'
+ >>> signer_notification['Subject']
'[ubuntu/hoary-updates] bar 1.0-2 (Waiting for approval)'
And clean up.
@@ -457,7 +456,7 @@
>>> upload_data = datadir('suite/bar_1.0-2')
>>> os.remove(os.path.join(upload_data, 'bar_1.0.orig.tar.gz'))
-UNAPPROVED upload to BACKPORTS via insecure policy will send a notification
+UNAPPROVED upload to BACKPORTS via insecure policy will send notifications
saying they are waiting for approval:
>>> unapproved_backports_policy = getPolicy(
@@ -475,16 +474,21 @@
DEBUG Setting it to UNAPPROVED
...
- >>> [notification] = pop_notifications()
-
- >>> notification['X-Katie']
- 'Launchpad actually'
-
- >>> print_addrlist(notification['To'])
+ >>> changer_notification, signer_notification = pop_notifications()
+
+ >>> changer_notification['X-Katie']
+ 'Launchpad actually'
+ >>> signer_notification['X-Katie']
+ 'Launchpad actually'
+
+ >>> print_addrlist(changer_notification['To'])
Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+ >>> print_addrlist(signer_notification['To'])
Foo Bar <foo.bar@xxxxxxxxxxxxx>
- >>> notification['Subject']
+ >>> changer_notification['Subject']
+ '[ubuntu/hoary-backports] bar 1.0-3 (Waiting for approval)'
+ >>> signer_notification['Subject']
'[ubuntu/hoary-backports] bar 1.0-3 (Waiting for approval)'
AUTO-APPROVED upload to BACKPORTS pocket via 'sync' policy:
@@ -528,9 +532,8 @@
DEBUG Thank you for your contribution to Ubuntu.
DEBUG
DEBUG --
- DEBUG You are receiving this email because you are the uploader,
- maintainer or
- DEBUG signer of the above package.
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
There is one email generated:
@@ -662,9 +665,8 @@
...
DEBUG Announcing to hoary-announce@xxxxxxxxxxxxxxxx
...
- DEBUG You are receiving this email because you are the uploader, maintainer
- or
- DEBUG signer of the above package.
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
DEBUG Would have sent a mail:
DEBUG Subject: [ubuntu/hoary] bar 1.0-6 (Accepted)
DEBUG Sender: Celso Providelo <cprov@xxxxxxxxxx>
@@ -698,10 +700,11 @@
>>> msgs = pop_notifications(sort_key=operator.itemgetter('To'))
>>> len(msgs)
- 2
+ 3
>>> [message['From'].replace('\n ', ' ') for message in msgs]
- ['Root <root@localhost>', '=?utf-8?q?Non-ascii_changed-by_=C4=8Ciha=C5=99?=
+ ['Root <root@localhost>', 'Root <root@localhost>',
+ '=?utf-8?q?Non-ascii_changed-by_=C4=8Ciha=C5=99?=
<daniel.silverstone@xxxxxxxxxxxxx>']
UTF-8 text in the changes file that is sent on the email is preserved
@@ -746,8 +749,8 @@
<BLANKLINE>
-- =
<BLANKLINE>
- You are receiving this email because you are the uploader, maintainer or
- signer of the above package.
+ You are receiving this email because you are the most recent person
+ listed in this package's changelog.
<BLANKLINE>
In order to facilitate scripts that parse announcement emails, the changes
@@ -763,10 +766,10 @@
And what follows is the content of the attachment.
>>> print attachment.as_string() # doctest: -NORMALIZE_WHITESPACE
+ Content-Disposition: attachment; filename="changesfile"
+ MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
- MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
- Content-Disposition: attachment; filename="changesfile"
<BLANKLINE>
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
@@ -832,7 +835,6 @@
>>> result = bar_src.do_accept()
DEBUG Building recipients list.
...
- DEBUG Sending rejection email.
DEBUG Sent a mail:
...
DEBUG Rejected:
@@ -844,9 +846,8 @@
DEBUG http://answers.launchpad.net/soyuz
DEBUG
DEBUG --
- DEBUG You are receiving this email because you are the uploader,
- maintainer or
- DEBUG signer of the above package.
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
>>> [notification] = pop_notifications()
=== modified file 'lib/lp/archiveuploader/tests/test_ppauploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2015-04-20 15:59:52 +0000
+++ lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2015-08-25 14:09:28 +0000
@@ -73,56 +73,48 @@
def makeArchive(self, owner):
return self.factory.makeArchive(owner=owner, name='ppa')
- def assertEmail(self, contents=None, recipients=None, ppa_header='name16'):
- """Check email last upload notification attributes.
+ def assertEmails(self, expected):
+ """Check recent email upload notification attributes.
- :param: contents: can be a list of one or more lines, if passed
- they will be checked against the lines in Subject + Body.
- :param: recipients: can be a list of recipients lines, it defaults
- to 'Foo Bar <foo.bar@xxxxxxxxxxxxx>' (name16 account) and
- should match the email To: header content.
- :param: ppa_header: is the content of the 'X-Launchpad-PPA' header,
- it defaults to 'name16' and should be explicitly set to None for
- non-PPA or rejection notifications.
+ :param expected: A list of dicts, each of which represents an
+ expected email and may have "contents", "recipient", and
+ "ppa_header" keys. All the items in expected must match in the
+ correct order, with none left over. "contents" is a list of
+ lines; assert that each is in Subject + Body. "recipient" is
+ the To address the email must have, defaulting to
+ "foo.bar@xxxxxxxxxxxxx" which is the signer on most of the test
+ data uploads. "ppa_header" is the content of the
+ "X-Launchpad-PPA" header; it defaults to "name16" and should be
+ explicitly set to None for non-PPA or rejection notifications.
"""
- if not recipients:
- recipients = [self.name16_recipient]
-
- if not contents:
- contents = []
-
- queue_size = len(stub.test_emails)
- messages = "\n".join(m for f, t, m in stub.test_emails)
- self.assertEqual(
- queue_size, 1, 'Unexpected number of emails sent: %s\n%s'
- % (queue_size, messages))
-
- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
- msg = message_from_string(raw_msg)
-
- # This is now a MIMEMultipart message.
- body = msg.get_payload(0)
- body = body.get_payload(decode=True)
-
- clean_recipients = [r.strip() for r in to_addrs]
- for recipient in list(recipients):
- self.assertTrue(
- recipient in clean_recipients,
- "%s not in %s" % (recipient, clean_recipients))
- self.assertEqual(
- len(recipients), len(clean_recipients),
- "Email recipients do not match exactly. Expected %s, got %s" %
- (recipients, clean_recipients))
-
- subject = "Subject: %s\n" % msg['Subject']
- body = subject + body
-
- for content in list(contents):
- self.assertIn(content, body)
-
- if ppa_header is not None:
- self.assertIn('X-Launchpad-PPA', msg.keys())
- self.assertEqual(msg['X-Launchpad-PPA'], ppa_header)
+ for item in expected:
+ recipient = item.get("recipient", self.name16_recipient)
+ contents = item.get("contents", [])
+ ppa_header = item.get("ppa_header", "name16")
+
+ from_addr, to_addrs, raw_msg = stub.test_emails.pop()
+ msg = message_from_string(raw_msg)
+
+ # This is now a non-multipart message.
+ self.assertFalse(msg.is_multipart())
+ body = msg.get_payload(decode=True)
+
+ clean_recipients = [r.strip() for r in to_addrs]
+ self.assertContentEqual([recipient], clean_recipients)
+
+ subject = "Subject: %s\n" % msg['Subject']
+ body = subject + body
+
+ for content in list(contents):
+ self.assertIn(content, body)
+
+ if ppa_header is not None:
+ self.assertIn('X-Launchpad-PPA', msg.keys())
+ self.assertEqual(msg['X-Launchpad-PPA'], ppa_header)
+
+ self.assertEqual(
+ [], stub.test_emails,
+ "%d emails left over" % len(stub.test_emails))
def checkFilesRestrictedInLibrarian(self, queue_item, condition):
"""Check the libraryfilealias restricted flag.
@@ -255,7 +247,7 @@
# it's the default PPA.
contents = [
"Subject: [~name16/ubuntu/ppa/breezy] bar 1.0-1 (Accepted)"]
- self.assertEmail(contents, ppa_header='name16')
+ self.assertEmails([{"contents": contents, "ppa_header": "name16"}])
def testNamedPPAUploadNonDefault(self):
"""Test PPA uploads to a named PPA."""
@@ -273,7 +265,8 @@
# Subject and PPA email-header are specific for this named-ppa.
contents = [
"Subject: [~name16/ubuntu/testing/breezy] bar 1.0-1 (Accepted)"]
- self.assertEmail(contents, ppa_header='name16-testing')
+ self.assertEmails(
+ [{"contents": contents, "ppa_header": "name16-testing"}])
def testNamedPPAUploadWithSeries(self):
"""Test PPA uploads to a named PPA location and with a distroseries.
@@ -447,7 +440,7 @@
# name16 is Foo Bar, who signed the upload. The package that was
# uploaded also contains two other valid (in sampledata) email
# addresses for maintainer and changed-by which must be ignored.
- self.assertEmail()
+ self.assertEmails([{}])
def testUploadSendsEmailToPeopleInArchivePermissions(self):
"""PPA uploads result in notifications to ArchivePermission uploaders.
@@ -474,17 +467,17 @@
upload_dir = self.queueUpload("bar_1.0-1", "~cprov/ppa/ubuntu")
self.processUpload(self.uploadprocessor, upload_dir)
- name12_email = "%s <%s>" % (
- name12.displayname, name12.preferredemail.email)
- team_email = "%s <%s>" % (team.displayname, team.preferredemail.email)
-
# We expect the recipients to be:
- # - the package signer (name15),
+ # - the package signer (name16),
# - the team in the extra permissions,
# - name12 who is in the extra permissions.
expected_recipients = (
- self.name16_recipient, name12_email, team_email)
- self.assertEmail(ppa_header="cprov", recipients=expected_recipients)
+ self.name16_recipient,
+ team.preferredemail.email,
+ name12.preferredemail.email)
+ self.assertEmails([
+ {"ppa_header": "cprov", "recipient": expected_recipient}
+ for expected_recipient in reversed(sorted(expected_recipients))])
def testPPADistroSeriesOverrides(self):
"""It's possible to override target distroseries of PPA uploads.
@@ -805,17 +798,15 @@
'previous error.'], rejection_message.splitlines())
contents = [
- "Subject: [~cprov/ubuntu/ppa] bar_1.0-1_source.changes rejected",
+ "Subject: [~cprov/ubuntu/ppa] bar_1.0-1_source.changes (Rejected)",
"Could not find person or team named 'boing'",
"https://help.launchpad.net/Packaging/PPA/Uploading",
"If you don't understand why your files were rejected please "
"send an email",
("to %s for help (requires membership)."
% config.launchpad.users_address),
- "You are receiving this email because you are the uploader "
- "of the above",
- "PPA package."]
- self.assertEmail(contents, ppa_header=None)
+ "You are receiving this email because you made this upload."]
+ self.assertEmails([{"contents": contents, "ppa_header": None}])
class TestPPAUploadProcessorFileLookups(TestPPAUploadProcessorBase):
@@ -1003,8 +994,7 @@
# Also, the email generated should be sane.
from_addr, to_addrs, raw_msg = stub.test_emails.pop()
msg = message_from_string(raw_msg)
- body = msg.get_payload(0)
- body = body.get_payload(decode=True)
+ body = msg.get_payload(decode=True)
self.assertTrue(
"File bar_1.0.orig.tar.gz already exists in unicode PPA name: "
@@ -1027,8 +1017,7 @@
# The email generated should be sane.
from_addr, to_addrs, raw_msg = stub.test_emails.pop()
msg = message_from_string(raw_msg)
- body = msg.get_payload(0)
- body = body.get_payload(decode=True)
+ body = msg.get_payload(decode=True)
self.assertTrue(
"Rejected:\n"
@@ -1229,12 +1218,13 @@
# An email communicating the rejection and the reason why it was
# rejected is sent to the uploaders.
contents = [
- "Subject: [~name16/ubuntu/ppa] bar_1.0-1_source.changes rejected",
+ "Subject: [~name16/ubuntu/ppa] bar_1.0-1_source.changes "
+ "(Rejected)",
"Rejected:",
"PPA exceeded its size limit (2048.00 of 2048.00 MiB). "
"Ask a question in https://answers.launchpad.net/soyuz/ "
"if you need more space."]
- self.assertEmail(contents)
+ self.assertEmails([{"contents": contents}])
def testPPASizeNoQuota(self):
self.name16.archive.authorized_size = None
@@ -1242,7 +1232,7 @@
self.processUpload(self.uploadprocessor, upload_dir)
contents = [
"Subject: [~name16/ubuntu/ppa/breezy] bar 1.0-1 (Accepted)"]
- self.assertEmail(contents)
+ self.assertEmails([{"contents": contents}])
self.assertEqual(
self.uploadprocessor.last_processed_upload.queue_root.status,
PackageUploadStatus.DONE)
@@ -1266,7 +1256,7 @@
"PPA exceeded 95 % of its size limit (2000.00 of 2048.00 MiB). "
"Ask a question in https://answers.launchpad.net/soyuz/ "
"if you need more space."]
- self.assertEmail(contents)
+ self.assertEmails([{"contents": contents}])
# User was warned about quota limits but the source was accepted
# as informed in the upload notification.
=== modified file 'lib/lp/archiveuploader/tests/test_sync_notification.py'
--- lib/lp/archiveuploader/tests/test_sync_notification.py 2012-11-15 01:41:14 +0000
+++ lib/lp/archiveuploader/tests/test_sync_notification.py 2015-08-25 14:09:28 +0000
@@ -149,10 +149,10 @@
In a situation like that, we should not bother those people with the
failure. We notify the person who requested the sync instead.
- (The logic in lp.soyuz.adapters.notification may still notify the
- author of the last change, if that person is also an uploader for the
- archive that the failure happened in. For this particular situation
- we consider that not so much an intended behaviour, as an emergent one
+ (The logic in lp.soyuz.mail.packageupload may still notify the author
+ of the last change, if that person is also an uploader for the archive
+ that the failure happened in. For this particular situation we
+ consider that not so much an intended behaviour, as an emergent one
that does not seem inappropriate. It'd be hard to change if we wanted
to.)
=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2015-02-17 11:20:15 +0000
+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2015-08-25 14:09:28 +0000
@@ -159,10 +159,8 @@
self.options.nomails = False
self.options.context = 'insecure'
- # common recipients
- self.kinnison_recipient = (
- "Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>")
- self.name16_recipient = "Foo Bar <foo.bar@xxxxxxxxxxxxx>"
+ # common recipient
+ self.name16_recipient = "foo.bar@xxxxxxxxxxxxx"
self.log = BufferLogger()
@@ -344,50 +342,49 @@
self.switchToUploader()
return upload_processor
- def assertEmail(self, contents=None, recipients=None):
- """Check last email content and recipients.
+ def assertEmails(self, expected, allow_leftover=False):
+ """Check recent email content and recipients.
- :param contents: A list of lines; assert that each is in the email.
- :param recipients: A list of recipients that must be on the email.
- Supply an empty list if you don't want them
- checked. Default action is to check that the
- recipient is foo.bar@xxxxxxxxxxxxx, which is the
- signer on most of the test data uploads.
+ :param expected: A list of dicts, each of which represents an
+ expected email and may have "contents" and "recipient" keys.
+ All the items in expected must match in the correct order, with
+ none left over. "contents" is a list of lines; assert that each
+ is in Subject + Body. "recipient" is the To address the email
+ must have, defaulting to "foo.bar@xxxxxxxxxxxxx" which is the
+ signer on most of the test data uploads; supply None if you
+ don't want it checked.
+ :param allow_leftover: If True, allow additional emails to be left
+ over after checking the ones in expected.
"""
- if recipients is None:
- recipients = [self.name16_recipient]
- if contents is None:
- contents = []
-
- self.assertEqual(
- len(stub.test_emails), 1,
- 'Unexpected number of emails sent: %s' % len(stub.test_emails))
-
- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
- msg = message_from_string(raw_msg)
- # This is now a MIMEMultipart message.
- body = msg.get_payload(0)
- body = body.get_payload(decode=True)
-
- # Only check recipients if callsite didn't provide an empty list.
- if recipients != []:
- clean_recipients = [r.strip() for r in to_addrs]
- for recipient in list(recipients):
+
+ for item in expected:
+ recipient = item.get("recipient", self.name16_recipient)
+ contents = item.get("contents", [])
+
+ from_addr, to_addrs, raw_msg = stub.test_emails.pop()
+ msg = message_from_string(raw_msg)
+ # This is now a MIMEMultipart message.
+ body = msg.get_payload(0)
+ body = body.get_payload(decode=True)
+
+ # Only check the recipient if the caller didn't explicitly pass
+ # "recipient": None.
+ if recipient is not None:
+ clean_recipients = [r.strip() for r in to_addrs]
+ self.assertContentEqual([recipient], clean_recipients)
+
+ subject = "Subject: %s\n" % msg['Subject']
+ body = subject + body
+
+ for content in list(contents):
self.assertTrue(
- recipient in clean_recipients,
- "%s not found in %s" % (recipients, clean_recipients))
+ content in body,
+ "Expect: '%s'\nGot:\n%s" % (content, body))
+
+ if not allow_leftover:
self.assertEqual(
- len(recipients), len(clean_recipients),
- "Email recipients do not match exactly. Expected %s, got %s" %
- (recipients, clean_recipients))
-
- subject = "Subject: %s\n" % msg['Subject']
- body = subject + body
-
- for content in list(contents):
- self.assertTrue(
- content in body,
- "Expect: '%s'\nGot:\n%s" % (content, body))
+ [], stub.test_emails,
+ "%d emails left over" % len(stub.test_emails))
def PGPSignatureNotPreserved(self, archive=None):
"""PGP signatures should be removed from .changes files.
@@ -427,7 +424,7 @@
def _checkPartnerUploadEmailSuccess(self):
"""Ensure partner uploads generate the right email."""
from_addr, to_addrs, raw_msg = stub.test_emails.pop()
- foo_bar = "Foo Bar <foo.bar@xxxxxxxxxxxxx>"
+ foo_bar = "foo.bar@xxxxxxxxxxxxx"
self.assertEqual([e.strip() for e in to_addrs], [foo_bar])
self.assertTrue(
"rejected" not in raw_msg,
@@ -572,8 +569,7 @@
body = msg.get_payload(0)
body = body.get_payload(decode=True)
- daniel = "Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>"
- self.assertEqual(to_addrs, [daniel])
+ self.assertEqual(["daniel.silverstone@xxxxxxxxxxxxx"], to_addrs)
self.assertTrue("Unhandled exception processing upload: Exception "
"raised by BrokenUploadPolicy for testing."
in body)
@@ -603,14 +599,15 @@
self.processUpload(uploadprocessor, upload_dir)
# Check it went ok to the NEW queue and all is going well so far.
- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
- to_addrs = [e.strip() for e in to_addrs]
- foo_bar = "Foo Bar <foo.bar@xxxxxxxxxxxxx>"
- daniel = "Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>"
- self.assertContentEqual(to_addrs, [foo_bar, daniel])
- self.assertTrue(
- "NEW" in raw_msg, "Expected email containing 'NEW', got:\n%s"
- % raw_msg)
+ foo_bar = "foo.bar@xxxxxxxxxxxxx"
+ daniel = "daniel.silverstone@xxxxxxxxxxxxx"
+ for expected_to_addr in foo_bar, daniel:
+ from_addr, to_addrs, raw_msg = stub.test_emails.pop()
+ to_addrs = [e.strip() for e in to_addrs]
+ self.assertContentEqual(to_addrs, [expected_to_addr])
+ self.assertTrue(
+ "NEW" in raw_msg, "Expected email containing 'NEW', got:\n%s"
+ % raw_msg)
# Accept and publish the upload.
# This is required so that the next upload of a later version of
@@ -638,14 +635,13 @@
self.processUpload(uploadprocessor, upload_dir)
# Verify we get an email talking about awaiting approval.
- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
- to_addrs = [e.strip() for e in to_addrs]
- daniel = "Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>"
- foo_bar = "Foo Bar <foo.bar@xxxxxxxxxxxxx>"
- self.assertContentEqual(to_addrs, [foo_bar, daniel])
- self.assertTrue("Waiting for approval" in raw_msg,
- "Expected an 'upload awaits approval' email.\n"
- "Got:\n%s" % raw_msg)
+ for expected_to_addr in foo_bar, daniel:
+ from_addr, to_addrs, raw_msg = stub.test_emails.pop()
+ to_addrs = [e.strip() for e in to_addrs]
+ self.assertContentEqual(to_addrs, [expected_to_addr])
+ self.assertTrue("Waiting for approval" in raw_msg,
+ "Expected an 'upload awaits approval' email.\n"
+ "Got:\n%s" % raw_msg)
# And verify that the queue item is in the unapproved state.
queue_items = self.breezy.getPackageUploads(
@@ -942,7 +938,7 @@
# Check that it was rejected.
from_addr, to_addrs, raw_msg = stub.test_emails.pop()
- foo_bar = "Foo Bar <foo.bar@xxxxxxxxxxxxx>"
+ foo_bar = "foo.bar@xxxxxxxxxxxxx"
self.assertEqual([e.strip() for e in to_addrs], [foo_bar])
self.assertTrue(
"Cannot mix partner files with non-partner." in raw_msg,
@@ -1049,10 +1045,10 @@
build_uploadprocessor, upload_dir, build=foocomm_build)
contents = [
- "Subject: [ubuntu/partner] foocomm_1.0-1_i386.changes rejected",
+ "Subject: [ubuntu/partner] foocomm_1.0-1_i386.changes (Rejected)",
"Attempt to upload binaries specifying build 31, "
"where they don't fit."]
- self.assertEmail(contents)
+ self.assertEmails([{"contents": contents}])
# Reset upload queue directory for a new upload.
shutil.rmtree(upload_dir)
@@ -1292,7 +1288,8 @@
# Check that the sourceful upload to the copy archive is rejected.
contents = [
"Invalid upload path (1/ubuntu) for this policy (insecure)"]
- self.assertEmail(contents=contents, recipients=[])
+ self.assertEmails(
+ [{"contents": contents, "recipient": None}], allow_leftover=True)
# Uploads that are new should have the component overridden
# such that:
@@ -1689,7 +1686,7 @@
from_addr, to_addrs, raw_msg = stub.test_emails.pop()
msg = message_from_string(raw_msg)
self.assertEqual(
- msg['Subject'], '[ubuntu] bar_1.0-2_source.changes rejected')
+ msg['Subject'], '[ubuntu] bar_1.0-2_source.changes (Rejected)')
# Grant the permissions in the proper series.
self.switchToAdmin()
@@ -1716,7 +1713,7 @@
def testUploadPathErrorIntendedForHumans(self):
# Distribution upload path errors are augmented with a hint
# to fix the current dput/dupload configuration.
- # This information gets included in the rejection email along
+ # This information gets included in the rejection emails along
# with pointer to the Soyuz questions in Launchpad and the
# reason why the message was sent to the current recipients.
self.setupBreezy()
@@ -1741,20 +1738,26 @@
],
rejection_message.splitlines())
- contents = [
- "Subject: [ubuntu] bar_1.0-1_source.changes rejected",
+ base_contents = [
+ "Subject: [ubuntu] bar_1.0-1_source.changes (Rejected)",
"Could not find distribution 'boing'",
"If you don't understand why your files were rejected",
"http://answers.launchpad.net/soyuz",
- "You are receiving this email because you are the "
- "uploader, maintainer or",
- "signer of the above package.",
- ]
- recipients = [
- 'Foo Bar <foo.bar@xxxxxxxxxxxxx>',
- 'Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>',
- ]
- self.assertEmail(contents, recipients=recipients)
+ ]
+ expected = []
+ expected.append({
+ "contents": base_contents + [
+ "You are receiving this email because you made this upload."],
+ "recipient": "foo.bar@xxxxxxxxxxxxx",
+ })
+ expected.append({
+ "contents": base_contents + [
+ "You are receiving this email because you are the most "
+ "recent person",
+ "listed in this package's changelog."],
+ "recipient": "daniel.silverstone@xxxxxxxxxxxxx",
+ })
+ self.assertEmails(expected)
def test30QuiltUploadToUnsupportingSeriesIsRejected(self):
"""Ensure that uploads to series without format support are rejected.
@@ -1909,21 +1912,27 @@
"the 'CURRENT' state.",
rejection_message)
- contents = [
- "Subject: [ubuntu] bar_1.0-1_source.changes rejected",
+ base_contents = [
+ "Subject: [ubuntu] bar_1.0-1_source.changes (Rejected)",
"Not permitted to upload to the RELEASE pocket in a series "
"in the 'CURRENT' state.",
"If you don't understand why your files were rejected",
"http://answers.launchpad.net/soyuz",
- "You are receiving this email because you are the "
- "uploader, maintainer or",
- "signer of the above package.",
- ]
- recipients = [
- 'Foo Bar <foo.bar@xxxxxxxxxxxxx>',
- 'Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>',
- ]
- self.assertEmail(contents, recipients=recipients)
+ ]
+ expected = []
+ expected.append({
+ "contents": base_contents + [
+ "You are receiving this email because you made this upload."],
+ "recipient": "foo.bar@xxxxxxxxxxxxx",
+ })
+ expected.append({
+ "contents": base_contents + [
+ "You are receiving this email because you are the most "
+ "recent person",
+ "listed in this package's changelog."],
+ "recipient": "daniel.silverstone@xxxxxxxxxxxxx",
+ })
+ self.assertEmails(expected)
def testPGPSignatureNotPreserved(self):
"""PGP signatures should be removed from .changes files.
@@ -1998,9 +2007,11 @@
self.switchToUploader()
upload_dir = self.queueUpload("bar_1.0-1")
self.processUpload(uploadprocessor, upload_dir)
- self.assertEmail(
- contents=["Redirecting ubuntu breezy to ubuntu breezy-proposed."],
- recipients=[])
+ self.assertEmails([{
+ "contents":
+ ["Redirecting ubuntu breezy to ubuntu breezy-proposed."],
+ "recipient": None,
+ }], allow_leftover=True)
[queue_item] = self.breezy.getPackageUploads(
status=PackageUploadStatus.NEW, name=u"bar",
version=u"1.0-1", exact_match=True)
@@ -2010,9 +2021,11 @@
pop_notifications()
upload_dir = self.queueUpload("bar_1.0-2")
self.processUpload(uploadprocessor, upload_dir)
- self.assertEmail(
- contents=["Redirecting ubuntu breezy to ubuntu breezy-proposed."],
- recipients=[])
+ self.assertEmails([{
+ "contents":
+ ["Redirecting ubuntu breezy to ubuntu breezy-proposed."],
+ "recipient": None,
+ }], allow_leftover=True)
[queue_item] = self.breezy.getPackageUploads(
status=PackageUploadStatus.DONE, name=u"bar",
version=u"1.0-2", exact_match=True)
@@ -2271,7 +2284,7 @@
status=PackageUploadStatus.ACCEPTED,
version=u"1.0-1", name=u"bar")
queue_item.setDone()
- stub.test_emails.pop()
+ stub.test_emails = []
build.buildqueue_record.markAsBuilding(self.factory.makeBuilder())
build.updateStatus(status)
=== modified file 'lib/lp/services/mail/basemailer.py'
--- lib/lp/services/mail/basemailer.py 2015-08-23 22:53:55 +0000
+++ lib/lp/services/mail/basemailer.py 2015-08-25 14:09:28 +0000
@@ -135,6 +135,10 @@
"""
pass
+ def _getTemplateName(self, email, recipient):
+ """Return the name of the template to use for this email body."""
+ return self._template_name
+
def _getTemplateParams(self, email, recipient):
"""Return a dict of values to use in the body and subject."""
reason, rationale = self._recipients.getReason(email)
@@ -150,7 +154,8 @@
def _getBody(self, email, recipient):
"""Return the complete body to use for this email."""
- template = get_email_template(self._template_name, app=self.app)
+ template = get_email_template(
+ self._getTemplateName(email, recipient), app=self.app)
params = self._getTemplateParams(email, recipient)
body = template % params
footer = self._getFooter(email, recipient, params)
=== modified file 'lib/lp/services/mail/notificationrecipientset.py'
--- lib/lp/services/mail/notificationrecipientset.py 2015-07-08 16:05:11 +0000
+++ lib/lp/services/mail/notificationrecipientset.py 2015-08-25 14:09:28 +0000
@@ -6,6 +6,7 @@
__metaclass__ = type
__all__ = [
'NotificationRecipientSet',
+ 'StubPerson',
]
@@ -21,6 +22,22 @@
)
+class StubPerson:
+ """A stub recipient person.
+
+ This can be used when sending to special email addresses that do not
+ correspond to a real Person.
+ """
+
+ displayname = None
+ is_team = False
+ expanded_notification_footers = False
+
+ def __init__(self, email):
+ self.preferredemail = type(
+ "StubEmailAddress", (object,), {"email": email})
+
+
@implementer(INotificationRecipientSet)
class NotificationRecipientSet:
"""Set of recipients along the rationale for being in the set."""
@@ -87,17 +104,24 @@
"""See `INotificationRecipientSet`."""
from zope.security.proxy import removeSecurityProxy
from lp.registry.model.person import get_recipients
- if IPerson.providedBy(persons):
+ if (IPerson.providedBy(persons) or
+ zope_isinstance(persons, StubPerson)):
persons = [persons]
for person in persons:
- assert IPerson.providedBy(person), (
- 'You can only add() an IPerson: %r' % person)
+ assert (
+ IPerson.providedBy(person) or
+ zope_isinstance(person, StubPerson)), (
+ 'You can only add() an IPerson or a StubPerson: %r' % person)
# If the person already has a rationale, keep the first one.
if person in self._personToRationale:
continue
self._personToRationale[person] = reason, header
- for receiving_person in get_recipients(person):
+ if IPerson.providedBy(person):
+ recipients = get_recipients(person)
+ else:
+ recipients = [person]
+ for receiving_person in recipients:
# Bypass zope's security because IEmailAddress.email is not
# public.
preferred_email = removeSecurityProxy(
=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-notify.txt'
--- lib/lp/soyuz/doc/distroseriesqueue-notify.txt 2013-07-25 11:58:55 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue-notify.txt 2015-08-25 14:09:28 +0000
@@ -59,7 +59,8 @@
...
DEBUG above if files already exist in other distroseries.
...
- DEBUG signer of the above package.
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
Helper functions to examine emails that were sent:
@@ -99,8 +100,8 @@
<BLANKLINE>
-- =
<BLANKLINE>
- You are receiving this email because you are the uploader, maintainer or
- signer of the above package.
+ You are receiving this email because you are the most recent person
+ listed in this package's changelog.
<BLANKLINE>
Now we will process a signed package. Signed packages will potentially
@@ -133,15 +134,17 @@
DEBUG Sent a mail:
...
-There are two emails, the upload acknowledgement and the announcement,
-because this upload is already accepted.
+There are three emails, the upload acknowledgement to the changer, the
+upload acknowledgement to the signer, and the announcement, because this
+upload is already accepted.
>>> msgs = pop_notifications()
>>> len(msgs)
- 2
+ 3
-The mail 'To:' addresses contain the signer and the changer's email.
-The announcement email contains the serieses changeslist.
+The two upload acknowledgements contain the changer's email and the signer's
+email in their respective 'To:' headers.
+The announcement email contains the series's changeslist.
>>> def to_lower(address):
... """Return lower-case version of email address."""
@@ -153,12 +156,10 @@
... [addr.strip() for addr in header_field.split(',')],
... key=to_lower)
- >>> for addr in extract_addresses(msgs[0]['To']):
- ... print addr
+ >>> for msg in msgs:
+ ... print msg['To']
Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
Foo Bar <foo.bar@xxxxxxxxxxxxx>
-
- >>> print msgs[1]['To']
autotest_changes@xxxxxxxxxx
The mail 'Bcc:' address is the uploader. The announcement has the
@@ -167,35 +168,36 @@
>>> for msg in msgs:
... print extract_addresses(msg['Bcc'])
['Root <root@localhost>']
+ ['Root <root@localhost>']
['netapplet_derivatives@xxxxxxxxxxxxxxxxxxxxxx', 'Root <root@localhost>']
-The mail 'From:' addresses are the uploader and the changer.
+The mail 'From:' addresses are the uploader (for acknowledgements sent to
+the uploader and the changer) and the changer.
>>> for msg in msgs:
... print msg['From']
Root <root@localhost>
+ Root <root@localhost>
Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
- >>> print notification['Subject']
- [ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)
+ >>> print msgs[0]['Subject']
+ [ubuntu/breezy-autotest] netapplet 0.99.6-1 (Accepted)
The mail body contains the same list of files again:
- >>> print notification.get_payload(0) # doctest: -NORMALIZE_WHITESPACE
+ >>> print msgs[0].get_payload(0) # doctest: -NORMALIZE_WHITESPACE
From nobody ...
...
- NEW: netapplet_1.0-1.dsc
- NEW: netapplet_1.0.orig.tar.gz
- NEW: netapplet_1.0-1.diff.gz
+ OK: netapplet_1.0-1.dsc
+ -> Component: main Section: web
+ OK: netapplet_1.0.orig.tar.gz
+ OK: netapplet_1.0-1.diff.gz
<BLANKLINE>
...
- You may have gotten the distroseries wrong. If so, you may get warnings
- above if files already exist in other distroseries.
- <BLANKLINE>
-- =
<BLANKLINE>
- You are receiving this email because you are the uploader, maintainer or
- signer of the above package.
+ You are receiving this email because you are the most recent person
+ listed in this package's changelog.
<BLANKLINE>
notify() will also work without passing the changes_file_object
@@ -216,46 +218,75 @@
...
DEBUG Sent a mail:
...
- DEBUG Recipients: ... Silverstone ...
- ...
- DEBUG above if files already exist in other distroseries.
- ...
- DEBUG signer of the above package.
-
-Only one email is generated:
-
- >>> [notification] = pop_notifications()
+ DEBUG Recipients: ... Silverstone ...
+ ...
+ DEBUG above if files already exist in other distroseries.
+ ...
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
+ DEBUG Sent a mail:
+ ...
+ DEBUG Recipients: ... Bar ...
+ ...
+ DEBUG above if files already exist in other distroseries.
+ ...
+ DEBUG You are receiving this email because you made this upload.
+
+Two emails are generated, one to the changer and one to the signer:
+
+ >>> [changer_notification, signer_notification] = pop_notifications()
The mail headers are the same as before:
- >>> for addr in extract_addresses(notification['To']):
- ... print addr
+ >>> print changer_notification['To']
Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+ >>> print signer_notification['To']
Foo Bar <foo.bar@xxxxxxxxxxxxx>
- >>> print notification['Bcc']
+ >>> print changer_notification['Bcc']
+ Root <root@localhost>
+ >>> print signer_notification['Bcc']
Root <root@localhost>
- >>> print notification['Subject']
+ >>> print changer_notification['Subject']
+ [ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)
+ >>> print signer_notification['Subject']
[ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)
The mail body contains the same list of files again:
- >>> print notification.get_payload(0) # doctest: -NORMALIZE_WHITESPACE
- From nobody ...
- ...
- NEW: netapplet_1.0-1.dsc
- NEW: netapplet_1.0.orig.tar.gz
- NEW: netapplet_1.0-1.diff.gz
- <BLANKLINE>
- ...
- You may have gotten the distroseries wrong. If so, you may get warnings
- above if files already exist in other distroseries.
- <BLANKLINE>
- -- =
- <BLANKLINE>
- You are receiving this email because you are the uploader, maintainer or
- signer of the above package.
+ >>> print changer_notification.get_payload(0)
+ ... # doctest: -NORMALIZE_WHITESPACE
+ From nobody ...
+ ...
+ NEW: netapplet_1.0-1.dsc
+ NEW: netapplet_1.0.orig.tar.gz
+ NEW: netapplet_1.0-1.diff.gz
+ <BLANKLINE>
+ ...
+ You may have gotten the distroseries wrong. If so, you may get warnings
+ above if files already exist in other distroseries.
+ <BLANKLINE>
+ -- =
+ <BLANKLINE>
+ You are receiving this email because you are the most recent person
+ listed in this package's changelog.
+ <BLANKLINE>
+ >>> print signer_notification.get_payload(0)
+ ... # doctest: -NORMALIZE_WHITESPACE
+ From nobody ...
+ ...
+ NEW: netapplet_1.0-1.dsc
+ NEW: netapplet_1.0.orig.tar.gz
+ NEW: netapplet_1.0-1.diff.gz
+ <BLANKLINE>
+ ...
+ You may have gotten the distroseries wrong. If so, you may get warnings
+ above if files already exist in other distroseries.
+ <BLANKLINE>
+ -- =
+ <BLANKLINE>
+ You are receiving this email because you made this upload.
<BLANKLINE>
notify() will also generate rejection notices if the upload failed. The
@@ -268,7 +299,19 @@
... summary_text="Testing rejection message", logger=FakeLogger())
DEBUG Building recipients list.
...
- DEBUG Sending rejection email.
+ DEBUG Sent a mail:
+ DEBUG Subject: [ubuntu/breezy-autotest] netapplet 0.99.6-1 (Rejected)
+ DEBUG Sender: Root <root@localhost>
+ DEBUG Recipients: ... Silverstone ...
+ DEBUG Bcc: Root <root@localhost>
+ DEBUG Body:
+ DEBUG Rejected:
+ DEBUG Testing rejection message
+ ...
+ DEBUG If you don't understand why your files were rejected, or if the
+ ...
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
...
DEBUG Subject: [ubuntu/breezy-autotest] netapplet 0.99.6-1 (Rejected)
DEBUG Sender: Root <root@localhost>
@@ -280,13 +323,13 @@
...
DEBUG If you don't understand why your files were rejected, or if the
...
- DEBUG signer of the above package.
+ DEBUG You are receiving this email because you made this upload.
-Only one email is generated:
+Two emails are generated:
>>> transaction.commit()
>>> len(stub.test_emails)
- 1
+ 2
Clean up, otherwise stuff is left lying around in /var/tmp.
=== modified file 'lib/lp/soyuz/doc/soyuz-set-of-uploads.txt'
--- lib/lp/soyuz/doc/soyuz-set-of-uploads.txt 2015-07-29 05:56:50 +0000
+++ lib/lp/soyuz/doc/soyuz-set-of-uploads.txt 2015-08-25 14:09:28 +0000
@@ -304,10 +304,11 @@
Rejected uploads: ['bar_1.0-3']
>>> read_email()
- To:
- Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>,
- Foo Bar <foo.bar@xxxxxxxxxxxxx>
- Subject: [ubuntutest] bar_1.0-3_source.changes rejected
+ To: Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+ Subject: [ubuntutest] bar_1.0-3_source.changes (Rejected)
+ ...
+ To: Foo Bar <foo.bar@xxxxxxxxxxxxx>
+ Subject: [ubuntutest] bar_1.0-3_source.changes (Rejected)
...
Force weird behaviour with rfc2047 sentences containing '.' on
@@ -321,10 +322,8 @@
'.', must be rfc2047 compliant:
>>> simulate_upload('bar_1.0-4')
- >>> uninteresting_email = stub.test_emails.pop()
>>> read_email()
- To: "Foo B. Bar" <foo.bar@xxxxxxxxxxxxx>,
- Celso Providelo <celso.providelo@xxxxxxxxxxxxx>
+ To: "Foo B. Bar" <foo.bar@xxxxxxxxxxxxx>
Subject: [ubuntutest/breezy] bar 1.0-4 (Accepted)
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
@@ -353,10 +352,13 @@
<BLANKLINE>
-- =
<BLANKLINE>
- You are receiving this email because you are the uploader, maintainer or
- signer of the above package.
- <BLANKLINE>
- <BLANKLINE>
+ You are receiving this email because you made this upload.
+ <BLANKLINE>
+ <BLANKLINE>
+ To: Celso Providelo <celso.providelo@xxxxxxxxxxxxx>
+ ...
+ To: breezy-changes@xxxxxxxxxx
+ ...
Revert changes:
@@ -468,9 +470,8 @@
DEBUG above if files already exist in other distroseries.
DEBUG
DEBUG --
- DEBUG You are receiving this email because you are the uploader,
- maintainer or
- DEBUG signer of the above package.
+ DEBUG You are receiving this email because you are the most recent person
+ DEBUG listed in this package's changelog.
INFO Committing the transaction and any mails associated with this
upload.
...
=== modified file 'lib/lp/soyuz/emailtemplates/ppa-upload-accepted.txt'
--- lib/lp/soyuz/emailtemplates/ppa-upload-accepted.txt 2012-02-10 09:31:39 +0000
+++ lib/lp/soyuz/emailtemplates/ppa-upload-accepted.txt 2015-08-25 14:09:28 +0000
@@ -2,7 +2,3 @@
%(SUMMARY)s
%(CHANGESFILE)s
-
---%(ARCHIVE_URL)s
-You are receiving this email because you are the uploader of the above
-PPA package.
=== modified file 'lib/lp/soyuz/emailtemplates/ppa-upload-rejection.txt'
--- lib/lp/soyuz/emailtemplates/ppa-upload-rejection.txt 2012-02-10 09:31:39 +0000
+++ lib/lp/soyuz/emailtemplates/ppa-upload-rejection.txt 2015-08-25 14:09:28 +0000
@@ -7,7 +7,3 @@
If you don't understand why your files were rejected please send an email
to %(USERS_ADDRESS)s for help (requires membership).
-
---%(ARCHIVE_URL)s
-You are receiving this email because you are the uploader of the above
-PPA package.
=== modified file 'lib/lp/soyuz/emailtemplates/upload-accepted.txt'
--- lib/lp/soyuz/emailtemplates/upload-accepted.txt 2012-10-24 09:43:58 +0000
+++ lib/lp/soyuz/emailtemplates/upload-accepted.txt 2015-08-25 14:09:28 +0000
@@ -10,7 +10,3 @@
%(ANNOUNCE)s
Thank you for your contribution to %(DISTRO)s.
-
---
-You are receiving this email because you are the uploader, maintainer or
-signer of the above package.
=== modified file 'lib/lp/soyuz/emailtemplates/upload-new.txt'
--- lib/lp/soyuz/emailtemplates/upload-new.txt 2011-12-18 23:30:56 +0000
+++ lib/lp/soyuz/emailtemplates/upload-new.txt 2015-08-25 14:09:28 +0000
@@ -9,7 +9,3 @@
You may have gotten the distroseries wrong. If so, you may get warnings
above if files already exist in other distroseries.
-
---
-You are receiving this email because you are the uploader, maintainer or
-signer of the above package.
=== modified file 'lib/lp/soyuz/emailtemplates/upload-rejection.txt'
--- lib/lp/soyuz/emailtemplates/upload-rejection.txt 2011-12-18 23:30:56 +0000
+++ lib/lp/soyuz/emailtemplates/upload-rejection.txt 2015-08-25 14:09:28 +0000
@@ -10,7 +10,3 @@
If you don't understand why your files were rejected, or if the
override file requires editing, please go to:
http://answers.launchpad.net/soyuz
-
---
-You are receiving this email because you are the uploader, maintainer or
-signer of the above package.
=== renamed file 'lib/lp/soyuz/adapters/notification.py' => 'lib/lp/soyuz/mail/packageupload.py'
--- lib/lp/soyuz/adapters/notification.py 2015-07-29 06:58:37 +0000
+++ lib/lp/soyuz/mail/packageupload.py 2015-08-25 14:09:28 +0000
@@ -1,20 +1,16 @@
# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Notification for uploads and copies."""
-
__metaclass__ = type
-
__all__ = [
- 'notify',
+ 'PackageUploadMailer',
]
-
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-import os
+from collections import OrderedDict
+import os.path
from zope.component import getUtility
+from zope.security.proxy import isinstance as zope_isinstance
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.archivepublisher.utils import get_ppa_reference
@@ -28,67 +24,175 @@
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.services.config import config
from lp.services.encoding import guess as guess_encoding
-from lp.services.mail.helpers import get_email_template
+from lp.services.mail.basemailer import (
+ BaseMailer,
+ RecipientReason,
+ )
+from lp.services.mail.mailwrapper import MailWrapper
+from lp.services.mail.notificationrecipientset import StubPerson
from lp.services.mail.sendmail import (
format_address,
format_address_for_person,
- sendmail,
)
from lp.services.webapp import canonical_url
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
-def reject_changes_file(blamer, changes_file_path, changes, archive,
- distroseries, reason, logger=None):
- """Notify about a rejection where all of the details are not known.
-
- :param blamer: The `IPerson` that is to blame for this notification.
- :param changes_file_path: The path to the changes file.
- :param changes: A dictionary of the parsed changes file.
- :param archive: The `IArchive` the notification is regarding.
- :param distroseries: The `IDistroSeries` the notification is regarding.
- :param reason: The reason for the rejection.
+class PackageUploadRecipientReason(RecipientReason):
+
+ @classmethod
+ def forRequester(cls, requester, recipient):
+ header = cls.makeRationale("Requester", requester)
+ # This is a little vague - copies may end up here too - but it's
+ # close enough.
+ reason = "You are receiving this email because you made this upload."
+ return cls(requester, recipient, header, reason)
+
+ @classmethod
+ def forMaintainer(cls, maintainer, recipient):
+ header = cls.makeRationale("Maintainer", maintainer)
+ reason = (
+ "You are receiving this email because you are listed as this "
+ "package's maintainer.")
+ return cls(maintainer, recipient, header, reason)
+
+ @classmethod
+ def forChangedBy(cls, changed_by, recipient):
+ header = cls.makeRationale("Changed-By", changed_by)
+ reason = (
+ "You are receiving this email because you are the most recent "
+ "person listed in this package's changelog.")
+ return cls(changed_by, recipient, header, reason)
+
+ @classmethod
+ def forPPAUploader(cls, uploader, recipient):
+ header = cls.makeRationale("PPA-Uploader", uploader)
+ reason = (
+ "You are receiving this email because you have upload permissions "
+ "to this PPA.")
+ return cls(uploader, recipient, header, reason)
+
+ @classmethod
+ def forChangesList(cls, recipient):
+ return cls(recipient, recipient, "Changes-List", "")
+
+ def getReason(self):
+ """See `RecipientReason`."""
+ return MailWrapper(width=72).format(
+ super(PackageUploadRecipientReason, self).getReason())
+
+
+def debug(logger, msg, *args, **kwargs):
+ """Shorthand debug notation."""
+ if logger is not None:
+ logger.debug(msg, *args, **kwargs)
+
+
+def sanitize_string(s):
+ """Make sure string does not trigger 'ascii' codec errors.
+
+ Convert string to unicode if needed so that characters outside
+ the (7-bit) ASCII range do not cause errors like these:
+
+ 'ascii' codec can't decode byte 0xc4 in position 21: ordinal
+ not in range(128)
"""
- ignored, filename = os.path.split(changes_file_path)
- information = {
- 'SUMMARY': reason,
- 'CHANGESFILE': '',
- 'DATE': '',
- 'CHANGEDBY': '',
- 'MAINTAINER': '',
- 'SIGNER': '',
- 'ORIGIN': '',
- 'ARCHIVE_URL': '',
- 'USERS_ADDRESS': config.launchpad.users_address,
- }
- subject = '%s rejected' % filename
- if archive:
- subject = '[%s] %s' % (archive.reference, subject)
- information['ARCHIVE_URL'] = '\n%s' % canonical_url(archive)
- template = get_template(archive, 'rejected')
- body = template % information
- to_addrs = get_upload_notification_recipients(
- blamer, archive, distroseries, logger, changes=changes)
- debug(logger, "Sending rejection email.")
- if not to_addrs:
- debug(logger, "No recipients have a preferred email.")
+ if isinstance(s, unicode):
+ return s
+ else:
+ return guess_encoding(s)
+
+
+def add_recipient(recipients, person, reason_factory, logger=None):
+ # Circular import.
+ from lp.registry.model.person import get_recipients
+
+ if person is None:
return
- send_mail(None, archive, to_addrs, subject, body, False, logger=logger)
-
-
-def get_template(archive, action):
- """Return the appropriate email template."""
- template_name = 'upload-'
- if action in ('new', 'accepted', 'announcement'):
- template_name += action
- elif action == 'unapproved':
- template_name += 'accepted'
- elif action == 'rejected':
- template_name += 'rejection'
- if archive.is_ppa:
- template_name = 'ppa-%s' % template_name
- template_name += '.txt'
- return get_email_template(template_name, app='soyuz')
+ for recipient in get_recipients(person):
+ if recipient not in recipients:
+ debug(
+ logger, "Adding recipient: '%s'" % format_address_for_person(
+ recipient))
+ reason = reason_factory(person, recipient)
+ recipients[recipient] = reason
+
+
+def fetch_information(spr, bprs, changes, previous_version=None):
+ changelog = date = changedby = maintainer = None
+
+ if changes:
+ changelog = ChangesFile.formatChangesComment(
+ sanitize_string(changes.get('Changes')))
+ date = changes.get('Date')
+ 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
+ changelog = spr.aggregate_changelog(previous_version)
+ date = spr.dateuploaded
+ 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': changelog,
+ 'date': date,
+ 'changedby': changedby,
+ 'maintainer': maintainer,
+ }
+
+
+def addr_to_person(addr):
+ """Return an `IPerson` given a name and 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 `addr` is None.
+ """
+ if addr is None:
+ return None
+ return getUtility(IPersonSet).getByEmail(addr[1])
+
+
+def is_valid_uploader(person, distribution):
+ """Is `person` an uploader for `distribution`?
+
+ A `None` person is not an uploader.
+ """
+ if person is None:
+ return None
+ else:
+ return not getUtility(IArchivePermissionSet).componentsForUploader(
+ distribution.main_archive, person).is_empty()
+
+
+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 = addr_to_person(changed_by)
+ return (
+ spr and
+ not bprs and
+ changed_by == getUtility(ILaunchpadCelebrities).katie and
+ pocket != PackagePublishingPocket.SECURITY)
ACTION_DESCRIPTIONS = {
@@ -101,9 +205,8 @@
def calculate_subject(spr, bprs, customfiles, archive, distroseries,
- pocket, action):
+ pocket, action, changesfile_object=None):
"""Return the email subject for the notification."""
- suite = distroseries.getSuite(pocket)
names = set()
version = '-'
if spr:
@@ -114,431 +217,46 @@
version = bprs[0].build.source_package_release.version
for custom in customfiles:
names.add(custom.libraryfilealias.filename)
- name_str = ', '.join(names)
- subject = '[%s/%s] %s %s (%s)' % (
- archive.reference, suite, name_str, version,
- ACTION_DESCRIPTIONS[action])
+ if names:
+ archive_and_suite = '%s/%s' % (
+ archive.reference, distroseries.getSuite(pocket))
+ name_and_version = '%s %s' % (', '.join(names), version)
+ else:
+ if changesfile_object is None:
+ return None
+ # The suite may not be meaningful if we have no
+ # spr/bprs/customfiles, since this must be a very early rejection.
+ # Don't introduce confusion by including it.
+ archive_and_suite = archive.reference
+ name_and_version = os.path.basename(changesfile_object.name)
+ subject = '[%s] %s (%s)' % (
+ archive_and_suite, name_and_version, ACTION_DESCRIPTIONS[action])
return subject
-def notify(blamer, spr, bprs, customfiles, archive, distroseries, pocket,
- summary_text=None, changes=None, changesfile_content=None,
- changesfile_object=None, action=None, dry_run=False,
- logger=None, announce_from_person=None, previous_version=None):
- """Notify about an upload or package copy.
-
- :param blamer: The `IPerson` who is to blame for this notification.
- :param spr: The `ISourcePackageRelease` that was created.
- :param bprs: A list of `IBinaryPackageRelease` that were created.
- :param customfiles: An `ILibraryFileAlias` that was created.
- :param archive: The target `IArchive`.
- :param distroseries: The target `IDistroSeries`.
- :param pocket: The target `PackagePublishingPocket`.
- :param summary_text: The summary of the notification.
- :param changes: A dictionary of the parsed changes file.
- :param changesfile_content: The raw content of the changes file, so it
- can be attached to the mail if desired.
- :param changesfile_object: The raw object of the changes file. Only used
- to work out the filename for `reject_changes_file`.
- :param action: A string of what action to notify for, such as 'new',
- 'accepted'.
- :param dry_run: If True, only log the mail.
- :param announce_from_person: If passed, use this `IPerson` as the From: in
- announcement emails. If the person has no preferred email address,
- the person is ignored and the default From: is used instead.
- :param previous_version: If specified, the change log on the email will
- include all of the source package's change logs after that version
- up to and including the passed spr's version.
- """
- # If this is a binary or mixed upload, we don't send *any* emails
- # provided it's not a rejection or a security upload:
- if (
- bprs and action != 'rejected' and
- pocket != PackagePublishingPocket.SECURITY):
- debug(logger, "Not sending email; upload is from a build.")
- return
-
- if spr and spr.source_package_recipe_build and action == 'accepted':
- debug(logger, "Not sending email; upload is from a recipe.")
- return
-
- if spr is None and not bprs and not customfiles:
- # We do not have enough context to do a normal notification, so
- # reject what we do have.
- if changesfile_object is None:
- return
- reject_changes_file(
- blamer, changesfile_object.name, changes, archive, distroseries,
- summary_text, logger=logger)
- return
-
- # "files" will contain a list of tuples of filename,component,section.
- # If files is empty, we don't need to send an email if this is not
- # a rejection.
- try:
- files = build_uploaded_files_list(spr, bprs, customfiles, logger)
- except LanguagePackEncountered:
- # Don't send emails for language packs.
- return
-
- if not files and action != 'rejected':
- return
-
- recipients = get_upload_notification_recipients(
- blamer, archive, distroseries, logger, changes=changes, spr=spr,
- bprs=bprs)
-
- # There can be no recipients if none of the emails are registered
- # in LP.
- if not recipients:
- debug(logger, "No recipients on email, not sending.")
- return
-
- if action == 'rejected':
- default_recipient = "%s <%s>" % (
- config.uploader.default_recipient_name,
- config.uploader.default_recipient_address)
- if not recipients:
- recipients = [default_recipient]
- debug(logger, "Sending rejection email.")
- summarystring = summary_text
- else:
- summary = build_summary(spr, files, action)
- if summary_text:
- summary.append(summary_text)
- summarystring = "\n".join(summary)
-
- attach_changes = not archive.is_ppa
-
- def build_and_send_mail(action, recipients, from_addr=None, bcc=None,
- previous_version=None):
- subject = calculate_subject(
- spr, bprs, customfiles, archive, distroseries, pocket, action)
- body = assemble_body(
- blamer, spr, bprs, archive, distroseries, summarystring, changes,
- action, previous_version=previous_version)
- body = body.encode("utf8")
- send_mail(
- spr, archive, recipients, subject, body, dry_run,
- changesfile_content=changesfile_content,
- attach_changes=attach_changes, from_addr=from_addr, bcc=bcc,
- logger=logger)
-
- build_and_send_mail(
- action, recipients, previous_version=previous_version)
-
- info = fetch_information(spr, bprs, changes)
- from_addr = info['changedby']
- 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
- # security uploads, or autosync uploads.
- if (action == 'accepted' and distroseries.changeslist
- 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_addr)):
- name = None
- bcc_addr = None
- if spr:
- name = spr.name
- elif bprs:
- name = bprs[0].build.source_package_release.name
- if name:
- email_base = distroseries.distribution.package_derivatives_email
- if email_base:
- bcc_addr = email_base.format(package_name=name)
-
- build_and_send_mail(
- '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,
- action, previous_version=None):
- """Assemble the email notification body."""
- if changes is None:
- changes = {}
- info = fetch_information(
- spr, bprs, changes, previous_version=previous_version)
- information = {
- 'STATUS': ACTION_DESCRIPTIONS[action],
- 'SUMMARY': summary,
- 'DATE': 'Date: %s' % info['date'],
- 'CHANGESFILE': info['changelog'],
- 'DISTRO': distroseries.distribution.title,
- 'ANNOUNCE': 'No announcement sent',
- 'CHANGEDBY': '',
- 'MAINTAINER': '',
- 'ORIGIN': '',
- 'SIGNER': '',
- 'SPR_URL': '',
- 'ARCHIVE_URL': '\n%s' % canonical_url(archive),
- 'USERS_ADDRESS': config.launchpad.users_address,
- }
- if spr:
- information['SPR_URL'] = canonical_url(
- distroseries.distribution.getSourcePackageRelease(spr))
-
- # 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
- if action == 'unapproved':
- information['SUMMARY'] += (
- "\nThis upload awaits approval by a distro manager\n")
- if distroseries.changeslist:
- information['ANNOUNCE'] = "Announcing to %s" % (
- distroseries.changeslist)
-
- return get_template(archive, action) % information
-
-
-def send_mail(
- spr, archive, to_addrs, subject, mail_text, dry_run, from_addr=None,
- bcc=None, changesfile_content=None, attach_changes=False, logger=None):
- """Send an email to to_addrs with the given text and subject.
-
- :param spr: The `ISourcePackageRelease` to be notified about.
- :param archive: The target `IArchive`.
- :param to_addrs: A list of email addresses to be used as recipients.
- Each email must be a valid ASCII str instance or a unicode one.
- :param subject: The email's subject.
- :param mail_text: The text body of the email. Unicode is preserved in the
- email.
- :param dry_run: Whether or not an email should actually be sent. But
- please note that this flag is (largely) ignored.
- :param from_addr: The email address to be used as the sender. Must be a
- valid ASCII str instance or a unicode one. Defaults to the email
- for config.uploader.
- :param bcc: Optional email Blind Carbon Copy address(es).
- :param param changesfile_content: The content of the actual changesfile.
- :param attach_changes: A flag governing whether the original changesfile
- content shall be attached to the email.
- """
- extra_headers = {
- 'X-Katie': 'Launchpad actually',
- 'X-Launchpad-Archive': archive.reference,
- }
-
- # The deprecated PPA reference header is included for Ubuntu PPAs to
- # avoid breaking existing consumers.
- if archive.is_ppa and archive.distribution.name == u'ubuntu':
- extra_headers['X-Launchpad-PPA'] = get_ppa_reference(archive)
-
- # Include a 'X-Launchpad-Component' header with the component and
- # the section of the source package uploaded in order to facilitate
- # filtering on the part of the email recipients.
- if spr:
- xlp_component_header = 'component=%s, section=%s' % (
- spr.component.name, spr.section.name)
- extra_headers['X-Launchpad-Component'] = xlp_component_header
-
- if from_addr is None:
- from_addr = format_address(
- config.uploader.default_sender_name,
- config.uploader.default_sender_address)
-
- # All emails from here have a Bcc to the default recipient.
- bcc_text = format_address(
- config.uploader.default_recipient_name,
- config.uploader.default_recipient_address)
- if bcc:
- bcc_text = "%s, %s" % (bcc_text, bcc)
- extra_headers['Bcc'] = bcc_text
-
- recipients = ", ".join(to_addrs)
-
- if dry_run and logger is not None:
- debug(logger, "Would have sent a mail:")
- else:
- debug(logger, "Sent a mail:")
- debug(logger, " Subject: %s" % subject)
- debug(logger, " Sender: %s" % from_addr)
- debug(logger, " Recipients: %s" % recipients)
- if 'Bcc' in extra_headers:
- debug(logger, " Bcc: %s" % extra_headers['Bcc'])
- debug(logger, " Body:")
- for line in mail_text.splitlines():
- if isinstance(line, str):
- line = line.decode('utf-8', 'replace')
- debug(logger, line)
-
- if not dry_run:
- # Since we need to send the original changesfile as an
- # attachment the sendmail() method will be used as opposed to
- # simple_sendmail().
- message = MIMEMultipart()
- message['from'] = from_addr
- message['subject'] = subject
- message['to'] = recipients
-
- # Set the extra headers if any are present.
- for key, value in extra_headers.iteritems():
- message.add_header(key, value)
-
- # Add the email body.
- message.attach(
- MIMEText(sanitize_string(mail_text).encode('utf-8'),
- 'plain', 'utf-8'))
-
- if attach_changes:
- # Add the original changesfile as an attachment.
- if changesfile_content is not None:
- changesfile_text = sanitize_string(changesfile_content)
- else:
- changesfile_text = ("Sorry, changesfile not available.")
-
- attachment = MIMEText(
- changesfile_text.encode('utf-8'), 'plain', 'utf-8')
- attachment.add_header(
- 'Content-Disposition',
- 'attachment; filename="changesfile"')
- message.attach(attachment)
-
- # And finally send the message.
- sendmail(message)
-
-
-def sanitize_string(s):
- """Make sure string does not trigger 'ascii' codec errors.
-
- Convert string to unicode if needed so that characters outside
- the (7-bit) ASCII range do not cause errors like these:
-
- 'ascii' codec can't decode byte 0xc4 in position 21: ordinal
- not in range(128)
- """
- if isinstance(s, unicode):
- return s
- else:
- return guess_encoding(s)
-
-
-def debug(logger, msg, *args, **kwargs):
- """Shorthand debug notation for publish() methods."""
- if logger is not None:
- logger.debug(msg, *args, **kwargs)
-
-
-def is_valid_uploader(person, distribution):
- """Is `person` an uploader for `distribution`?
-
- A `None` person is not an uploader.
- """
- if person is None:
- return None
- else:
- return not getUtility(IArchivePermissionSet).componentsForUploader(
- distribution.main_archive, person).is_empty()
-
-
-def get_upload_notification_recipients(blamer, archive, distroseries,
- logger=None, changes=None, spr=None,
- bprs=None):
- """Return a list of recipients for notification emails."""
- debug(logger, "Building recipients list.")
- candidate_recipients = [blamer]
- info = fetch_information(spr, bprs, changes)
-
- 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.")
- candidate_recipients.append(changer)
-
- if archive.is_ppa:
- # For PPAs, any person or team mentioned explicitly in the
- # ArchivePermissions as uploaders for the archive will also
- # get emailed.
- candidate_recipients.extend([
- permission.person
- for permission in archive.getUploadersForComponent()])
- elif archive.is_copy:
- # For copy archives, notifying anyone else will probably only
- # confuse them.
- pass
- else:
- # If this is not a PPA, we also consider maintainer and changed-by.
- if blamer is not None:
- if is_valid_uploader(maintainer, distroseries.distribution):
- debug(logger, "Adding maintainer to recipients")
- candidate_recipients.append(maintainer)
-
- if is_valid_uploader(changer, distroseries.distribution):
- debug(logger, "Adding changed-by to recipients")
- candidate_recipients.append(changer)
-
- # Collect email addresses to notify. Skip persons who do not have a
- # preferredemail set, such as people who have not activated their
- # Launchpad accounts (and are therefore not expecting this email).
- recipients = [
- format_address_for_person(person)
- for person in filter(None, set(candidate_recipients))
- if person.preferredemail is not None]
-
- for recipient in recipients:
- debug(logger, "Adding recipient: '%s'", recipient)
-
- return recipients
-
-
def build_uploaded_files_list(spr, builds, customfiles, logger):
"""Return a list of tuples of (filename, component, section).
Component and section are only set where the file is a source upload.
If an empty list is returned, it means there are no files.
- Raises LanguagePackRejection if a language pack is detected.
- No emails should be sent for language packs.
"""
- files = []
- # Bail out early if this is an upload for the translations
- # section.
if spr:
- if spr.section.name == 'translations':
- debug(logger,
- "Skipping acceptance and announcement, it is a "
- "language-package upload.")
- raise LanguagePackEncountered
for sprfile in spr.files:
- files.append(
- (sprfile.libraryfile.filename, spr.component.name,
- spr.section.name))
+ yield (
+ sprfile.libraryfile.filename, spr.component.name,
+ spr.section.name)
# Component and section don't get set for builds and custom, since
# this information is only used in the summary string for source
# uploads.
for build in builds:
for bpr in build.build.binarypackages:
- files.extend([
- (bpf.libraryfile.filename, '', '') for bpf in bpr.files])
+ for bpf in bpr.files:
+ yield bpf.libraryfile.filename, '', ''
if customfiles:
- files.extend(
- [(file.libraryfilealias.filename, '', '') for file in customfiles])
-
- return files
+ for customfile in customfiles:
+ yield customfile.libraryfilealias.filename, '', ''
def build_summary(spr, files, action):
@@ -555,70 +273,336 @@
return summary
-def addr_to_person(addr):
- """Return an `IPerson` given a name and 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 `addr` is None.
- """
- if addr is None:
- return None
- 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 = addr_to_person(changed_by)
- return (
- spr and
- not bprs and
- changed_by == getUtility(ILaunchpadCelebrities).katie and
- pocket != PackagePublishingPocket.SECURITY)
-
-
-def fetch_information(spr, bprs, changes, previous_version=None):
- changelog = date = changedby = maintainer = None
-
- if changes:
- changelog = ChangesFile.formatChangesComment(
- sanitize_string(changes.get('Changes')))
- date = changes.get('Date')
- 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
- changelog = spr.aggregate_changelog(previous_version)
- date = spr.dateuploaded
- 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': changelog,
- 'date': date,
- 'changedby': changedby,
- 'maintainer': maintainer,
- }
-
-
-class LanguagePackEncountered(Exception):
- """Thrown when not wanting to email notifications for language packs."""
+class PackageUploadMailer(BaseMailer):
+
+ app = 'soyuz'
+
+ @classmethod
+ def getRecipientsForAction(cls, action, info, blamee, spr, bprs, archive,
+ distroseries, pocket, announce_from_person=None,
+ logger=None):
+ # If this is a binary or mixed upload, we don't send *any* emails
+ # provided it's not a rejection or a security upload:
+ if (
+ bprs and action != 'rejected' and
+ pocket != PackagePublishingPocket.SECURITY):
+ debug(logger, "Not sending email; upload is from a build.")
+ return {}, ''
+
+ if spr and spr.source_package_recipe_build and action == 'accepted':
+ debug(logger, "Not sending email; upload is from a recipe.")
+ return {}, ''
+
+ if spr and spr.section.name == 'translations':
+ debug(
+ logger,
+ "Skipping acceptance and announcement for language packs.")
+ return {}, ''
+
+ debug(logger, "Building recipients list.")
+ recipients = OrderedDict()
+
+ add_recipient(
+ recipients, blamee, PackageUploadRecipientReason.forRequester,
+ logger=logger)
+
+ changer = addr_to_person(info['changedby'])
+ maintainer = addr_to_person(info['maintainer'])
+
+ if blamee is None and not archive.is_copy:
+ debug(
+ logger,
+ "Changes file is unsigned; adding changer as recipient.")
+ add_recipient(
+ recipients, changer, PackageUploadRecipientReason.forChangedBy,
+ logger=logger)
+
+ if archive.is_ppa:
+ # For PPAs, any person or team mentioned explicitly in the
+ # ArchivePermissions as uploaders for the archive will also get
+ # emailed.
+ for permission in archive.getUploadersForComponent():
+ add_recipient(
+ recipients, permission.person,
+ PackageUploadRecipientReason.forPPAUploader, logger=logger)
+ elif archive.is_copy:
+ # For copy archives, notifying anyone else will probably only
+ # confuse them.
+ pass
+ else:
+ # If this is not a PPA, we also consider maintainer and changed-by.
+ if blamee is not None:
+ if is_valid_uploader(maintainer, distroseries.distribution):
+ debug(logger, "Adding maintainer to recipients")
+ add_recipient(
+ recipients, maintainer,
+ PackageUploadRecipientReason.forMaintainer,
+ logger=logger)
+
+ if is_valid_uploader(changer, distroseries.distribution):
+ debug(logger, "Adding changed-by to recipients")
+ add_recipient(
+ recipients, changer,
+ PackageUploadRecipientReason.forChangedBy,
+ logger=logger)
+
+ if announce_from_person is not None:
+ announce_from_addr = (
+ announce_from_person.displayname,
+ announce_from_person.preferredemail.email)
+ else:
+ announce_from_addr = info['changedby']
+
+ # If we're sending an acceptance notification for a non-PPA upload,
+ # announce if possible. Avoid announcing backports, binary-only
+ # security uploads, or autosync uploads.
+ if (action == 'accepted' and distroseries.changeslist
+ 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, announce_from_addr)):
+ recipient = StubPerson(distroseries.changeslist)
+ recipients[recipient] = (
+ PackageUploadRecipientReason.forChangesList(recipient))
+
+ if announce_from_addr is not None:
+ announce_from_address = format_address(*announce_from_addr)
+ else:
+ announce_from_address = None
+ return recipients, announce_from_address
+
+ @classmethod
+ def forAction(cls, action, blamee, spr, bprs, customfiles, archive,
+ distroseries, pocket, changes=None, changesfile_object=None,
+ announce_from_person=None, previous_version=None,
+ logger=None, **kwargs):
+ info = fetch_information(
+ spr, bprs, changes, previous_version=previous_version)
+ recipients, announce_from_address = cls.getRecipientsForAction(
+ action, info, blamee, spr, bprs, archive, distroseries, pocket,
+ announce_from_person=announce_from_person, logger=logger)
+ subject = calculate_subject(
+ spr, bprs, customfiles, archive, distroseries, pocket, action,
+ changesfile_object=changesfile_object)
+ if subject is None:
+ # We don't even have enough information to build a minimal
+ # subject, so do nothing.
+ recipients = {}
+ template_name = "upload-"
+ if action in ("new", "accepted", "announcement"):
+ template_name += action
+ elif action == "unapproved":
+ template_name += "accepted"
+ elif action == "rejected":
+ template_name += "rejection"
+ if archive.is_ppa:
+ template_name = "ppa-%s" % template_name
+ template_name += ".txt"
+ from_address = format_address(
+ config.uploader.default_sender_name,
+ config.uploader.default_sender_address)
+ return cls(
+ subject, template_name, recipients, from_address, action, info,
+ blamee, spr, bprs, customfiles, archive, distroseries, pocket,
+ changes=changes, announce_from_address=announce_from_address,
+ logger=logger, **kwargs)
+
+ def __init__(self, subject, template_name, recipients, from_address,
+ action, info, blamee, spr, bprs, customfiles, archive,
+ distroseries, pocket, summary_text=None, changes=None,
+ changesfile_content=None, dry_run=False,
+ announce_from_address=None, previous_version=None,
+ logger=None):
+ super(PackageUploadMailer, self).__init__(
+ subject, template_name, recipients, from_address,
+ notification_type="package-upload")
+ self.action = action
+ self.info = info
+ self.blamee = blamee
+ self.spr = spr
+ self.bprs = bprs
+ self.customfiles = customfiles
+ self.archive = archive
+ self.distroseries = distroseries
+ self.pocket = pocket
+ self.changes = changes
+ self.changesfile_content = changesfile_content
+ self.dry_run = dry_run
+ self.logger = logger
+ self.announce_from_address = announce_from_address
+ self.previous_version = previous_version
+
+ if action == 'rejected':
+ self.summarystring = summary_text
+ else:
+ files = build_uploaded_files_list(spr, bprs, customfiles, logger)
+ summary = build_summary(spr, files, action)
+ if summary_text:
+ summary.append(summary_text)
+ self.summarystring = "\n".join(summary)
+
+ def _getFromAddress(self, email, recipient):
+ """See `BaseMailer`."""
+ if (zope_isinstance(recipient, StubPerson) and
+ self.announce_from_address is not None):
+ return self.announce_from_address
+ else:
+ return super(PackageUploadMailer, self)._getFromAddress(
+ email, recipient)
+
+ def _getHeaders(self, email, recipient):
+ """See `BaseMailer`."""
+ headers = super(PackageUploadMailer, self)._getHeaders(
+ email, recipient)
+ headers['X-Katie'] = 'Launchpad actually'
+ headers['X-Launchpad-Archive'] = self.archive.reference
+
+ # The deprecated PPA reference header is included for Ubuntu PPAs to
+ # avoid breaking existing consumers.
+ if self.archive.is_ppa and self.archive.distribution.name == u'ubuntu':
+ headers['X-Launchpad-PPA'] = get_ppa_reference(self.archive)
+
+ # Include a 'X-Launchpad-Component' header with the component and
+ # the section of the source package uploaded in order to facilitate
+ # filtering on the part of the email recipients.
+ if self.spr:
+ headers['X-Launchpad-Component'] = 'component=%s, section=%s' % (
+ self.spr.component.name, self.spr.section.name)
+
+ # All emails from here have a Bcc to the default recipient.
+ bcc_text = format_address(
+ config.uploader.default_recipient_name,
+ config.uploader.default_recipient_address)
+ if zope_isinstance(recipient, StubPerson):
+ name = None
+ if self.spr:
+ name = self.spr.name
+ elif self.bprs:
+ name = self.bprs[0].build.source_package_release.name
+ if name:
+ distribution = self.distroseries.distribution
+ email_base = distribution.package_derivatives_email
+ if email_base:
+ bcc_text += ", " + email_base.format(package_name=name)
+ headers['Bcc'] = bcc_text
+
+ return headers
+
+ def _addAttachments(self, ctrl, email):
+ """See `BaseMailer`."""
+ if not self.archive.is_ppa:
+ if self.changesfile_content is not None:
+ changesfile_text = sanitize_string(self.changesfile_content)
+ else:
+ changesfile_text = "Sorry, changesfile not available."
+ ctrl.addAttachment(
+ changesfile_text, content_type='text/plain',
+ filename='changesfile', charset='utf-8')
+
+ def _getTemplateName(self, email, recipient):
+ """See `BaseMailer`."""
+ if zope_isinstance(recipient, StubPerson):
+ return "upload-announcement.txt"
+ else:
+ return self._template_name
+
+ def _getTemplateParams(self, email, recipient):
+ """See `BaseMailer`."""
+ params = super(PackageUploadMailer, self)._getTemplateParams(
+ email, recipient)
+ params.update({
+ 'STATUS': ACTION_DESCRIPTIONS[self.action],
+ 'SUMMARY': self.summarystring,
+ 'DATE': '',
+ 'CHANGESFILE': '',
+ 'DISTRO': self.distroseries.distribution.title,
+ 'ANNOUNCE': 'No announcement sent',
+ 'CHANGEDBY': '',
+ 'MAINTAINER': '',
+ 'ORIGIN': '',
+ 'SIGNER': '',
+ 'SPR_URL': '',
+ 'ARCHIVE_URL': canonical_url(self.archive),
+ 'USERS_ADDRESS': config.launchpad.users_address,
+ })
+ changes = self.changes
+ if changes is None:
+ changes = {}
+
+ if self.info['date'] is not None:
+ params['DATE'] = 'Date: %s' % self.info['date']
+ if self.info['changelog'] is not None:
+ params['CHANGESFILE'] = self.info['changelog']
+ if self.spr:
+ params['SPR_URL'] = canonical_url(
+ self.distroseries.distribution.getSourcePackageRelease(
+ self.spr))
+
+ # 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(self.info['changedby'])
+ if self.info['changedby']:
+ params['CHANGEDBY'] = '\nChanged-By: %s' % rfc822_encode_address(
+ *self.info['changedby'])
+ if (self.blamee is not None and self.blamee != changedby_person
+ and self.blamee.preferredemail):
+ params['SIGNER'] = '\nSigned-By: %s' % rfc822_encode_address(
+ self.blamee.displayname, self.blamee.preferredemail.email)
+ if (self.info['maintainer']
+ and self.info['maintainer'] != self.info['changedby']):
+ params['MAINTAINER'] = '\nMaintainer: %s' % rfc822_encode_address(
+ *self.info['maintainer'])
+
+ origin = changes.get('Origin')
+ if origin:
+ params['ORIGIN'] = '\nOrigin: %s' % origin
+ if self.action == 'unapproved':
+ params['SUMMARY'] += (
+ "\nThis upload awaits approval by a distro manager\n")
+ if self.distroseries.changeslist:
+ params['ANNOUNCE'] = "Announcing to %s" % (
+ self.distroseries.changeslist)
+
+ return params
+
+ def _getFooter(self, email, recipient, params):
+ """See `BaseMailer`."""
+ if zope_isinstance(recipient, StubPerson):
+ return None
+ else:
+ footer_lines = []
+ if self.archive.is_ppa:
+ footer_lines.append("%(ARCHIVE_URL)s\n")
+ footer_lines.append("%(reason)s\n")
+ return "".join(footer_lines) % params
+
+ def generateEmail(self, email, recipient, force_no_attachments=False):
+ """See `BaseMailer`."""
+ ctrl = super(PackageUploadMailer, self).generateEmail(
+ email, recipient, force_no_attachments=force_no_attachments)
+ if self.dry_run:
+ debug(self.logger, "Would have sent a mail:")
+ else:
+ debug(self.logger, "Sent a mail:")
+ debug(self.logger, " Subject: %s" % ctrl.subject)
+ debug(self.logger, " Sender: %s" % ctrl.from_addr)
+ debug(self.logger, " Recipients: %s" % ", ".join(ctrl.to_addrs))
+ if 'Bcc' in ctrl.headers:
+ debug(self.logger, " Bcc: %s" % ctrl.headers['Bcc'])
+ debug(self.logger, " Body:")
+ for line in ctrl.body.splitlines():
+ if isinstance(line, bytes):
+ line = line.decode('utf-8', 'replace')
+ debug(self.logger, line)
+ return ctrl
+
+ def sendOne(self, email, recipient):
+ """See `BaseMailer`."""
+ if self.dry_run:
+ # Just generate the email for the sake of debugging output.
+ self.generateEmail(email, recipient)
+ else:
+ super(PackageUploadMailer, self).sendOne(email, recipient)
=== added directory 'lib/lp/soyuz/mail/tests'
=== added file 'lib/lp/soyuz/mail/tests/__init__.py'
=== renamed file 'lib/lp/soyuz/adapters/tests/test_notification.py' => 'lib/lp/soyuz/mail/tests/test_packageupload.py'
--- lib/lp/soyuz/adapters/tests/test_notification.py 2015-07-29 07:01:04 +0000
+++ lib/lp/soyuz/mail/tests/test_packageupload.py 2015-08-25 14:09:28 +0000
@@ -2,35 +2,35 @@
# NOTE: The first line above must stay first; do not move the copyright
# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
#
-# Copyright 2011-2014 Canonical Ltd. This software is licensed under the
+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
from textwrap import dedent
-from storm.store import Store
+from testtools.matchers import (
+ Contains,
+ ContainsDict,
+ Equals,
+ KeysEqual,
+ )
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
+from lp.archivepublisher.utils import get_ppa_reference
from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.services.log.logger import BufferLogger
-from lp.services.mail.sendmail import format_address_for_person
from lp.services.propertycache import get_property_cache
from lp.services.webapp.publisher import canonical_url
-from lp.soyuz.adapters.notification import (
- assemble_body,
- calculate_subject,
- fetch_information,
- get_upload_notification_recipients,
- is_auto_sync_upload,
- notify,
- reject_changes_file,
- )
from lp.soyuz.enums import (
ArchivePurpose,
PackageUploadCustomFormat,
)
from lp.soyuz.interfaces.component import IComponentSet
-from lp.soyuz.model.component import ComponentSelection
+from lp.soyuz.mail.packageupload import (
+ calculate_subject,
+ fetch_information,
+ is_auto_sync_upload,
+ PackageUploadMailer,
+ )
from lp.soyuz.model.distributionsourcepackagerelease import (
DistributionSourcePackageRelease,
)
@@ -49,7 +49,7 @@
layer = LaunchpadZopelessLayer
- def test_notify_from_unicode_names(self):
+ def test_mail_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.
creator = self.factory.makePerson(displayname=u"Loïc")
@@ -60,9 +60,9 @@
distroseries = self.factory.makeDistroSeries()
distroseries.changeslist = "blah@xxxxxxxxxxx"
blamer = self.factory.makePerson(displayname=u"Stéphane")
- notify(
- blamer, spr, [], [], archive, distroseries, pocket,
- action='accepted')
+ mailer = PackageUploadMailer.forAction(
+ "accepted", blamer, spr, [], [], archive, distroseries, pocket)
+ mailer.sendAll()
notifications = pop_notifications()
self.assertEqual(2, len(notifications))
msg = notifications[1].get_payload(0)
@@ -98,13 +98,15 @@
blamer = self.factory.makePerson()
if from_person is None:
from_person = self.factory.makePerson()
- notify(
- blamer, spr, [], [], archive, distroseries, pocket,
- action='accepted', announce_from_person=from_person)
+ mailer = PackageUploadMailer.forAction(
+ "accepted", blamer, spr, [], [], archive, distroseries, pocket,
+ announce_from_person=from_person)
+ mailer.sendAll()
- def test_notify_from_person_override(self):
- # notify() takes an optional from_person to override the calculated
- # From: address in announcement emails.
+ def test_forAction_announce_from_person_override(self):
+ # PackageUploadMailer.forAction() takes an optional
+ # announce_from_person to override the calculated From: address in
+ # announcement emails.
spr = self.factory.makeSourcePackageRelease()
self.factory.makeSourcePackageReleaseFile(sourcepackagerelease=spr)
archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
@@ -114,21 +116,28 @@
blamer = self.factory.makePerson()
from_person = self.factory.makePerson(
email="lemmy@xxxxxxxxxxx", displayname="Lemmy Kilmister")
- notify(
- blamer, spr, [], [], archive, distroseries, pocket,
- action='accepted', announce_from_person=from_person)
+ mailer = PackageUploadMailer.forAction(
+ "accepted", blamer, spr, [], [], archive, distroseries, pocket,
+ announce_from_person=from_person)
+ mailer.sendAll()
notifications = pop_notifications()
self.assertEqual(2, len(notifications))
# The first notification is to the blamer, the second notification is
# to the announce list, which is the one that gets the overridden
# From:
- self.assertEqual(
- "Lemmy Kilmister <lemmy@xxxxxxxxxxx>", notifications[1]["From"])
+ self.assertThat(
+ dict(notifications[1]),
+ ContainsDict({
+ "From": Equals("Lemmy Kilmister <lemmy@xxxxxxxxxxx>"),
+ "X-Launchpad-Message-Rationale": Equals("Changes-List"),
+ "X-Launchpad-Notification-Type": Equals("package-upload"),
+ }))
- def test_notify_from_person_override_with_unicode_names(self):
- # notify() takes an optional from_person to override the calculated
- # From: address in announcement emails. Non-ASCII real names should be
- # correctly encoded in the From heade.
+ def test_forAction_announce_from_person_override_with_unicode_names(self):
+ # PackageUploadMailer.forAction() takes an optional
+ # announce_from_person to override the calculated From: address in
+ # announcement emails. Non-ASCII real names should be correctly
+ # encoded in the From header.
spr = self.factory.makeSourcePackageRelease()
self.factory.makeSourcePackageReleaseFile(sourcepackagerelease=spr)
archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
@@ -138,21 +147,28 @@
blamer = self.factory.makePerson()
from_person = self.factory.makePerson(
email="loic@xxxxxxxxxxx", displayname=u"Loïc Motörhead")
- notify(
- blamer, spr, [], [], archive, distroseries, pocket,
- action='accepted', announce_from_person=from_person)
+ mailer = PackageUploadMailer.forAction(
+ "accepted", blamer, spr, [], [], archive, distroseries, pocket,
+ announce_from_person=from_person)
+ mailer.sendAll()
notifications = pop_notifications()
self.assertEqual(2, len(notifications))
# The first notification is to the blamer, the second notification is
# to the announce list, which is the one that gets the overridden
# From:
- self.assertEqual(
- "=?utf-8?q?Lo=C3=AFc_Mot=C3=B6rhead?= <loic@xxxxxxxxxxx>",
- notifications[1]["From"])
+ self.assertThat(
+ dict(notifications[1]),
+ ContainsDict({
+ "From": Equals(
+ "=?utf-8?q?Lo=C3=AFc_Mot=C3=B6rhead?= <loic@xxxxxxxxxxx>"),
+ "X-Launchpad-Message-Rationale": Equals("Changes-List"),
+ "X-Launchpad-Notification-Type": Equals("package-upload"),
+ }))
- def test_notify_bcc_to_derivatives_list(self):
- # notify() will BCC the announcement email to the address defined in
- # Distribution.package_derivatives_email if it's defined.
+ def test_forAction_bcc_to_derivatives_list(self):
+ # PackageUploadMailer.forAction() will BCC the announcement email to
+ # the address defined in Distribution.package_derivatives_email if
+ # it's defined.
email = "{package_name}_thing@xxxxxxx"
distroseries = self.factory.makeDistroSeries()
with person_logged_in(distroseries.distribution.owner):
@@ -162,9 +178,14 @@
notifications = pop_notifications()
self.assertEqual(2, len(notifications))
- bcc_address = notifications[1]["Bcc"]
expected_email = email.format(package_name=spr.sourcepackagename.name)
- self.assertIn(expected_email, bcc_address)
+ self.assertThat(
+ dict(notifications[1]),
+ ContainsDict({
+ "Bcc": Contains(expected_email),
+ "X-Launchpad-Message-Rationale": Equals("Changes-List"),
+ "X-Launchpad-Notification-Type": Equals("package-upload"),
+ }))
def test_fetch_information_spr_multiple_changelogs(self):
# If previous_version is passed the "changelog" entry in the
@@ -182,9 +203,9 @@
self.assertIn("foo (1.1)", info['changelog'])
self.assertIn("foo (1.2)", info['changelog'])
- def test_notify_bpr_rejected(self):
- # If we notify about a rejected bpr with no source, a notification is
- # sent.
+ def test_forAction_bpr_rejected(self):
+ # If we try to send mail about a rejected bpr with no source, a
+ # notification is sent.
bpr = self.factory.makeBinaryPackageRelease()
changelog = self.factory.makeChangelog(spn="foo", versions=["1.1"])
removeSecurityProxy(
@@ -196,31 +217,30 @@
distroseries = self.factory.makeDistroSeries()
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')
+ mailer = PackageUploadMailer.forAction(
+ "rejected", person, None, [bpr], [], archive, distroseries, pocket,
+ summary_text="Rejected by archive administrator.")
+ mailer.sendAll()
[notification] = pop_notifications()
- body = notification.get_payload()[0].get_payload()
+ body = notification.get_payload(decode=True)
self.assertEqual('Blamer <blamer@xxxxxxxxxxx>', notification['To'])
expected_body = dedent("""\
Rejected:
Rejected by archive administrator.
- foo (1.1) unstable; urgency=3Dlow
+ foo (1.1) unstable; urgency=low
* 1.1.
- =3D=3D=3D
+ ===
If you don't understand why your files were rejected please send an email
to launchpad-users@xxxxxxxxxxxxxxxxxxx for help (requires membership).
- --
+ %s
http://launchpad.dev/~archiver/+archive/ubuntu/ppa
- You are receiving this email because you are the uploader of the above
- PPA package.
- """)
+ You are receiving this email because you made this upload.
+ """ % "-- ")
self.assertEqual(expected_body, body)
@@ -295,42 +315,41 @@
None, [bpr], [], archive, distroseries, pocket, 'accepted')
self.assertEqual(expected_subject, subject)
- def test_notify_bpr(self):
- # If we notify about an accepted bpr with no source, it is from a
- # build, and no notification is sent.
+ def test_forAction_bpr(self):
+ # If we try to send mail about an accepted bpr with no source, it is
+ # from a build, and no notification is sent.
bpr = self.factory.makeBinaryPackageRelease()
archive = self.factory.makeArchive()
pocket = self.factory.getAnyPocket()
distroseries = self.factory.makeDistroSeries()
person = self.factory.makePerson()
- notify(
- person, None, [bpr], [], archive, distroseries, pocket,
- action='accepted')
+ mailer = PackageUploadMailer.forAction(
+ "accepted", person, None, [bpr], [], archive, distroseries, pocket)
+ mailer.sendAll()
notifications = pop_notifications()
self.assertEqual(0, len(notifications))
def test_reject_changes_file_no_email(self):
- # If we are rejecting a mail, and the person to notify has no
+ # If we are rejecting an upload, and the person to notify has no
# preferred email, we should return early.
archive = self.factory.makeArchive()
distroseries = self.factory.makeDistroSeries()
uploader = self.factory.makePerson()
get_property_cache(uploader).preferredemail = None
- email = '%s <foo@xxxxxxxxxxx>' % uploader.displayname
- changes = {'Changed-By': email, 'Maintainer': email}
- logger = BufferLogger()
- reject_changes_file(
- uploader, '/tmp/changes', changes, archive, distroseries, '',
- logger=logger)
- self.assertIn(
- 'No recipients have a preferred email.', logger.getLogBuffer())
+ info = fetch_information(None, None, None)
+ recipients, _ = PackageUploadMailer.getRecipientsForAction(
+ 'rejected', info, uploader, None, [], archive, distroseries,
+ PackagePublishingPocket.RELEASE)
+ self.assertEqual({}, recipients)
def test_reject_with_no_changes(self):
# If we don't have any files and no changes content, nothing happens.
archive = self.factory.makeArchive()
distroseries = self.factory.makeDistroSeries()
pocket = self.factory.getAnyPocket()
- notify(None, None, (), (), archive, distroseries, pocket)
+ mailer = PackageUploadMailer.forAction(
+ "rejected", None, None, (), (), archive, distroseries, pocket)
+ mailer.sendAll()
notifications = pop_notifications()
self.assertEqual(0, len(notifications))
@@ -351,20 +370,18 @@
# Now set the uploaders.
component = getUtility(IComponentSet).ensure('main')
if component not in distroseries.components:
- store = Store.of(distroseries)
- store.add(
- ComponentSelection(
- distroseries=distroseries, component=component))
+ self.factory.makeComponentSelection(
+ distroseries=distroseries, component=component)
distribution.main_archive.newComponentUploader(maintainer, component)
distribution.main_archive.newComponentUploader(changer, component)
- observed = get_upload_notification_recipients(
- blamer, archive, distroseries, logger=None, changes=changes)
- self.assertContentEqual(
- [format_address_for_person(person) for person in expected],
- observed)
+ info = fetch_information(None, None, changes)
+ observed, _ = PackageUploadMailer.getRecipientsForAction(
+ 'accepted', info, blamer, None, [], archive, distroseries,
+ PackagePublishingPocket.RELEASE)
+ self.assertThat(observed, KeysEqual(*expected))
- def test_get_upload_notification_recipients_good_emails(self):
- # Test get_upload_notification_recipients with good email addresses..
+ def test_getRecipientsForAction_good_emails(self):
+ # Test getRecipientsForAction with good email addresses..
blamer, maintainer, changer = self._setup_recipients()
changes = {
'Date': '2001-01-01',
@@ -376,7 +393,7 @@
[blamer, maintainer, changer],
changes, blamer, maintainer, changer)
- def test_get_upload_notification_recipients_bad_maintainer_email(self):
+ def test_getRecipientsForAction_bad_maintainer_email(self):
blamer, maintainer, changer = self._setup_recipients()
changes = {
'Date': '2001-01-01',
@@ -387,9 +404,8 @@
self.assertRecipientsEqual(
[blamer, changer], changes, blamer, maintainer, changer)
- def test_get_upload_notification_recipients_bad_changedby_email(self):
- # Test get_upload_notification_recipients with invalid changedby
- # email address.
+ def test_getRecipientsForAction_bad_changedby_email(self):
+ # Test getRecipientsForAction with invalid changedby email address.
blamer, maintainer, changer = self._setup_recipients()
changes = {
'Date': '2001-01-01',
@@ -400,7 +416,7 @@
self.assertRecipientsEqual(
[blamer, maintainer], changes, blamer, maintainer, changer)
- def test_get_upload_notification_recipients_unsigned_copy_archive(self):
+ def test_getRecipientsForAction_unsigned_copy_archive(self):
# Notifications for unsigned build uploads to copy archives only go
# to the archive owner.
_, maintainer, changer = self._setup_recipients()
@@ -414,9 +430,92 @@
[], changes, None, maintainer, changer,
purpose=ArchivePurpose.COPY)
- def test_assemble_body_handles_no_preferred_email_for_changer(self):
+ def test__getHeaders_primary(self):
+ # _getHeaders returns useful values for headers used for filtering.
+ # For a primary archive, this includes the maintainer and changer.
+ blamer, maintainer, changer = self._setup_recipients()
+ distroseries = self.factory.makeDistroSeries()
+ archive = distroseries.distribution.main_archive
+ component = getUtility(IComponentSet).ensure("main")
+ if component not in distroseries.components:
+ self.factory.makeComponentSelection(
+ distroseries=distroseries, component=component)
+ archive.newComponentUploader(maintainer, component)
+ archive.newComponentUploader(changer, component)
+ spr = self.factory.makeSourcePackageRelease(
+ component=component, section_name="libs")
+ changes = {
+ 'Date': '2001-01-01',
+ 'Changed-By': 'Changer <changer@xxxxxxxxxxx>',
+ 'Maintainer': 'Maintainer <maintainer@xxxxxxxxxxx>',
+ 'Changes': ' * Foo!',
+ }
+ mailer = PackageUploadMailer.forAction(
+ "accepted", blamer, spr, [], [], archive, distroseries,
+ PackagePublishingPocket.RELEASE, changes=changes)
+ recipients = dict(mailer._recipients.getRecipientPersons())
+ for email, rationale in (
+ (blamer.preferredemail.email, "Requester"),
+ ("maintainer@xxxxxxxxxxx", "Maintainer"),
+ ("changer@xxxxxxxxxxx", "Changed-By")):
+ headers = mailer._getHeaders(email, recipients[email])
+ self.assertThat(
+ headers,
+ ContainsDict({
+ "X-Launchpad-Message-Rationale": Equals(rationale),
+ "X-Launchpad-Notification-Type": Equals("package-upload"),
+ "X-Katie": Equals("Launchpad actually"),
+ "X-Launchpad-Archive": Equals(archive.reference),
+ "X-Launchpad-Component": Equals(
+ "component=main, section=libs"),
+ }))
+ self.assertNotIn("X-Launchpad-PPA", headers)
+
+ def test__getHeaders_ppa(self):
+ # _getHeaders returns useful values for headers used for filtering.
+ # For a PPA, this includes other people with component upload
+ # permissions.
+ blamer = self.factory.makePerson()
+ uploader = self.factory.makePerson()
+ distroseries = self.factory.makeUbuntuDistroSeries()
+ archive = self.factory.makeArchive(
+ distribution=distroseries.distribution, purpose=ArchivePurpose.PPA)
+ component = getUtility(IComponentSet).ensure("main")
+ if component not in distroseries.components:
+ self.factory.makeComponentSelection(
+ distroseries=distroseries, component=component)
+ archive.newComponentUploader(uploader, component)
+ spr = self.factory.makeSourcePackageRelease(
+ component=component, section_name="libs")
+ changes = {
+ 'Date': '2001-01-01',
+ 'Changed-By': 'Changer <changer@xxxxxxxxxxx>',
+ 'Maintainer': 'Maintainer <maintainer@xxxxxxxxxxx>',
+ 'Changes': ' * Foo!',
+ }
+ mailer = PackageUploadMailer.forAction(
+ "accepted", blamer, spr, [], [], archive, distroseries,
+ PackagePublishingPocket.RELEASE, changes=changes)
+ recipients = dict(mailer._recipients.getRecipientPersons())
+ for email, rationale in (
+ (blamer.preferredemail.email, "Requester"),
+ (uploader.preferredemail.email, "PPA-Uploader")):
+ headers = mailer._getHeaders(email, recipients[email])
+ self.assertThat(
+ headers,
+ ContainsDict({
+ "X-Launchpad-Message-Rationale": Equals(rationale),
+ "X-Launchpad-Notification-Type": Equals("package-upload"),
+ "X-Katie": Equals("Launchpad actually"),
+ "X-Launchpad-Archive": Equals(archive.reference),
+ "X-Launchpad-PPA": Equals(get_ppa_reference(archive)),
+ "X-Launchpad-Component": Equals(
+ "component=main, section=libs"),
+ }))
+
+ def test__getTemplateParams_handles_no_preferred_email_for_changer(self):
# If changer has no preferred email address,
- # assemble_body should still work.
+ # _getTemplateParams should still work.
spr = self.factory.makeSourcePackageRelease()
blamer = self.factory.makePerson()
archive = self.factory.makeArchive()
@@ -424,11 +523,14 @@
spr.creator.setPreferredEmail(None)
- body = assemble_body(blamer, spr, [], archive, series, "",
- None, "unapproved")
- self.assertIn("Waiting for approval", body)
+ mailer = PackageUploadMailer.forAction(
+ "unapproved", blamer, spr, [], [], archive, series,
+ PackagePublishingPocket.RELEASE)
+ email, recipient = list(mailer._recipients.getRecipientPersons())[0]
+ params = mailer._getTemplateParams(email, recipient)
+ self.assertEqual("Waiting for approval", params["STATUS"])
- def test_assemble_body_inserts_package_url_for_distro_upload(self):
+ def test__getTemplateParams_inserts_package_url_for_distro_upload(self):
# The email body should contain the canonical url to the package
# page in the target distroseries.
spr = self.factory.makeSourcePackageRelease()
@@ -436,13 +538,16 @@
archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
series = self.factory.makeDistroSeries()
- body = assemble_body(blamer, spr, [], archive, series, "",
- None, "unapproved")
+ mailer = PackageUploadMailer.forAction(
+ "unapproved", blamer, spr, [], [], archive, series,
+ PackagePublishingPocket.RELEASE)
+ email, recipient = list(mailer._recipients.getRecipientPersons())[0]
+ params = mailer._getTemplateParams(email, recipient)
dsspr = DistributionSourcePackageRelease(series.distribution, spr)
url = canonical_url(dsspr)
- self.assertIn(url, body)
+ self.assertEqual(url, params["SPR_URL"])
- def test__is_auto_sync_upload__no_preferred_email_for_changer(self):
+ def test_is_auto_sync_upload__no_preferred_email_for_changer(self):
# If changer has no preferred email address,
# is_auto_sync_upload should still work.
result = is_auto_sync_upload(
=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py 2015-07-08 16:05:11 +0000
+++ lib/lp/soyuz/model/queue.py 2015-08-25 14:09:28 +0000
@@ -82,7 +82,6 @@
cachedproperty,
get_property_cache,
)
-from lp.soyuz.adapters.notification import notify
from lp.soyuz.enums import (
PackageUploadCustomFormat,
PackageUploadStatus,
@@ -115,6 +114,7 @@
QueueStateWriteProtectedError,
)
from lp.soyuz.interfaces.section import ISectionSet
+from lp.soyuz.mail.packageupload import PackageUploadMailer
from lp.soyuz.model.binarypackagename import BinaryPackageName
from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
from lp.soyuz.model.component import Component
@@ -915,11 +915,14 @@
else:
changesfile_content = 'No changes file content available.'
blamee = self.findPersonToNotify()
- notify(
- blamee, self.sourcepackagerelease, self.builds, self.customfiles,
- self.archive, self.distroseries, self.pocket, summary_text,
- changes, changesfile_content, changes_file_object,
- status_action[self.status], dry_run=dry_run, logger=logger)
+ mailer = PackageUploadMailer.forAction(
+ status_action[self.status], blamee, self.sourcepackagerelease,
+ self.builds, self.customfiles, self.archive, self.distroseries,
+ self.pocket, summary_text=summary_text, changes=changes,
+ changesfile_content=changesfile_content,
+ changesfile_object=changes_file_object, dry_run=dry_run,
+ logger=logger)
+ mailer.sendAll()
@property
def components(self):
=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
--- lib/lp/soyuz/scripts/packagecopier.py 2015-07-09 20:06:17 +0000
+++ lib/lp/soyuz/scripts/packagecopier.py 2015-08-25 14:09:28 +0000
@@ -19,7 +19,6 @@
from zope.security.proxy import removeSecurityProxy
from lp.services.database.bulk import load_related
-from lp.soyuz.adapters.notification import notify
from lp.soyuz.adapters.overrides import SourceOverride
from lp.soyuz.enums import SourcePackageFormat
from lp.soyuz.interfaces.archive import CannotCopy
@@ -31,6 +30,7 @@
ISourcePackagePublishingHistory,
)
from lp.soyuz.interfaces.queue import IPackageUploadCustom
+from lp.soyuz.mail.packageupload import PackageUploadMailer
from lp.soyuz.model.processacceptedbugsjob import (
close_bugs_for_sourcepublication,
)
@@ -572,9 +572,11 @@
if series is None:
series = source.distroseries
# In zopeless mode this email will be sent immediately.
- notify(
- person, source.sourcepackagerelease, [], [], archive,
- series, pocket, summary_text=error_text, action='rejected')
+ mailer = PackageUploadMailer.forAction(
+ 'rejected', person, source.sourcepackagerelease, [], [],
+ archive, series, pocket, summary_text=error_text,
+ logger=logger)
+ mailer.sendAll()
raise CannotCopy(error_text)
overrides_index = 0
@@ -610,14 +612,15 @@
sponsor=sponsor, packageupload=packageupload,
phased_update_percentage=phased_update_percentage, logger=logger)
if send_email:
- notify(
- person, source.sourcepackagerelease, [], [], archive,
- destination_series, pocket, action='accepted',
+ mailer = PackageUploadMailer.forAction(
+ 'accepted', person, source.sourcepackagerelease, [], [],
+ archive, destination_series, pocket,
announce_from_person=announce_from_person,
- previous_version=old_version)
+ previous_version=old_version, logger=logger)
+ mailer.sendAll()
if not archive.private and has_restricted_files(source):
# Fix copies by unrestricting files with privacy mismatch.
- # We must do this *after* calling notify (which only
+ # We must do this *after* calling mailer.sendAll (which only
# actually sends mail on commit), because otherwise the new
# changelog LFA won't be visible without a commit, which may
# not be safe here.
=== modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py'
--- lib/lp/soyuz/scripts/tests/test_copypackage.py 2015-05-19 02:24:48 +0000
+++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2015-08-25 14:09:28 +0000
@@ -4,10 +4,7 @@
__metaclass__ = type
import datetime
-from textwrap import (
- dedent,
- fill,
- )
+from textwrap import dedent
import pytz
from testtools.content import text_content
@@ -1426,24 +1423,20 @@
[notification] = pop_notifications()
self.assertEqual(
target_archive.reference, notification['X-Launchpad-Archive'])
- body = notification.get_payload()[0].get_payload()
- expected = (dedent("""\
+ body = notification.get_payload(decode=True)
+ expected = dedent("""\
Accepted:
OK: foo_1.0-2.dsc
-> Component: main Section: base
- foo (1.0-2) unstable; urgency=3Dlow
+ foo (1.0-2) unstable; urgency=low
* 1.0-2.
- --
+ %s
http://launchpad.dev/~archiver/+archive/ubuntutest/ppa
- """) +
- # Slight contortion to avoid a long line.
- fill(dedent("""\
- You are receiving this email because you are the uploader of the
- above PPA package.
- """), 72) + "\n")
+ You are receiving this email because you made this upload.
+ """ % "-- ")
self.assertEqual(expected, body)
def test_copy_generates_notification(self):
@@ -1481,8 +1474,7 @@
# Spurious newlines are a pain and don't really affect the end
# results so stripping is the easiest route here.
expected_text.strip()
- body = mail.get_payload()[0].get_payload()
- self.assertEqual(expected_text, body)
+ body = announcement.get_payload()[0].get_payload()
self.assertEqual(expected_text, body)
def test_sponsored_copy_notification(self):
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt 2013-09-27 04:13:23 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt 2015-08-25 14:09:28 +0000
@@ -347,9 +347,11 @@
if it is someone other than the uploader) and (usually) an email to the
distroseries' announcement list (see nascentupload-announcements.txt).
- >>> [notification, announcement] = pop_notifications()
- >>> print sort_addresses(notification['To'])
- Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>,
+ >>> [changer_notification, signer_notification,
+ ... announcement] = pop_notifications()
+ >>> print changer_notification['To']
+ Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+ >>> print signer_notification['To']
Foo Bar <foo.bar@xxxxxxxxxxxxx>
>>> print announcement['To']
autotest_changes@xxxxxxxxxx
@@ -511,6 +513,8 @@
Rejecting 'alsa-utils' source:
+ >>> stub.test_emails = []
+
>>> upload_manager_browser.getControl(name="QUEUE_ID").value = ['4']
>>> upload_manager_browser.getControl(name="Reject").disabled
False
@@ -543,8 +547,8 @@
Rejected:
Rejected by Sample Person: Foo
...
- You are receiving this email because you are the uploader, maintainer or
- signer of the above package.
+ You are receiving this email because you are the most recent person
+ listed in this package's changelog.
<BLANKLINE>
The override controls are now available for rejected packages.
=== modified file 'lib/lp/soyuz/tests/test_distroseriesqueue_debian_installer.py'
--- lib/lp/soyuz/tests/test_distroseriesqueue_debian_installer.py 2015-07-21 09:04:01 +0000
+++ lib/lp/soyuz/tests/test_distroseriesqueue_debian_installer.py 2015-08-25 14:09:28 +0000
@@ -7,6 +7,7 @@
of debian-installer custom upload extraction.
"""
+from itertools import chain
import os
import transaction
@@ -52,11 +53,16 @@
self.assertEqual(1, len(upload.queue_root.customfiles))
def test_generates_mail(self):
- # Two email messages were generated (acceptance and announcement).
+ # Three email messages were generated (acceptance to signer,
+ # acceptance to changer, and announcement).
self.anything_policy.setDistroSeriesAndPocket("hoary-test")
self.anything_policy.distroseries.changeslist = "announce@xxxxxxxxxxx"
self.uploadTestData()
- self.assertEqual(2, len(stub.test_emails))
+ self.assertContentEqual(
+ ["announce@xxxxxxxxxxx", "celso.providelo@xxxxxxxxxxxxx",
+ "foo.bar@xxxxxxxxxxxxx"],
+ list(chain.from_iterable(
+ [to_addrs for _, to_addrs, _ in stub.test_emails])))
def test_bad_upload_remains_in_accepted(self):
# Bad debian-installer uploads remain in accepted. Simulate an
=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_packagecopyjob.py 2015-04-09 05:16:37 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2015-08-25 14:09:28 +0000
@@ -1230,12 +1230,14 @@
# do it here.
emails = pop_notifications(sort_key=operator.itemgetter('To'))
- # We expect an uploader email and an announcement to the changeslist.
- self.assertEqual(2, len(emails))
- self.assertIn("requester@xxxxxxxxxxx", emails[0]['To'])
- self.assertIn("changes@xxxxxxxxxxx", emails[1]['To'])
+ # We expect an email to the signer, an email to the uploader, and an
+ # announcement to the changeslist.
+ self.assertEqual(3, len(emails))
+ self.assertIn("foo.bar@xxxxxxxxxxxxx", emails[0]['To'])
+ self.assertIn("requester@xxxxxxxxxxx", emails[1]['To'])
+ self.assertIn("changes@xxxxxxxxxxx", emails[2]['To'])
self.assertEqual(
- "Nancy Requester <requester@xxxxxxxxxxx>", emails[1]['From'])
+ "Nancy Requester <requester@xxxxxxxxxxx>", emails[2]['From'])
def test_silent(self):
# Copies into a non-PPA archive normally send emails. They can
=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
--- lib/lp/soyuz/tests/test_packageupload.py 2015-08-03 12:59:18 +0000
+++ lib/lp/soyuz/tests/test_packageupload.py 2015-08-25 14:09:28 +0000
@@ -29,7 +29,6 @@
from lp.services.job.interfaces.job import JobStatus
from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.mail import stub
-from lp.services.mail.sendmail import format_address_for_person
from lp.soyuz.adapters.overrides import SourceOverride
from lp.soyuz.enums import (
PackagePublishingStatus,
@@ -195,9 +194,9 @@
upload, uploader = self.makeSourcePackageUpload()
upload.acceptFromQueue()
self.assertEqual(2, len(stub.test_emails))
- # Emails sent are the announcement and the uploader's notification:
+ # Emails sent are the uploader's notification and the announcement:
+ self.assertEmail([uploader.preferredemail.email])
self.assertEmail(["autotest_changes@xxxxxxxxxx"])
- self.assertEmail([format_address_for_person(uploader)])
def test_acceptFromQueue_source_backports_sends_no_announcement(self):
# Accepting a source package into BACKPORTS does not send an
@@ -211,7 +210,7 @@
self.assertEqual(1, len(stub.test_emails))
# Only one email is sent, to the person in the changed-by field. No
# announcement email is sent.
- self.assertEmail([format_address_for_person(uploader)])
+ self.assertEmail([uploader.preferredemail.email])
def test_acceptFromQueue_source_translations_sends_no_email(self):
# Accepting source packages in the "translations" section (i.e.
@@ -316,7 +315,7 @@
upload, uploader = self.makeSourcePackageUpload()
upload.rejectFromQueue(self.factory.makePerson())
self.assertEqual(1, len(stub.test_emails))
- self.assertEmail([format_address_for_person(uploader)])
+ self.assertEmail([uploader.preferredemail.email])
def test_rejectFromQueue_binary_sends_email(self):
# Rejecting a binary package sends an email to the uploader.
@@ -324,7 +323,7 @@
upload, uploader = self.makeBuildPackageUpload()
upload.rejectFromQueue(self.factory.makePerson())
self.assertEqual(1, len(stub.test_emails))
- self.assertEmail([format_address_for_person(uploader)])
+ self.assertEmail([uploader.preferredemail.email])
def test_rejectFromQueue_source_translations_sends_no_email(self):
# Rejecting a language pack sends no email.
Follow ups