launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19238
Re: [Merge] lp:~cjwatson/launchpad/upload-mail into lp:launchpad
Review: Approve code
Diff comments:
>
> === 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 23:28:10 +0000
> @@ -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."
It's unfortunate we can't say "signed" rather than "made", but it is indeed difficult to distinguish from copies.
> + 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)
The maintainer and some other fields may be team. Should we include the team name in that case, as usual?
> +
> + @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 forAnnouncement(cls, recipient):
> + return cls(recipient, recipient, "Announcement", "")
> +
> + 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 = {
> @@ -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).
s/Return/Generate/
>
> 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:
This drops the preferredemail presence check from the old code. Couldn't this crash in some circumstances?
> + 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.forAnnouncement(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
Sneaky. Could use a comment.
> + 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):
Could really use a comment.
> + 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)
--
https://code.launchpad.net/~cjwatson/launchpad/upload-mail/+merge/269066
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
References