def send(self):
     """Send a message to the user about the product's licence."""
     if not self.needs_notification(self.product):
         # The project has a common licence.
         return False
     maintainer = self.product.owner
     if maintainer.is_team:
         user_address = maintainer.getTeamAdminsEmailAddresses()
     else:
         user_address = format_address_for_person(maintainer)
     from_address = format_address(
         "Launchpad", config.canonical.noreply_from_address)
     commercial_address = format_address(
         'Commercial', '*****@*****.**')
     substitutions = dict(
         user_displayname=maintainer.displayname,
         user_name=maintainer.name,
         product_name=self.product.name,
         product_url=canonical_url(self.product),
         commercial_use_expiration=self.getCommercialUseMessage(),
         )
     # Email the user about licence policy.
     subject = (
         "Licence information for %(product_name)s "
         "in Launchpad" % substitutions)
     template = get_email_template(
         self.getTemplateName(), app='registry')
     message = template % substitutions
     simple_sendmail(
         from_address, user_address,
         subject, message, headers={'Reply-To': commercial_address})
     # Inform that Launchpad recognized the licence change.
     self._addLicenseChangeToReviewWhiteboard()
     return True
def get_bugmail_from_address(person, bug):
    """Returns the right From: address to use for a bug notification."""
    if person == getUtility(ILaunchpadCelebrities).janitor:
        return format_address(
            'Launchpad Bug Tracker',
            "%s@%s" % (bug.id, config.launchpad.bugs_domain))

    if person.hide_email_addresses:
        return format_address(
            person.displayname,
            "%s@%s" % (bug.id, config.launchpad.bugs_domain))

    if person.preferredemail is not None:
        return format_address(person.displayname, person.preferredemail.email)

    # XXX: Bjorn Tillenius 2006-04-05:
    # The person doesn't have a preferred email set, but he
    # added a comment (either via the email UI, or because he was
    # imported as a deaf reporter). It shouldn't be possible to use the
    # email UI if you don't have a preferred email set, but work around
    # it for now by trying hard to find the right email address to use.
    email_addresses = shortlist(
        getUtility(IEmailAddressSet).getByPerson(person))
    if not email_addresses:
        # XXX: Bjorn Tillenius 2006-05-21 bug=33427:
        # A user should always have at least one email address,
        # but due to bug #33427, this isn't always the case.
        return format_address(person.displayname,
            "%s@%s" % (bug.id, config.launchpad.bugs_domain))

    # At this point we have no validated emails to use: if any of the
    # person's emails had been validated the preferredemail would be
    # set. Since we have no idea of which email address is best to use,
    # we choose the first one.
    return format_address(person.displayname, email_addresses[0].email)
Example #3
0
def get_bugmail_from_address(person, bug):
    """Returns the right From: address to use for a bug notification."""
    if person == getUtility(ILaunchpadCelebrities).janitor:
        return format_address('Launchpad Bug Tracker',
                              "%s@%s" % (bug.id, config.launchpad.bugs_domain))

    if person.hide_email_addresses:
        return format_address(person.displayname,
                              "%s@%s" % (bug.id, config.launchpad.bugs_domain))

    if person.preferredemail is not None:
        return format_address(person.displayname, person.preferredemail.email)

    # XXX: Bjorn Tillenius 2006-04-05:
    # The person doesn't have a preferred email set, but he
    # added a comment (either via the email UI, or because he was
    # imported as a deaf reporter). It shouldn't be possible to use the
    # email UI if you don't have a preferred email set, but work around
    # it for now by trying hard to find the right email address to use.
    email_addresses = shortlist(
        getUtility(IEmailAddressSet).getByPerson(person))
    if not email_addresses:
        # XXX: Bjorn Tillenius 2006-05-21 bug=33427:
        # A user should always have at least one email address,
        # but due to bug #33427, this isn't always the case.
        return format_address(person.displayname,
                              "%s@%s" % (bug.id, config.launchpad.bugs_domain))

    # At this point we have no validated emails to use: if any of the
    # person's emails had been validated the preferredemail would be
    # set. Since we have no idea of which email address is best to use,
    # we choose the first one.
    return format_address(person.displayname, email_addresses[0].email)
Example #4
0
 def send(self):
     """Send a message to the user about the product's licence."""
     if not self.needs_notification(self.product):
         # The project has a common licence.
         return False
     maintainer = self.product.owner
     if maintainer.is_team:
         user_address = maintainer.getTeamAdminsEmailAddresses()
     else:
         user_address = format_address_for_person(maintainer)
     from_address = format_address("Launchpad",
                                   config.canonical.noreply_from_address)
     commercial_address = format_address('Commercial',
                                         '*****@*****.**')
     substitutions = dict(
         user_displayname=maintainer.displayname,
         user_name=maintainer.name,
         product_name=self.product.name,
         product_url=canonical_url(self.product),
         commercial_use_expiration=self.getCommercialUseMessage(),
     )
     # Email the user about licence policy.
     subject = ("Licence information for %(product_name)s "
                "in Launchpad" % substitutions)
     template = get_email_template(self.getTemplateName(), app='registry')
     message = template % substitutions
     simple_sendmail(from_address,
                     user_address,
                     subject,
                     message,
                     headers={'Reply-To': commercial_address})
     # Inform that Launchpad recognized the licence change.
     self._addLicenseChangeToReviewWhiteboard()
     return True
Example #5
0
def notify_new_ppa_subscription(subscription, event):
    """Notification that a new PPA subscription can be activated."""
    non_active_subscribers = subscription.getNonActiveSubscribers()

    archive = subscription.archive

    # We don't send notification emails for some PPAs, particularly those that
    # are purchased via the Software Centre, so that its users do not have to
    # learn about Launchpad.
    if archive.suppress_subscription_notifications:
        return

    registrant_name = subscription.registrant.displayname
    ppa_displayname = archive.displayname
    ppa_reference = "ppa:%s/%s" % (
        archive.owner.name, archive.name)
    ppa_description = archive.description
    subject = 'PPA access granted for ' + ppa_displayname

    template = get_email_template('ppa-subscription-new.txt', app='soyuz')

    for person, preferred_email in non_active_subscribers:
        to_address = [preferred_email.email]
        root = getUtility(ILaunchpadRoot)
        recipient_subscriptions_url = "%s~/+archivesubscriptions" % (
            canonical_url(root))
        description_blurb = '.'
        if ppa_description is not None and ppa_description != '':
            description_blurb = (
                ' and has the following description:\n\n%s' % ppa_description)
        replacements = {
            'recipient_name': person.displayname,
            'registrant_name': registrant_name,
            'registrant_profile_url': canonical_url(subscription.registrant),
            'ppa_displayname': ppa_displayname,
            'ppa_reference': ppa_reference,
            'ppa_description_blurb': description_blurb,
            'recipient_subscriptions_url': recipient_subscriptions_url,
            }
        body = MailWrapper(72).format(template % replacements,
                                      force_wrap=True)

        from_address = format_address(
            registrant_name, config.canonical.noreply_from_address)

        headers = {
            'Sender': config.canonical.bounce_address,
            }

        # If the registrant has a preferred email, then use it for the
        # Reply-To.
        if subscription.registrant.preferredemail:
            headers['Reply-To'] = format_address(
                registrant_name,
                subscription.registrant.preferredemail.email)

        simple_sendmail(from_address, to_address, subject, body, headers)
def notify_message_held(message_approval, event):
    """Send a notification of a message hold to all team administrators."""
    message_details = getAdapter(message_approval, IHeldMessageDetails)
    team = message_approval.mailing_list.team
    from_address = format_address(
        team.displayname, config.canonical.noreply_from_address)
    subject = (
        'New mailing list message requiring approval for %s'
        % team.displayname)
    template = get_email_template('new-held-message.txt', app='registry')

    # Most of the replacements are the same for everyone.
    replacements = {
        'subject': message_details.subject,
        'author_name': message_details.author.displayname,
        'author_url': canonical_url(message_details.author),
        'date': message_details.date,
        'message_id': message_details.message_id,
        'review_url': '%s/+mailinglist-moderate' % canonical_url(team),
        'team': team.displayname,
        }

    # Don't wrap the paragraph with the url.
    def wrap_function(paragraph):
        return (paragraph.startswith('http:') or
                paragraph.startswith('https:'))

    # Send one message to every team administrator.
    person_set = getUtility(IPersonSet)
    for address in team.getTeamAdminsEmailAddresses():
        user = person_set.getByEmail(address)
        replacements['user'] = user.displayname
        body = MailWrapper(72).format(
            template % replacements, force_wrap=True, wrap_func=wrap_function)
        simple_sendmail(from_address, address, subject, body)
Example #7
0
def notify_message_held(message_approval, event):
    """Send a notification of a message hold to all team administrators."""
    message_details = getAdapter(message_approval, IHeldMessageDetails)
    team = message_approval.mailing_list.team
    from_address = format_address(team.displayname,
                                  config.canonical.noreply_from_address)
    subject = ('New mailing list message requiring approval for %s' %
               team.displayname)
    template = get_email_template('new-held-message.txt', app='registry')

    # Most of the replacements are the same for everyone.
    replacements = {
        'subject': message_details.subject,
        'author_name': message_details.author.displayname,
        'author_url': canonical_url(message_details.author),
        'date': message_details.date,
        'message_id': message_details.message_id,
        'review_url': '%s/+mailinglist-moderate' % canonical_url(team),
        'team': team.displayname,
    }

    # Don't wrap the paragraph with the url.
    def wrap_function(paragraph):
        return (paragraph.startswith('http:')
                or paragraph.startswith('https:'))

    # Send one message to every team administrator.
    person_set = getUtility(IPersonSet)
    for address in team.getTeamAdminsEmailAddresses():
        user = person_set.getByEmail(address)
        replacements['user'] = user.displayname
        body = MailWrapper(72).format(template % replacements,
                                      force_wrap=True,
                                      wrap_func=wrap_function)
        simple_sendmail(from_address, address, subject, body)
Example #8
0
    def _sendFailureNotification(self, notify_owner, log):
        """Send a failure notification to the distribution's mirror admins and
        to the mirror owner, in case notify_owner is True.
        """
        template = get_email_template('notify-mirror-owner.txt',
                                      app='registry')
        fromaddress = format_address("Launchpad Mirror Prober",
                                     config.canonical.noreply_from_address)

        replacements = {
            'distro': self.distribution.title,
            'mirror_name': self.name,
            'mirror_url': canonical_url(self),
            'log_snippet': "\n".join(log.split('\n')[:20]),
            'logfile_url': self.last_probe_record.log_file.http_url
        }
        message = template % replacements
        subject = "Launchpad: Verification of %s failed" % self.name

        mirror_admin_address = get_contact_email_addresses(
            self.distribution.mirror_admin)
        simple_sendmail(fromaddress, mirror_admin_address, subject, message)

        if notify_owner:
            owner_address = get_contact_email_addresses(self.owner)
            if len(owner_address) > 0:
                simple_sendmail(fromaddress, owner_address, subject, message)
Example #9
0
    def forInvitationToJoinTeam(cls, member, team):
        """Create a mailer for notifying about team joining invitations.

        XXX: Guilherme Salgado 2007-05-08:
        At some point we may want to extend this functionality to allow
        invites to be sent to users as well, but for now we only use it for
        teams.
        """
        assert member.is_team
        membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
            member, team)
        assert membership is not None
        recipients = OrderedDict()
        for admin in member.adminmembers:
            for recipient in get_recipients(admin):
                recipients[recipient] = TeamMembershipRecipientReason.forAdmin(
                    admin, member, recipient)
        from_addr = format_address(team.displayname,
                                   config.canonical.noreply_from_address)
        subject = "Invitation for %s to join" % member.name
        return cls(subject,
                   "membership-invitation.txt",
                   recipients,
                   from_addr,
                   "team-membership-invitation",
                   member,
                   team,
                   membership.proposed_by,
                   membership=membership)
    def _sendFailureNotification(self, notify_owner, log):
        """Send a failure notification to the distribution's mirror admins and
        to the mirror owner, in case notify_owner is True.
        """
        template = get_email_template(
            'notify-mirror-owner.txt', app='registry')
        fromaddress = format_address(
            "Launchpad Mirror Prober", config.canonical.noreply_from_address)

        replacements = {
            'distro': self.distribution.title,
            'mirror_name': self.name,
            'mirror_url': canonical_url(self),
            'log_snippet': "\n".join(log.split('\n')[:20]),
            'logfile_url': self.last_probe_record.log_file.http_url}
        message = template % replacements
        subject = "Launchpad: Verification of %s failed" % self.name

        mirror_admin_address = get_contact_email_addresses(
            self.distribution.mirror_admin)
        simple_sendmail(fromaddress, mirror_admin_address, subject, message)

        if notify_owner:
            owner_address = get_contact_email_addresses(self.owner)
            if len(owner_address) > 0:
                simple_sendmail(fromaddress, owner_address, subject, message)
Example #11
0
 def _send_email(self, from_name, subject, message, headers=None):
     """Send an email to this token's email address."""
     from_address = format_address(
         from_name, config.canonical.noreply_from_address)
     to_address = str(self.email)
     simple_sendmail(
         from_address, to_address, subject, message,
         headers=headers, bulk=False)
 def to_addresses(self):
     """See `IPersonNotification`."""
     if self.person.is_team:
         return self.person.getTeamAdminsEmailAddresses()
     elif self.person.preferredemail is None:
         return []
     else:
         return [format_address(self.person.displayname, self.person.preferredemail.email)]
 def to_addresses(self):
     """See `IPersonNotification`."""
     if self.person.is_team:
         return self.person.getTeamAdminsEmailAddresses()
     elif self.person.preferredemail is None:
         return []
     else:
         return [format_address(
             self.person.displayname, self.person.preferredemail.email)]
Example #14
0
    def run(self):
        """See `BaseRunnableJob`.

         Subclasses that are updating products may make changes to the product
         before or after calling this class' run() method.
        """
        from_address = format_address(
            'Launchpad', config.canonical.noreply_from_address)
        self.sendEmailToMaintainer(
            self.email_template_name, self.subject, from_address)
Example #15
0
    def run(self):
        """See `BaseRunnableJob`.

         Subclasses that are updating products may make changes to the product
         before or after calling this class' run() method.
        """
        from_address = format_address(
            'Launchpad', config.canonical.noreply_from_address)
        self.sendEmailToMaintainer(
            self.email_template_name, self.subject, from_address)
Example #16
0
 def test_from_address(self):
     # The from_address is the question with the user displayname.
     question = self.factory.makeQuestion()
     user, subject, body, headers = make_user_subject_body_headers(
         self.factory)
     job = QuestionEmailJob.create(question, user,
                                   QuestionRecipientSet.SUBSCRIBER, subject,
                                   body, headers)
     address = format_address(
         user.displayname, "*****@*****.**" % question.id)
     self.assertEqual(address, job.from_address)
Example #17
0
 def sendAdvertisementEmail(self, subject, content):
     """See ISignedCodeOfConduct."""
     assert self.owner.preferredemail
     template = open('lib/lp/registry/emailtemplates/'
                     'signedcoc-acknowledge.txt').read()
     fromaddress = format_address("Launchpad Code Of Conduct System",
                                  config.canonical.noreply_from_address)
     replacements = {'user': self.owner.displayname, 'content': content}
     message = template % replacements
     simple_sendmail(fromaddress, str(self.owner.preferredemail.email),
                     subject, message)
 def test_from_address(self):
     # The from_address is the question with the user displayname.
     question = self.factory.makeQuestion()
     user, subject, body, headers = make_user_subject_body_headers(
         self.factory)
     job = QuestionEmailJob.create(
         question, user, QuestionRecipientSet.SUBSCRIBER,
         subject, body, headers)
     address = format_address(
         user.displayname,
         "*****@*****.**" % question.id)
     self.assertEqual(address, job.from_address)
Example #19
0
def send_bug_details_to_new_bug_subscribers(
    bug, previous_subscribers, current_subscribers, subscribed_by=None,
    event_creator=None):
    """Send an email containing full bug details to new bug subscribers.

    This function is designed to handle situations where bugtasks get
    reassigned to new products or sourcepackages, and the new bug subscribers
    need to be notified of the bug.

    A boolean is returned indicating whether any emails were sent.
    """
    prev_subs_set = set(previous_subscribers)
    cur_subs_set = set(current_subscribers)
    new_subs = cur_subs_set.difference(prev_subs_set)

    if (event_creator is not None
            and not event_creator.selfgenerated_bugnotifications):
        new_subs.discard(event_creator)

    to_addrs = set()
    for new_sub in new_subs:
        to_addrs.update(get_contact_email_addresses(new_sub))

    if not to_addrs:
        return False

    from_addr = format_address(
        'Launchpad Bug Tracker',
        "%s@%s" % (bug.id, config.launchpad.bugs_domain))
    # Now's a good a time as any for this email; don't use the original
    # reported date for the bug as it will just confuse mailer and
    # recipient.
    email_date = datetime.datetime.now()

    # The new subscriber email is effectively the initial message regarding
    # a new bug. The bug's initial message is used in the References
    # header to establish the message's context in the email client.
    references = [bug.initial_message.rfc822msgid]
    recipients = bug.getBugNotificationRecipients()

    bug_notification_builder = BugNotificationBuilder(bug, event_creator)
    for to_addr in sorted(to_addrs):
        reason, rationale = recipients.getReason(to_addr)
        subject, contents = generate_bug_add_email(
            bug, new_recipients=True, subscribed_by=subscribed_by,
            reason=reason, event_creator=event_creator)
        msg = bug_notification_builder.build(
            from_addr, to_addr, contents, subject, email_date,
            rationale=rationale, references=references)
        sendmail(msg)

    return True
 def sendAdvertisementEmail(self, subject, content):
     """See ISignedCodeOfConduct."""
     assert self.owner.preferredemail
     template = open('lib/lp/registry/emailtemplates/'
                     'signedcoc-acknowledge.txt').read()
     fromaddress = format_address(
         "Launchpad Code Of Conduct System",
         config.canonical.noreply_from_address)
     replacements = {'user': self.owner.displayname,
                     'content': content}
     message = template % replacements
     simple_sendmail(
         fromaddress, str(self.owner.preferredemail.email),
         subject, message)
Example #21
0
 def assertBuildMessageValid(self, build, message):
     # Not currently used; can be used if we do want to check about any
     # notifications sent in other cases.
     requester = build.requester
     requester_address = format_address(
         requester.displayname, requester.preferredemail.email)
     mailer = SourcePackageRecipeBuildMailer.forStatus(build)
     expected = mailer.generateEmail(
         requester.preferredemail.email, requester)
     self.assertEqual(
         requester_address, re.sub(r'\n\t+', ' ', message['To']))
     self.assertEqual(expected.subject, message['Subject'].replace(
         '\n\t', ' '))
     self.assertEqual(
         expected.body, message.get_payload(decode=True))
Example #22
0
 def test_forCreation(self):
     """Ensure that forCreation produces a mailer with expected values."""
     comment, subscriber = self.makeCommentAndSubscriber()
     mailer = CodeReviewCommentMailer.forCreation(comment)
     self.assertEqual(comment.message.subject, mailer._subject_template)
     bmp = comment.branch_merge_proposal
     # The branch owners are implicitly subscribed to their branches
     # when the branches are created.
     self.assertRecipientsMatches(
         [subscriber, bmp.source_branch.owner, bmp.target_branch.owner],
         mailer)
     self.assertEqual(comment.branch_merge_proposal, mailer.merge_proposal)
     sender = comment.message.owner
     sender_address = format_address(sender.displayname,
                                     sender.preferredemail.email)
     self.assertEqual(sender_address, mailer.from_address)
     self.assertEqual(comment, mailer.code_review_comment)
 def test_forCreation(self):
     """Ensure that forCreation produces a mailer with expected values."""
     comment, subscriber = self.makeCommentAndSubscriber()
     mailer = CodeReviewCommentMailer.forCreation(comment)
     self.assertEqual(comment.message.subject,
                      mailer._subject_template)
     bmp = comment.branch_merge_proposal
     # The branch owners are implicitly subscribed to their branches
     # when the branches are created.
     self.assertRecipientsMatches(
         [subscriber, bmp.source_branch.owner, bmp.target_branch.owner],
         mailer)
     self.assertEqual(
         comment.branch_merge_proposal, mailer.merge_proposal)
     sender = comment.message.owner
     sender_address = format_address(sender.displayname,
         sender.preferredemail.email)
     self.assertEqual(sender_address, mailer.from_address)
     self.assertEqual(comment, mailer.code_review_comment)
    def _handleUnpushedBranch(self, productseries):
        """Branch has never been scanned.  Notify owner.

        This means that as far as the Launchpad database knows, there is
        no actual bzr branch behind this `IBranch` yet.
        """
        branch = productseries.translations_branch
        self.logger.info("Notifying %s of unpushed branch %s." % (
            branch.owner.name, branch.bzr_identity))

        template = get_email_template('unpushed-branch.txt', 'translations')
        text = template % {
            'productseries': productseries.title,
            'branch_url': branch.bzr_identity,
        }
        recipients = get_contact_email_addresses(branch.owner)
        sender = format_address(
            "Launchpad Translations", config.canonical.noreply_from_address)
        subject = "Launchpad: translations branch has not been set up."
        self._sendMail(sender, recipients, subject, text)
    def _getToAddresses(self, recipient, email):
        """Provide to addresses as if this were a mailing list.

        CodeReviewComments which are not replies shall list the merge proposer
        as their to address.  CodeReviewComments which are replies shall list
        the parent comment's author as their to address.
        """
        if self.message.parent is None:
            to_person = self.merge_proposal.registrant
        else:
            to_person = self.message.parent.owner
        if to_person.hide_email_addresses:
            return [self.merge_proposal.address]
        # Ensure the to header matches the envelope-to address.
        if to_person == recipient:
            to_email = email
        else:
            to_email = to_person.preferredemail.email
        to = [format_address(to_person.displayname, to_email)]
        return to
def notify_invitation_to_join_team(event):
    """Notify team admins that the team has been invited to join another team.

    The notification will include a link to a page in which any team admin can
    accept the invitation.

    XXX: Guilherme Salgado 2007-05-08:
    At some point we may want to extend this functionality to allow invites
    to be sent to users as well, but for now we only use it for teams.
    """
    member = event.member
    assert member.is_team
    team = event.team
    membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
        member, team)
    assert membership is not None

    reviewer = membership.proposed_by
    admin_addrs = member.getTeamAdminsEmailAddresses()
    from_addr = format_address(team.displayname,
                               config.canonical.noreply_from_address)
    subject = 'Invitation for %s to join' % member.name
    templatename = 'membership-invitation.txt'
    template = get_email_template(templatename, app='registry')
    replacements = {
        'reviewer':
        '%s (%s)' % (reviewer.displayname, reviewer.name),
        'member':
        '%s (%s)' % (member.displayname, member.name),
        'team':
        '%s (%s)' % (team.displayname, team.name),
        'team_url':
        canonical_url(team),
        'membership_invitations_url':
        "%s/+invitation/%s" % (canonical_url(member), team.name)
    }
    for address in admin_addrs:
        recipient = getUtility(IPersonSet).getByEmail(address)
        replacements['recipient_name'] = recipient.displayname
        msg = MailWrapper().format(template % replacements, force_wrap=True)
        simple_sendmail(from_addr, address, subject, msg)
 def __init__(self, code_review_comment, recipients, message_id=None):
     """Constructor."""
     self.code_review_comment = code_review_comment
     self.message = code_review_comment.message
     from_person = self.message.owner
     from_address = format_address(from_person.displayname,
                                   from_person.preferredemail.email)
     merge_proposal = code_review_comment.branch_merge_proposal
     BMPMailer.__init__(self,
                        self.message.subject,
                        None,
                        recipients,
                        merge_proposal,
                        from_address,
                        message_id=message_id)
     self.attachments = []
     original_email = self.code_review_comment.getOriginalEmail()
     if original_email is not None:
         # The original_email here is wrapped in a zope security proxy,
         # which is not helpful as there is no interface defined for
         # emails, so strip it off here.
         original_email = removeSecurityProxy(original_email)
         # The attachments for the code review comment are actually
         # library file aliases.
         display_aliases, other_aliases = (
             self.code_review_comment.getAttachments())
         include_attachments = set()
         for alias in display_aliases:
             include_attachments.add((alias.filename, alias.mimetype))
         for part in original_email.walk():
             if part.is_multipart():
                 continue
             filename = part.get_filename() or 'unnamed'
             if part['content-type'] is None:
                 content_type = 'application/octet-stream'
             else:
                 content_type = part['content-type']
             if (filename, content_type) in include_attachments:
                 payload = part.get_payload(decode=True)
                 self.attachments.append((payload, filename, content_type))
     self._generateBodyBits()
def notify_mailinglist_activated(mailinglist, event):
    """Notification that a mailing list is available.

    All active members of a team and its subteams receive notification when
    the team's mailing list is available.
    """
    # We will use the setting of the date_activated field as a hint
    # that this list is new, and that noboby has subscribed yet.  See
    # `MailingList.transitionToStatus()` for the details.
    old_date = event.object_before_modification.date_activated
    new_date = event.object.date_activated
    list_looks_new = old_date is None and new_date is not None

    if not (list_looks_new and mailinglist.is_usable):
        return

    team = mailinglist.team
    from_address = format_address(
        team.displayname, config.canonical.noreply_from_address)
    headers = {}
    subject = "New Mailing List for %s" % team.displayname
    template = get_email_template('new-mailing-list.txt', app='registry')
    editemails_url = '%s/+editemails'

    for person in team.allmembers:
        if person.is_team or person.preferredemail is None:
            # This is either a team or a person without a preferred email, so
            # don't send a notification.
            continue
        to_address = [str(person.preferredemail.email)]
        replacements = {
            'user': person.displayname,
            'team_displayname': team.displayname,
            'team_name': team.name,
            'team_url': canonical_url(team),
            'subscribe_url': editemails_url % canonical_url(person),
            }
        body = MailWrapper(72).format(template % replacements,
                                      force_wrap=True)
        simple_sendmail(from_address, to_address, subject, body, headers)
Example #29
0
    def sendCancellationEmail(self, token):
        """Send an email to the person whose subscription was cancelled."""
        if token.archive.suppress_subscription_notifications:
            # Don't send an email if they should be suppresed for the
            # archive
            return
        send_to_person = token.person
        ppa_name = token.archive.displayname
        ppa_owner_url = canonical_url(token.archive.owner)
        subject = "PPA access cancelled for %s" % ppa_name
        template = get_email_template(
            "ppa-subscription-cancelled.txt", app='soyuz')

        assert not send_to_person.is_team, (
            "Token.person is a team, it should always be individuals.")

        if send_to_person.preferredemail is None:
            # The person has no preferred email set, so we don't
            # email them.
            return

        to_address = [send_to_person.preferredemail.email]
        replacements = {
            'recipient_name': send_to_person.displayname,
            'ppa_name': ppa_name,
            'ppa_owner_url': ppa_owner_url,
            }
        body = MailWrapper(72).format(
            template % replacements, force_wrap=True)

        from_address = format_address(
            ppa_name,
            config.canonical.noreply_from_address)

        headers = {
            'Sender': config.canonical.bounce_address,
            }

        simple_sendmail(from_address, to_address, subject, body, headers)
Example #30
0
def notify_mailinglist_activated(mailinglist, event):
    """Notification that a mailing list is available.

    All active members of a team and its subteams receive notification when
    the team's mailing list is available.
    """
    # We will use the setting of the date_activated field as a hint
    # that this list is new, and that noboby has subscribed yet.  See
    # `MailingList.transitionToStatus()` for the details.
    old_date = event.object_before_modification.date_activated
    new_date = event.object.date_activated
    list_looks_new = old_date is None and new_date is not None

    if not (list_looks_new and mailinglist.is_usable):
        return

    team = mailinglist.team
    from_address = format_address(team.displayname,
                                  config.canonical.noreply_from_address)
    headers = {}
    subject = "New Mailing List for %s" % team.displayname
    template = get_email_template('new-mailing-list.txt', app='registry')
    editemails_url = '%s/+editmailinglists'

    for person in team.allmembers:
        if person.is_team or person.preferredemail is None:
            # This is either a team or a person without a preferred email, so
            # don't send a notification.
            continue
        to_address = [str(person.preferredemail.email)]
        replacements = {
            'user': person.displayname,
            'team_displayname': team.displayname,
            'team_name': team.name,
            'team_url': canonical_url(team),
            'subscribe_url': editemails_url % canonical_url(person),
        }
        body = MailWrapper(72).format(template % replacements, force_wrap=True)
        simple_sendmail(from_address, to_address, subject, body, headers)
    def sendSelfRenewalNotification(self):
        """See `ITeamMembership`."""
        team = self.team
        member = self.person
        assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND

        from_addr = format_address(
            team.displayname, config.canonical.noreply_from_address)
        replacements = {'member_name': member.unique_displayname,
                        'team_name': team.unique_displayname,
                        'team_url': canonical_url(team),
                        'dateexpires': self.dateexpires.strftime('%Y-%m-%d')}
        subject = '%s extended their membership' % member.name
        template = get_email_template(
            'membership-member-renewed.txt', app='registry')
        admins_addrs = self.team.getTeamAdminsEmailAddresses()
        for address in admins_addrs:
            recipient = getUtility(IPersonSet).getByEmail(address)
            replacements['recipient_name'] = recipient.displayname
            msg = MailWrapper().format(
                template % replacements, force_wrap=True)
            simple_sendmail(from_addr, address, subject, msg)
Example #32
0
 def forSelfRenewal(cls, member, team, dateexpires):
     """Create a mailer for notifying about a self-renewal."""
     assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND
     template_name = "membership-member-renewed.txt"
     subject = "%s extended their membership" % member.name
     recipients = OrderedDict()
     for admin in team.adminmembers:
         for recipient in get_recipients(admin):
             recipients[recipient] = TeamMembershipRecipientReason.forAdmin(
                 admin, team, recipient)
     extra_params = {"dateexpires": dateexpires.strftime("%Y-%m-%d")}
     from_addr = format_address(team.displayname,
                                config.canonical.noreply_from_address)
     return cls(subject,
                template_name,
                recipients,
                from_addr,
                "team-membership-renewed",
                member,
                team,
                None,
                extra_params=extra_params)
Example #33
0
def new_import(code_import, event):
    """Email the vcs-imports team about a new code import."""
    if (event.user is None
        or IUnauthenticatedPrincipal.providedBy(event.user)):
        # If there is no logged in user, then we are most likely in a
        # test.
        return
    user = IPerson(event.user)
    subject = 'New code import: %s/%s' % (
        code_import.branch.target.name, code_import.branch.name)
    if code_import.rcs_type == RevisionControlSystems.CVS:
        location = '%s, %s' % (code_import.cvs_root, code_import.cvs_module)
    else:
        location = code_import.url
    rcs_type_map = {
        RevisionControlSystems.CVS: 'CVS',
        RevisionControlSystems.SVN: 'subversion',
        RevisionControlSystems.BZR_SVN: 'subversion',
        RevisionControlSystems.GIT: 'git',
        RevisionControlSystems.BZR: 'bazaar',
        }
    body = get_email_template('new-code-import.txt', app='code') % {
        'person': code_import.registrant.displayname,
        'branch': canonical_url(code_import.branch),
        'rcs_type': rcs_type_map[code_import.rcs_type],
        'location': location,
        }

    from_address = format_address(
        user.displayname, user.preferredemail.email)

    vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
    headers = {'X-Launchpad-Branch': code_import.branch.unique_name,
               'X-Launchpad-Message-Rationale':
                   'Operator @%s' % vcs_imports.name,
               'X-Launchpad-Notification-Type': 'code-import',
               }
    for address in get_contact_email_addresses(vcs_imports):
        simple_sendmail(from_address, address, subject, body, headers)
Example #34
0
    def forBranchModified(cls, branch, user, delta):
        """Construct a BranchMailer for mail about a branch modification.

        :param branch: The branch that was modified.
        :param user: The user making the change.
        :param delta: an IBranchDelta representing the modification.
        :return: a BranchMailer.
        """
        recipients = branch.getNotificationRecipients()
        interested_levels = (BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
                             BranchSubscriptionNotificationLevel.FULL)
        actual_recipients = {}
        # If the person editing the branch isn't in the team of the owner
        # then notify the branch owner of the changes as well.
        if not user.inTeam(branch.owner):
            # Existing rationales are kept.
            recipients.add(branch.owner, None, None)
        for recipient in recipients:
            subscription, rationale = recipients.getReason(recipient)
            if (subscription is not None and subscription.notification_level
                    not in interested_levels):
                continue
            if subscription is None:
                actual_recipients[recipient] = RecipientReason.forBranchOwner(
                    branch, recipient)
            else:
                actual_recipients[recipient] = \
                    RecipientReason.forBranchSubscriber(
                    subscription, recipient, rationale)
        from_address = format_address(
            user.displayname,
            removeSecurityProxy(user).preferredemail.email)
        return cls('[Branch %(unique_name)s]',
                   'branch-modified.txt',
                   actual_recipients,
                   from_address,
                   delta=delta,
                   notification_type='branch-updated')
Example #35
0
def new_import(code_import, event):
    """Email the vcs-imports team about a new code import."""
    if (event.user is None
            or IUnauthenticatedPrincipal.providedBy(event.user)):
        # If there is no logged in user, then we are most likely in a
        # test.
        return
    user = IPerson(event.user)
    subject = 'New code import: %s/%s' % (code_import.branch.target.name,
                                          code_import.branch.name)
    if code_import.rcs_type == RevisionControlSystems.CVS:
        location = '%s, %s' % (code_import.cvs_root, code_import.cvs_module)
    else:
        location = code_import.url
    rcs_type_map = {
        RevisionControlSystems.CVS: 'CVS',
        RevisionControlSystems.SVN: 'subversion',
        RevisionControlSystems.BZR_SVN: 'subversion',
        RevisionControlSystems.GIT: 'git',
        RevisionControlSystems.BZR: 'bazaar',
    }
    body = get_email_template('new-code-import.txt', app='code') % {
        'person': code_import.registrant.displayname,
        'branch': canonical_url(code_import.branch),
        'rcs_type': rcs_type_map[code_import.rcs_type],
        'location': location,
    }

    from_address = format_address(user.displayname, user.preferredemail.email)

    vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
    headers = {
        'X-Launchpad-Branch': code_import.branch.unique_name,
        'X-Launchpad-Message-Rationale': 'Operator @%s' % vcs_imports.name,
        'X-Launchpad-Notification-Type': 'code-import',
    }
    for address in get_contact_email_addresses(vcs_imports):
        simple_sendmail(from_address, address, subject, body, headers)
 def __init__(self, code_review_comment, recipients, message_id=None):
     """Constructor."""
     self.code_review_comment = code_review_comment
     self.message = code_review_comment.message
     from_person = self.message.owner
     from_address = format_address(
         from_person.displayname, from_person.preferredemail.email)
     merge_proposal = code_review_comment.branch_merge_proposal
     BMPMailer.__init__(
         self, self.message.subject, None, recipients, merge_proposal,
         from_address, message_id=message_id)
     self.attachments = []
     original_email = self.code_review_comment.getOriginalEmail()
     if original_email is not None:
         # The original_email here is wrapped in a zope security proxy,
         # which is not helpful as there is no interface defined for
         # emails, so strip it off here.
         original_email = removeSecurityProxy(original_email)
         # The attachments for the code review comment are actually
         # library file aliases.
         display_aliases, other_aliases = (
             self.code_review_comment.getAttachments())
         include_attachments = set()
         for alias in display_aliases:
             include_attachments.add((alias.filename, alias.mimetype))
         for part in original_email.walk():
             if part.is_multipart():
                 continue
             filename = part.get_filename() or 'unnamed'
             if part['content-type'] is None:
                 content_type = 'application/octet-stream'
             else:
                 content_type = part['content-type']
             if (filename, content_type) in include_attachments:
                 payload = part.get_payload(decode=True)
                 self.attachments.append(
                     (payload, filename, content_type))
     self._generateBodyBits()
Example #37
0
    def forBranchModified(cls, branch, user, delta):
        """Construct a BranchMailer for mail about a branch modification.

        :param branch: The branch that was modified.
        :param user: The user making the change.
        :param delta: an IBranchDelta representing the modification.
        :return: a BranchMailer.
        """
        recipients = branch.getNotificationRecipients()
        interested_levels = (
            BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
            BranchSubscriptionNotificationLevel.FULL)
        actual_recipients = {}
        # If the person editing the branch isn't in the team of the owner
        # then notify the branch owner of the changes as well.
        if not user.inTeam(branch.owner):
            # Existing rationales are kept.
            recipients.add(branch.owner, None, None)
        for recipient in recipients:
            subscription, rationale = recipients.getReason(recipient)
            if (subscription is not None and
                subscription.notification_level not in interested_levels):
                continue
            if subscription is None:
                actual_recipients[recipient] = RecipientReason.forBranchOwner(
                    branch, recipient)
            else:
                actual_recipients[recipient] = \
                    RecipientReason.forBranchSubscriber(
                    subscription, recipient, rationale)
        from_address = format_address(
            user.displayname, removeSecurityProxy(user).preferredemail.email)
        return cls(
            '[Branch %(unique_name)s]', 'branch-modified.txt',
            actual_recipients, from_address, delta=delta,
            notification_type='branch-updated')
def notify_invitation_to_join_team(event):
    """Notify team admins that the team has been invited to join another team.

    The notification will include a link to a page in which any team admin can
    accept the invitation.

    XXX: Guilherme Salgado 2007-05-08:
    At some point we may want to extend this functionality to allow invites
    to be sent to users as well, but for now we only use it for teams.
    """
    member = event.member
    assert member.is_team
    team = event.team
    membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
        member, team)
    assert membership is not None

    reviewer = membership.proposed_by
    admin_addrs = member.getTeamAdminsEmailAddresses()
    from_addr = format_address(
        team.displayname, config.canonical.noreply_from_address)
    subject = 'Invitation for %s to join' % member.name
    templatename = 'membership-invitation.txt'
    template = get_email_template(templatename, app='registry')
    replacements = {
        'reviewer': '%s (%s)' % (reviewer.displayname, reviewer.name),
        'member': '%s (%s)' % (member.displayname, member.name),
        'team': '%s (%s)' % (team.displayname, team.name),
        'team_url': canonical_url(team),
        'membership_invitations_url':
            "%s/+invitation/%s" % (canonical_url(member), team.name)}
    for address in admin_addrs:
        recipient = getUtility(IPersonSet).getByEmail(address)
        replacements['recipient_name'] = recipient.displayname
        msg = MailWrapper().format(template % replacements, force_wrap=True)
        simple_sendmail(from_addr, address, subject, msg)
Example #39
0
    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, AnnouncementStubPerson):
            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 sendExpirationWarningEmail(self):
        """See `ITeamMembership`."""
        if self.dateexpires is None:
            raise AssertionError(
                '%s in team %s has no membership expiration date.' %
                (self.person.name, self.team.name))
        if self.dateexpires < datetime.now(pytz.timezone('UTC')):
            # The membership has reached expiration. Silently return because
            # there is nothing to do. The member will have received emails
            # from previous calls by flag-expired-memberships.py
            return
        member = self.person
        team = self.team
        if member.is_team:
            recipient = member.teamowner
            templatename = 'membership-expiration-warning-bulk.txt'
            subject = '%s will expire soon from %s' % (member.name, team.name)
        else:
            recipient = member
            templatename = 'membership-expiration-warning-personal.txt'
            subject = 'Your membership in %s is about to expire' % team.name

        if team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND:
            how_to_renew = (
                "If you want, you can renew this membership at\n"
                "<%s/+expiringmembership/%s>"
                % (canonical_url(member), team.name))
        elif not self.canChangeExpirationDate(recipient):
            admins_names = []
            admins = team.getDirectAdministrators()
            assert admins.count() >= 1
            if admins.count() == 1:
                admin = admins[0]
                how_to_renew = (
                    "To prevent this membership from expiring, you should "
                    "contact the\nteam's administrator, %s.\n<%s>"
                    % (admin.unique_displayname, canonical_url(admin)))
            else:
                for admin in admins:
                    admins_names.append(
                        "%s <%s>" % (admin.unique_displayname,
                                        canonical_url(admin)))

                how_to_renew = (
                    "To prevent this membership from expiring, you should "
                    "get in touch\nwith one of the team's administrators:\n")
                how_to_renew += "\n".join(admins_names)
        else:
            how_to_renew = (
                "To stay a member of this team you should extend your "
                "membership at\n<%s/+member/%s>"
                % (canonical_url(team), member.name))

        to_addrs = get_contact_email_addresses(recipient)
        if len(to_addrs) == 0:
            # The user does not have a preferred email address, he was
            # probably suspended.
            return
        formatter = DurationFormatterAPI(
            self.dateexpires - datetime.now(pytz.timezone('UTC')))
        replacements = {
            'recipient_name': recipient.displayname,
            'member_name': member.unique_displayname,
            'team_url': canonical_url(team),
            'how_to_renew': how_to_renew,
            'team_name': team.unique_displayname,
            'expiration_date': self.dateexpires.strftime('%Y-%m-%d'),
            'approximate_duration': formatter.approximateduration()}

        msg = get_email_template(templatename, app='registry') % replacements
        from_addr = format_address(
            team.displayname, config.canonical.noreply_from_address)
        simple_sendmail(from_addr, to_addrs, subject, msg)
    def notify(self, extra_info=None):
        """See `IPackageBuild`.

        If config.buildmaster.build_notification is disable, simply
        return.

        If config.builddmaster.notify_owner is enabled and SPR.creator
        has preferredemail it will send an email to the creator, Bcc:
        to the config.builddmaster.default_recipient. If one of the
        conditions was not satisfied, no preferredemail found (autosync
        or untouched packages from debian) or config options disabled,
        it will only send email to the specified default recipient.

        This notification will contain useful information about
        the record in question (all states are supported), see
        doc/build-notification.txt for further information.
        """

        if not config.builddmaster.send_build_notification:
            return
        if self.status == BuildStatus.FULLYBUILT:
            return

        recipients = set()

        fromaddress = format_address(
            config.builddmaster.default_sender_name,
            config.builddmaster.default_sender_address)

        extra_headers = {
            'X-Launchpad-Build-State': self.status.name,
            'X-Launchpad-Build-Component': self.current_component.name,
            'X-Launchpad-Build-Arch':
                self.distro_arch_series.architecturetag,
            }

        # XXX cprov 2006-10-27: Temporary extra debug info about the
        # SPR.creator in context, to be used during the service quarantine,
        # notify_owner will be disabled to avoid *spamming* Debian people.
        creator = self.source_package_release.creator
        extra_headers['X-Creator-Recipient'] = ",".join(
            get_contact_email_addresses(creator))

        # Currently there are 7038 SPR published in edgy which the creators
        # have no preferredemail. They are the autosync ones (creator = katie,
        # 3583 packages) and the untouched sources since we have migrated from
        # DAK (the rest). We should not spam Debian maintainers.

        # Please note that both the package creator and the package uploader
        # will be notified of failures if:
        #     * the 'notify_owner' flag is set
        #     * the package build (failure) occurred in the original
        #       archive.
        package_was_not_copied = (
            self.archive == self.source_package_release.upload_archive)

        if package_was_not_copied and config.builddmaster.notify_owner:
            if (self.archive.is_ppa and creator.inTeam(self.archive.owner)
                or
                not self.archive.is_ppa):
                # If this is a PPA, the package creator should only be
                # notified if they are the PPA owner or in the PPA team.
                # (see bug 375757)
                # Non-PPA notifications inform the creator regardless.
                recipients = recipients.union(
                    get_contact_email_addresses(creator))
            dsc_key = self.source_package_release.dscsigningkey
            if dsc_key:
                recipients = recipients.union(
                    get_contact_email_addresses(dsc_key.owner))

        # Modify notification contents according to the targeted archive.
        # 'Archive Tag', 'Subject' and 'Source URL' are customized for PPA.
        # We only send build-notifications to 'buildd-admin' celebrity for
        # main archive candidates.
        # For PPA build notifications we include the archive.owner
        # contact_address.
        if not self.archive.is_ppa:
            buildd_admins = getUtility(ILaunchpadCelebrities).buildd_admin
            recipients = recipients.union(
                get_contact_email_addresses(buildd_admins))
            archive_tag = '%s primary archive' % self.distribution.name
            subject = "[Build #%d] %s" % (self.id, self.title)
            source_url = canonical_url(self.distributionsourcepackagerelease)
        else:
            recipients = recipients.union(
                get_contact_email_addresses(self.archive.owner))
            # For PPAs we run the risk of having no available contact_address,
            # for instance, when both, SPR.creator and Archive.owner have
            # not enabled it.
            if len(recipients) == 0:
                return
            archive_tag = '%s PPA' % get_ppa_reference(self.archive)
            subject = "[Build #%d] %s (%s)" % (
                self.id, self.title, archive_tag)
            source_url = 'not available'
            extra_headers['X-Launchpad-PPA'] = get_ppa_reference(self.archive)

        # XXX cprov 2006-08-02: pending security recipients for SECURITY
        # pocket build. We don't build SECURITY yet :(

        # XXX cprov 2006-08-02: find out a way to glue parameters reported
        # with the state in the build worflow, maybe by having an
        # IBuild.statusReport property, which could also be used in the
        # respective page template.
        if self.status in [
            BuildStatus.NEEDSBUILD, BuildStatus.SUPERSEDED]:
            # untouched builds
            buildduration = 'not available'
            buildlog_url = 'not available'
            builder_url = 'not available'
        elif self.status == BuildStatus.UPLOADING:
            buildduration = 'uploading'
            buildlog_url = 'see builder page'
            builder_url = 'not available'
        elif self.status == BuildStatus.BUILDING:
            # build in process
            buildduration = 'not finished'
            buildlog_url = 'see builder page'
            builder_url = canonical_url(self.buildqueue_record.builder)
        else:
            # completed states (success and failure)
            buildduration = DurationFormatterAPI(
                self.duration).approximateduration()
            buildlog_url = self.log_url
            builder_url = canonical_url(self.builder)

        if self.status == BuildStatus.FAILEDTOUPLOAD:
            assert extra_info is not None, (
                'Extra information is required for FAILEDTOUPLOAD '
                'notifications.')
            extra_info = 'Upload log:\n%s' % extra_info
        else:
            extra_info = ''

        template = get_email_template('build-notification.txt', app='soyuz')
        replacements = {
            'source_name': self.source_package_release.name,
            'source_version': self.source_package_release.version,
            'architecturetag': self.distro_arch_series.architecturetag,
            'build_state': self.status.title,
            'build_duration': buildduration,
            'buildlog_url': buildlog_url,
            'builder_url': builder_url,
            'build_title': self.title,
            'build_url': canonical_url(self),
            'source_url': source_url,
            'extra_info': extra_info,
            'archive_tag': archive_tag,
            'component_tag': self.current_component.name,
            }
        message = template % replacements

        for toaddress in recipients:
            simple_sendmail(
                fromaddress, toaddress, subject, message,
                headers=extra_headers)
Example #42
0
 def _getToAddresses(self, email, recipient):
     return [format_address(recipient.displayname, email)]
 def from_address(self):
     """See `IQuestionEmailJob`."""
     address = 'question%s@%s' % (
         self.question.id, config.answertracker.email_domain)
     return format_address(self.user.displayname, address)
Example #44
0
 def _format_user_address(user):
     naked_email = removeSecurityProxy(user).preferredemail.email
     return format_address(user.displayname, naked_email)
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'}

    # Include the 'X-Launchpad-PPA' header for PPA upload notfications
    # containing the PPA owner name.
    if archive.is_ppa:
        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)

    # `sendmail`, despite handling unicode message bodies, can't
    # cope with non-ascii sender/recipient addresses, so ascii_smash
    # is used on all addresses.

    # 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'] = ascii_smash(bcc_text)

    recipients = ascii_smash(", ".join(to_addrs))
    if isinstance(from_addr, unicode):
        # ascii_smash only works on unicode strings.
        from_addr = ascii_smash(from_addr)
    else:
        from_addr.encode('ascii')

    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)
Example #46
0
def code_import_updated(code_import, event, new_whiteboard, person):
    """Email the branch subscribers, and the vcs-imports team with new status.
    """
    branch = code_import.branch
    recipients = branch.getNotificationRecipients()
    # Add in the vcs-imports user.
    vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
    herder_rationale = 'Operator @%s' % vcs_imports.name
    recipients.add(vcs_imports, None, herder_rationale)

    headers = {'X-Launchpad-Branch': branch.unique_name}

    subject = 'Code import %s/%s status: %s' % (
        code_import.branch.target.name, branch.name,
        code_import.review_status.title)

    email_template = get_email_template(
        'code-import-status-updated.txt', app='code')
    template_params = {
        'body': make_email_body_for_code_import_update(
            code_import, event, new_whiteboard),
        'branch': canonical_url(code_import.branch)}

    if person:
        from_address = format_address(
            person.displayname, person.preferredemail.email)
    else:
        from_address = config.canonical.noreply_from_address

    interested_levels = (
        BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
        BranchSubscriptionNotificationLevel.FULL)

    for email_address in recipients.getEmails():
        subscription, rationale = recipients.getReason(email_address)

        if subscription is None:
            if rationale == herder_rationale:
                template_params['rationale'] = (
                    'You are getting this email because you are a member of'
                    ' the vcs-imports team.')
            else:
                template_params['rationale'] = rationale
            template_params['unsubscribe'] = ''
        else:
            if subscription.notification_level in interested_levels:
                template_params['rationale'] = (
                    'You are receiving this email as you are subscribed '
                    'to the branch.')
                if not subscription.person.is_team:
                    # Give the users a link to unsubscribe.
                    template_params['unsubscribe'] = (
                        "\nTo unsubscribe from this branch go to "
                        "%s/+edit-subscription." % canonical_url(branch))
                else:
                    template_params['unsubscribe'] = ''
            else:
                # Don't send email to this subscriber.
                continue

        headers['X-Launchpad-Message-Rationale'] = rationale
        body = email_template % template_params
        simple_sendmail(from_address, email_address, subject, body, headers)
def notify_team_join(event):
    """Notify team admins that someone has asked to join the team.

    If the team's policy is Moderated, the email will say that the membership
    is pending approval. Otherwise it'll say that the person has joined the
    team and who added that person to the team.
    """
    person = event.person
    team = event.team
    membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
        person, team)
    assert membership is not None
    approved, admin, proposed = [
        TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN,
        TeamMembershipStatus.PROPOSED]
    admin_addrs = team.getTeamAdminsEmailAddresses()
    from_addr = format_address(
        team.displayname, config.canonical.noreply_from_address)

    reviewer = membership.proposed_by
    if reviewer != person and membership.status in [approved, admin]:
        reviewer = membership.reviewed_by
        # Somebody added this person as a member, we better send a
        # notification to the person too.
        member_addrs = get_contact_email_addresses(person)

        headers = {}
        if person.is_team:
            templatename = 'new-member-notification-for-teams.txt'
            subject = '%s joined %s' % (person.name, team.name)
            header_rational = "Indirect member (%s)" % team.name
            footer_rationale = (
                "You received this email because "
                "%s is the new member." % person.name)
        else:
            templatename = 'new-member-notification.txt'
            subject = 'You have been added to %s' % team.name
            header_rational = "Member (%s)" % team.name
            footer_rationale = (
                "You received this email because you are the new member.")

        if team.mailing_list is not None:
            template = get_email_template(
                'team-list-subscribe-block.txt', app='registry')
            editemails_url = urlappend(
                canonical_url(getUtility(ILaunchpadRoot)),
                'people/+me/+editemails')
            list_instructions = template % dict(editemails_url=editemails_url)
        else:
            list_instructions = ''

        template = get_email_template(templatename, app='registry')
        replacements = {
            'reviewer': '%s (%s)' % (reviewer.displayname, reviewer.name),
            'team_url': canonical_url(team),
            'member': '%s (%s)' % (person.displayname, person.name),
            'team': '%s (%s)' % (team.displayname, team.name),
            'list_instructions': list_instructions,
            }
        headers = {'X-Launchpad-Message-Rationale': header_rational}
        for address in member_addrs:
            recipient = getUtility(IPersonSet).getByEmail(address)
            replacements['recipient_name'] = recipient.displayname
            send_team_email(
                from_addr, address, subject, template, replacements,
                footer_rationale, headers)

        # The member's email address may be in admin_addrs too; let's remove
        # it so the member don't get two notifications.
        admin_addrs = set(admin_addrs).difference(set(member_addrs))

    # Yes, we can have teams with no members; not even admins.
    if not admin_addrs:
        return

    # Open teams do not notify admins about new members.
    if team.membership_policy == TeamMembershipPolicy.OPEN:
        return

    replacements = {
        'person_name': "%s (%s)" % (person.displayname, person.name),
        'team_name': "%s (%s)" % (team.displayname, team.name),
        'reviewer_name': "%s (%s)" % (reviewer.displayname, reviewer.name),
        'url': canonical_url(membership)}

    headers = {}
    if membership.status in [approved, admin]:
        template = get_email_template(
            'new-member-notification-for-admins.txt', app='registry')
        subject = '%s joined %s' % (person.name, team.name)
    elif membership.status == proposed:
        # In the UI, a user can only propose himself or a team he
        # admins. Some users of the REST API have a workflow, where
        # they propose users that are designated as mentees (Bug 498181).
        if reviewer != person:
            headers = {"Reply-To": reviewer.preferredemail.email}
            template = get_email_template(
                'pending-membership-approval-for-third-party.txt',
                app='registry')
        else:
            headers = {"Reply-To": person.preferredemail.email}
            template = get_email_template(
                'pending-membership-approval.txt', app='registry')
        subject = "%s wants to join" % person.name
    else:
        raise AssertionError(
            "Unexpected membership status: %s" % membership.status)

    for address in admin_addrs:
        recipient = getUtility(IPersonSet).getByEmail(address)
        replacements['recipient_name'] = recipient.displayname
        if recipient.is_team:
            header_rationale = 'Admin (%s via %s)' % (
                team.name, recipient.name)
            footer_rationale = (
                "you are an admin of the %s team\n"
                "via the %s team." % (
                team.displayname, recipient.displayname))
        elif recipient == team.teamowner:
            header_rationale = 'Owner (%s)' % team.name
            footer_rationale = (
                "you are the owner of the %s team." % team.displayname)
        else:
            header_rationale = 'Admin (%s)' % team.name
            footer_rationale = (
                "you are an admin of the %s team." % team.displayname)
        footer = 'You received this email because %s' % footer_rationale
        headers['X-Launchpad-Message-Rationale'] = header_rationale
        send_team_email(
            from_addr, address, subject, template, replacements,
            footer, headers)
Example #48
0
    def forExpiringMembership(cls, member, team, dateexpires):
        """Create a mailer for warning about expiring membership."""
        membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
            member, team)
        assert membership is not None
        if member.is_team:
            target = member.teamowner
            template_name = "membership-expiration-warning-bulk.txt"
            subject = "%s will expire soon from %s" % (member.name, team.name)
        else:
            target = member
            template_name = "membership-expiration-warning-personal.txt"
            subject = "Your membership in %s is about to expire" % team.name

        if team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND:
            how_to_renew = ("If you want, you can renew this membership at\n"
                            "<%s/+expiringmembership/%s>" %
                            (canonical_url(member), team.name))
        elif not membership.canChangeExpirationDate(target):
            admins_names = []
            admins = team.getDirectAdministrators()
            assert admins.count() >= 1
            if admins.count() == 1:
                admin = admins[0]
                how_to_renew = (
                    "To prevent this membership from expiring, you should "
                    "contact the\nteam's administrator, %s.\n<%s>" %
                    (admin.unique_displayname, canonical_url(admin)))
            else:
                for admin in admins:
                    admins_names.append(
                        "%s <%s>" %
                        (admin.unique_displayname, canonical_url(admin)))

                how_to_renew = (
                    "To prevent this membership from expiring, you should "
                    "get in touch\nwith one of the team's administrators:\n")
                how_to_renew += "\n".join(admins_names)
        else:
            how_to_renew = (
                "To stay a member of this team you should extend your "
                "membership at\n<%s/+member/%s>" %
                (canonical_url(team), member.name))

        recipients = OrderedDict()
        for recipient in get_recipients(target):
            recipients[recipient] = TeamMembershipRecipientReason.forMember(
                member, team, recipient)

        formatter = DurationFormatterAPI(dateexpires - datetime.now(pytz.UTC))
        extra_params = {
            "how_to_renew": how_to_renew,
            "expiration_date": dateexpires.strftime("%Y-%m-%d"),
            "approximate_duration": formatter.approximateduration(),
        }

        from_addr = format_address(team.displayname,
                                   config.canonical.noreply_from_address)
        return cls(subject,
                   template_name,
                   recipients,
                   from_addr,
                   "team-membership-expiration-warning",
                   member,
                   team,
                   membership.proposed_by,
                   membership=membership,
                   extra_params=extra_params,
                   wrap=False,
                   force_wrap=False)
Example #49
0
    def forMembershipStatusChange(cls, member, team, reviewer, old_status,
                                  new_status, last_change_comment):
        """Create a mailer for a membership status change."""
        template_name = 'membership-statuschange'
        notification_type = 'team-membership-change'
        subject = ('Membership change: %(member)s in %(team)s' % {
            'member': member.name,
            'team': team.name
        })
        if new_status == TeamMembershipStatus.EXPIRED:
            template_name = 'membership-expired'
            notification_type = 'team-membership-expired'
            subject = '%s expired from team' % member.name
        elif (new_status == TeamMembershipStatus.APPROVED
              and old_status != TeamMembershipStatus.ADMIN):
            if old_status == TeamMembershipStatus.INVITED:
                template_name = 'membership-invitation-accepted'
                notification_type = 'team-membership-invitation-accepted'
                subject = ('Invitation to %s accepted by %s' %
                           (member.name, reviewer.name))
            elif old_status == TeamMembershipStatus.PROPOSED:
                subject = '%s approved by %s' % (member.name, reviewer.name)
            else:
                subject = '%s added by %s' % (member.name, reviewer.name)
        elif new_status == TeamMembershipStatus.INVITATION_DECLINED:
            template_name = 'membership-invitation-declined'
            notification_type = 'team-membership-invitation-declined'
            subject = ('Invitation to %s declined by %s' %
                       (member.name, reviewer.name))
        elif new_status == TeamMembershipStatus.DEACTIVATED:
            subject = '%s deactivated by %s' % (member.name, reviewer.name)
        elif new_status == TeamMembershipStatus.ADMIN:
            subject = '%s made admin by %s' % (member.name, reviewer.name)
        elif new_status == TeamMembershipStatus.DECLINED:
            subject = '%s declined by %s' % (member.name, reviewer.name)
        else:
            # Use the default template and subject.
            pass
        template_name += "-%(recipient_class)s.txt"

        if last_change_comment:
            comment = "\n%s said:\n %s\n" % (reviewer.displayname,
                                             last_change_comment.strip())
        else:
            comment = ""

        recipients = OrderedDict()
        if reviewer != member:
            for recipient in get_recipients(member):
                if member.is_team:
                    recipient_class = "bulk"
                else:
                    recipient_class = "personal"
                recipients[recipient] = (
                    TeamMembershipRecipientReason.forMember(
                        member,
                        team,
                        recipient,
                        recipient_class=recipient_class))
        # Don't send admin notifications for open teams: they're
        # unrestricted, so notifications on join/leave do not help the
        # admins.
        if team.membership_policy != TeamMembershipPolicy.OPEN:
            for admin in team.adminmembers:
                for recipient in get_recipients(admin):
                    # The new member may also be a team admin; don't send
                    # two notifications in that case.
                    if recipient not in recipients:
                        recipients[recipient] = (
                            TeamMembershipRecipientReason.forAdmin(
                                admin, team, recipient,
                                recipient_class="bulk"))

        extra_params = {
            "old_status": old_status,
            "new_status": new_status,
            "comment": comment,
        }
        from_addr = format_address(team.displayname,
                                   config.canonical.noreply_from_address)
        return cls(subject,
                   template_name,
                   recipients,
                   from_addr,
                   notification_type,
                   member,
                   team,
                   reviewer,
                   extra_params=extra_params)
Example #50
0
 def forTeamJoin(cls, member, team):
     """Create a mailer for notifying about a new member joining a team."""
     membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
         member, team)
     assert membership is not None
     subject = None
     template_name = None
     notification_type = "team-membership-new"
     recipients = OrderedDict()
     reviewer = membership.proposed_by
     if reviewer != member and membership.status in [
             TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN
     ]:
         reviewer = membership.reviewed_by
         # Somebody added this person as a member, we better send a
         # notification to the person too.
         if member.is_team:
             template_name = "new-member-notification-for-teams.txt"
             subject = "%s joined %s" % (member.name, team.name)
         else:
             template_name = "new-member-notification.txt"
             subject = "You have been added to %s" % team.name
         for recipient in get_recipients(member):
             recipients[recipient] = (
                 TeamMembershipRecipientReason.forNewMember(
                     member,
                     team,
                     recipient,
                     subject=subject,
                     template_name=template_name))
     # Open teams do not notify admins about new members.
     if team.membership_policy != TeamMembershipPolicy.OPEN:
         reply_to = None
         if membership.status in [
                 TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN
         ]:
             template_name = "new-member-notification-for-admins.txt"
             subject = "%s joined %s" % (member.name, team.name)
         elif membership.status == TeamMembershipStatus.PROPOSED:
             # In the UI, a user can only propose themselves or a team
             # they admin.  Some users of the REST API have a workflow
             # where they propose users that are designated as undergoing
             # mentorship (Bug 498181).
             if reviewer != member:
                 reply_to = reviewer.preferredemail.email
                 template_name = (
                     "pending-membership-approval-for-third-party.txt")
             else:
                 reply_to = member.preferredemail.email
                 template_name = "pending-membership-approval.txt"
             notification_type = "team-membership-pending"
             subject = "%s wants to join" % member.name
         else:
             raise AssertionError("Unexpected membership status: %s" %
                                  membership.status)
         for admin in team.adminmembers:
             for recipient in get_recipients(admin):
                 # The new member may also be a team admin; don't send
                 # two notifications in that case.
                 if recipient not in recipients:
                     if recipient == team.teamowner:
                         reason_factory = (
                             TeamMembershipRecipientReason.forOwner)
                     else:
                         reason_factory = (
                             TeamMembershipRecipientReason.forAdmin)
                     recipients[recipient] = reason_factory(
                         admin,
                         team,
                         recipient,
                         subject=subject,
                         template_name=template_name,
                         reply_to=reply_to)
     from_addr = format_address(team.displayname,
                                config.canonical.noreply_from_address)
     return cls(subject,
                template_name,
                recipients,
                from_addr,
                notification_type,
                member,
                team,
                membership.proposed_by,
                membership=membership)
    def run(self):
        """See `IMembershipNotificationJob`."""
        from lp.services.scripts import log
        from_addr = format_address(
            self.team.displayname, config.canonical.noreply_from_address)
        admin_emails = self.team.getTeamAdminsEmailAddresses()
        # person might be a self.team, so we can't rely on its preferredemail.
        self.member_email = get_contact_email_addresses(self.member)
        # Make sure we don't send the same notification twice to anybody.
        for email in self.member_email:
            if email in admin_emails:
                admin_emails.remove(email)

        if self.reviewer != self.member:
            self.reviewer_name = self.reviewer.unique_displayname
        else:
            self.reviewer_name = 'the user'

        if self.last_change_comment:
            comment = ("\n%s said:\n %s\n" % (
                self.reviewer.displayname, self.last_change_comment.strip()))
        else:
            comment = ""

        replacements = {
            'member_name': self.member.unique_displayname,
            'recipient_name': self.member.displayname,
            'team_name': self.team.unique_displayname,
            'team_url': canonical_url(self.team),
            'old_status': self.old_status.title,
            'new_status': self.new_status.title,
            'reviewer_name': self.reviewer_name,
            'comment': comment}

        template_name = 'membership-statuschange'
        subject = (
            'Membership change: %(member)s in %(team)s'
            % {
                'member': self.member.name,
                'team': self.team.name,
              })
        if self.new_status == TeamMembershipStatus.EXPIRED:
            template_name = 'membership-expired'
            subject = '%s expired from team' % self.member.name
        elif (self.new_status == TeamMembershipStatus.APPROVED and
            self.old_status != TeamMembershipStatus.ADMIN):
            if self.old_status == TeamMembershipStatus.INVITED:
                subject = ('Invitation to %s accepted by %s'
                        % (self.member.name, self.reviewer.name))
                template_name = 'membership-invitation-accepted'
            elif self.old_status == TeamMembershipStatus.PROPOSED:
                subject = '%s approved by %s' % (
                    self.member.name, self.reviewer.name)
            else:
                subject = '%s added by %s' % (
                    self.member.name, self.reviewer.name)
        elif self.new_status == TeamMembershipStatus.INVITATION_DECLINED:
            subject = ('Invitation to %s declined by %s'
                    % (self.member.name, self.reviewer.name))
            template_name = 'membership-invitation-declined'
        elif self.new_status == TeamMembershipStatus.DEACTIVATED:
            subject = '%s deactivated by %s' % (
                self.member.name, self.reviewer.name)
        elif self.new_status == TeamMembershipStatus.ADMIN:
            subject = '%s made admin by %s' % (
                self.member.name, self.reviewer.name)
        elif self.new_status == TeamMembershipStatus.DECLINED:
            subject = '%s declined by %s' % (
                self.member.name, self.reviewer.name)
        else:
            # Use the default template and subject.
            pass

        # Must have someone to mail, and be a non-open team (because open
        # teams are unrestricted, notifications on join/ leave do not help the
        # admins.
        if (len(admin_emails) != 0 and
            self.team.membership_policy != TeamMembershipPolicy.OPEN):
            admin_template = get_email_template(
                "%s-bulk.txt" % template_name, app='registry')
            for address in admin_emails:
                recipient = getUtility(IPersonSet).getByEmail(address)
                replacements['recipient_name'] = recipient.displayname
                msg = MailWrapper().format(
                    admin_template % replacements, force_wrap=True)
                simple_sendmail(from_addr, address, subject, msg)

        # The self.member can be a self.self.team without any
        # self.members, and in this case we won't have a single email
        # address to send this notification to.
        if self.member_email and self.reviewer != self.member:
            if self.member.is_team:
                template = '%s-bulk.txt' % template_name
            else:
                template = '%s-personal.txt' % template_name
            self.member_template = get_email_template(
                template, app='registry')
            for address in self.member_email:
                recipient = getUtility(IPersonSet).getByEmail(address)
                replacements['recipient_name'] = recipient.displayname
                msg = MailWrapper().format(
                    self.member_template % replacements, force_wrap=True)
                simple_sendmail(from_addr, address, subject, msg)
        log.debug('MembershipNotificationJob sent email')
Example #52
0
 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)