← Back to team overview

launchpad-reviewers team mailing list archive

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