class TestBugNotificationBuilder(TestCaseWithFactory):
    """Test emails sent when subscribed by someone else."""

    layer = ZopelessDatabaseLayer

    def setUp(self):
        # Run the tests as a logged-in user.
        super(TestBugNotificationBuilder, self).setUp(user="******")
        self.bug = self.factory.makeBug()
        self.builder = BugNotificationBuilder(self.bug)

    def test_build_filters_empty(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build("from", "to", "body", "subject", utc_now, filters=[])
        self.assertIs(None, message.get("X-Launchpad-Subscription", None))

    def test_build_filters_single(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build("from", "to", "body", "subject", utc_now, filters=[u"Testing filter"])
        self.assertContentEqual([u"Testing filter"], message.get_all("X-Launchpad-Subscription"))

    def test_build_filters_multiple(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build(
            "from", "to", "body", "subject", utc_now, filters=[u"Testing filter", u"Second testing filter"]
        )
        self.assertContentEqual(
            [u"Testing filter", u"Second testing filter"], message.get_all("X-Launchpad-Subscription")
        )
Example #2
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
class TestBugNotificationBuilder(TestCaseWithFactory):
    """Test emails sent when subscribed by someone else."""

    layer = ZopelessDatabaseLayer

    def setUp(self):
        # Run the tests as a logged-in user.
        super(TestBugNotificationBuilder, self).setUp(
            user='******')
        self.bug = self.factory.makeBug()
        self.builder = BugNotificationBuilder(self.bug)

    def test_build_filters_empty(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build('from', 'to', 'body', 'subject',
                                     utc_now, filters=[])
        self.assertIs(None,
                      message.get("X-Launchpad-Subscription", None))

    def test_build_filters_single(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build('from', 'to', 'body', 'subject',
                                     utc_now, filters=[u"Testing filter"])
        self.assertContentEqual(
            [u"Testing filter"],
            message.get_all("X-Launchpad-Subscription"))

    def test_build_filters_multiple(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build(
            'from', 'to', 'body', 'subject', utc_now,
            filters=[u"Testing filter", u"Second testing filter"])
        self.assertContentEqual(
            [u"Testing filter", u"Second testing filter"],
            message.get_all("X-Launchpad-Subscription"))
 def setUp(self):
     # Run the tests as a logged-in user.
     super(TestBugNotificationBuilder, self).setUp(user="******")
     self.bug = self.factory.makeBug()
     self.builder = BugNotificationBuilder(self.bug)
 def setUp(self):
     # Run the tests as a logged-in user.
     super(TestBugNotificationBuilder, self).setUp(
         user='******')
     self.bug = self.factory.makeBug()
     self.builder = BugNotificationBuilder(self.bug)
def construct_email_notifications(bug_notifications):
    """Construct an email from a list of related bug notifications.

    The person and bug has to be the same for all notifications, and
    there can be only one comment.
    """
    first_notification = bug_notifications[0]
    bug = first_notification.bug
    actor = first_notification.message.owner
    subject = first_notification.message.subject

    comment = None
    references = []
    text_notifications = []
    old_values = {}
    new_values = {}

    for notification in bug_notifications:
        assert notification.bug == bug, bug.id
        assert notification.message.owner == actor, actor.id
        if notification.is_comment:
            assert comment is None, (
                "Only one of the notifications is allowed to be a comment.")
            comment = notification.message
        else:
            key = get_activity_key(notification)
            if key is not None:
                if key not in old_values:
                    old_values[key] = notification.activity.oldvalue
                new_values[key] = notification.activity.newvalue

    recipients = {}
    filtered_notifications = []
    omitted_notifications = []
    for notification in bug_notifications:
        key = get_activity_key(notification)
        if (notification.is_comment or key is None
                or key == 'removed_subscriber'
                or old_values[key] != new_values[key]):
            # We will report this notification.
            filtered_notifications.append(notification)
            for subscription_source in notification.recipients:
                for recipient in get_recipients(subscription_source.person):
                    # The subscription_source.person may be a person or a
                    # team.  The get_recipients function gives us everyone
                    # who should actually get an email for that person.
                    # If subscription_source.person is a person or a team
                    # with a preferred email address, then the people to
                    # be emailed will only be subscription_source.person.
                    # However, if it is a team without a preferred email
                    # address, then this list will be the people and teams
                    # that comprise the team, transitively, stopping the walk
                    # at each person and at each team with a preferred email
                    # address.
                    sources_for_person = recipients.get(recipient)
                    if sources_for_person is None:
                        sources_for_person = []
                        recipients[recipient] = sources_for_person
                    sources_for_person.append(subscription_source)
        else:
            omitted_notifications.append(notification)

    # If the actor does not want self-generated bug notifications, remove the
    # actor now.
    if not actor.selfgenerated_bugnotifications:
        recipients.pop(actor, None)

    if bug.duplicateof is not None:
        text_notifications.append(
            '*** This bug is a duplicate of bug %d ***\n    %s' %
            (bug.duplicateof.id, canonical_url(bug.duplicateof)))

    if comment is not None:
        if comment == bug.initial_message:
            subject, text = generate_bug_add_email(bug)
        else:
            text = comment.text_contents
        text_notifications.append(text)

        msgid = comment.rfc822msgid
        email_date = comment.datecreated

        reference = comment.parent
        while reference is not None:
            references.insert(0, reference.rfc822msgid)
            reference = reference.parent
    else:
        msgid = first_notification.message.rfc822msgid
        email_date = first_notification.message.datecreated

    for notification in filtered_notifications:
        if notification.message == comment:
            # Comments were just handled in the previous if block.
            continue
        text = notification.message.text_contents.rstrip()
        text_notifications.append(text)

    if bug.initial_message.rfc822msgid not in references:
        # Ensure that references contain the initial message ID
        references.insert(0, bug.initial_message.rfc822msgid)

    # At this point we've got the data we need to construct the
    # messages. Now go ahead and actually do that.
    messages = []
    mail_wrapper = MailWrapper(width=72)
    content = '\n\n'.join(text_notifications)
    from_address = get_bugmail_from_address(actor, bug)
    bug_notification_builder = BugNotificationBuilder(bug, actor)
    recipients = getUtility(IBugNotificationSet).getRecipientFilterData(
        bug, recipients, filtered_notifications)
    sorted_recipients = sorted(recipients.items(),
                               key=lambda t: t[0].preferredemail.email)

    for email_person, data in sorted_recipients:
        address = str(email_person.preferredemail.email)
        # Choosing the first source is a bit arbitrary, but it
        # is simple for the user to understand.  We may want to reconsider
        # this in the future.
        reason = data['sources'][0].reason_body
        rationale = data['sources'][0].reason_header

        if data['filter descriptions']:
            # There are some filter descriptions as well. Add them to
            # the email body.
            filters_text = u"\nMatching subscriptions: %s" % ", ".join(
                data['filter descriptions'])
        else:
            filters_text = u""

        # In the rare case of a bug with no bugtasks, we can't generate the
        # subscription management URL so just leave off the subscription
        # management message entirely.
        if len(bug.bugtasks):
            bug_url = canonical_url(bug.bugtasks[0])
            notification_url = bug_url + '/+subscriptions'
            subscriptions_message = (
                "To manage notifications about this bug go to:\n%s" %
                notification_url)
        else:
            subscriptions_message = ''

        data_wrapper = MailWrapper(width=72, indent='  ')
        body_data = {
            'content': mail_wrapper.format(content),
            'bug_title': data_wrapper.format(bug.title),
            'bug_url': canonical_url(bug),
            'notification_rationale': mail_wrapper.format(reason),
            'subscription_filters': filters_text,
            'subscriptions_message': subscriptions_message,
        }

        # If the person we're sending to receives verbose notifications
        # we include the description and status of the bug in the email
        # footer.
        if email_person.verbose_bugnotifications:
            email_template = 'bug-notification-verbose.txt'
            body_data['bug_description'] = data_wrapper.format(bug.description)

            status_base = "Status in %s:\n  %s"
            status_strings = []
            for bug_task in bug.bugtasks:
                status_strings.append(
                    status_base %
                    (bug_task.target.title, bug_task.status.title))

            body_data['bug_statuses'] = "\n".join(status_strings)
        else:
            email_template = 'bug-notification.txt'

        body_template = get_email_template(email_template, 'bugs')
        body = (body_template % body_data).strip()
        msg = bug_notification_builder.build(
            from_address,
            address,
            body,
            subject,
            email_date,
            rationale,
            references,
            msgid,
            filters=data['filter descriptions'])
        messages.append(msg)

    return filtered_notifications, omitted_notifications, messages
def construct_email_notifications(bug_notifications):
    """Construct an email from a list of related bug notifications.

    The person and bug has to be the same for all notifications, and
    there can be only one comment.
    """
    first_notification = bug_notifications[0]
    bug = first_notification.bug
    actor = first_notification.message.owner
    subject = first_notification.message.subject

    comment = None
    references = []
    text_notifications = []
    old_values = {}
    new_values = {}

    for notification in bug_notifications:
        assert notification.bug == bug, bug.id
        assert notification.message.owner == actor, actor.id
        if notification.is_comment:
            assert comment is None, "Only one of the notifications is allowed to be a comment."
            comment = notification.message
        else:
            key = get_activity_key(notification)
            if key is not None:
                if key not in old_values:
                    old_values[key] = notification.activity.oldvalue
                new_values[key] = notification.activity.newvalue

    recipients = {}
    filtered_notifications = []
    omitted_notifications = []
    for notification in bug_notifications:
        key = get_activity_key(notification)
        if notification.is_comment or key is None or key == "removed_subscriber" or old_values[key] != new_values[key]:
            # We will report this notification.
            filtered_notifications.append(notification)
            for subscription_source in notification.recipients:
                for recipient in get_recipients(subscription_source.person):
                    # The subscription_source.person may be a person or a
                    # team.  The get_recipients function gives us everyone
                    # who should actually get an email for that person.
                    # If subscription_source.person is a person or a team
                    # with a preferred email address, then the people to
                    # be emailed will only be subscription_source.person.
                    # However, if it is a team without a preferred email
                    # address, then this list will be the people and teams
                    # that comprise the team, transitively, stopping the walk
                    # at each person and at each team with a preferred email
                    # address.
                    sources_for_person = recipients.get(recipient)
                    if sources_for_person is None:
                        sources_for_person = []
                        recipients[recipient] = sources_for_person
                    sources_for_person.append(subscription_source)
        else:
            omitted_notifications.append(notification)

    # If the actor does not want self-generated bug notifications, remove the
    # actor now.
    if not actor.selfgenerated_bugnotifications:
        recipients.pop(actor, None)

    if bug.duplicateof is not None:
        text_notifications.append(
            "*** This bug is a duplicate of bug %d ***\n    %s" % (bug.duplicateof.id, canonical_url(bug.duplicateof))
        )

    if comment is not None:
        if comment == bug.initial_message:
            subject, text = generate_bug_add_email(bug)
        else:
            text = comment.text_contents
        text_notifications.append(text)

        msgid = comment.rfc822msgid
        email_date = comment.datecreated

        reference = comment.parent
        while reference is not None:
            references.insert(0, reference.rfc822msgid)
            reference = reference.parent
    else:
        msgid = first_notification.message.rfc822msgid
        email_date = first_notification.message.datecreated

    for notification in filtered_notifications:
        if notification.message == comment:
            # Comments were just handled in the previous if block.
            continue
        text = notification.message.text_contents.rstrip()
        text_notifications.append(text)

    if bug.initial_message.rfc822msgid not in references:
        # Ensure that references contain the initial message ID
        references.insert(0, bug.initial_message.rfc822msgid)

    # At this point we've got the data we need to construct the
    # messages. Now go ahead and actually do that.
    messages = []
    mail_wrapper = MailWrapper(width=72)
    content = "\n\n".join(text_notifications)
    from_address = get_bugmail_from_address(actor, bug)
    bug_notification_builder = BugNotificationBuilder(bug, actor)
    recipients = getUtility(IBugNotificationSet).getRecipientFilterData(bug, recipients, filtered_notifications)
    sorted_recipients = sorted(recipients.items(), key=lambda t: t[0].preferredemail.email)

    for email_person, data in sorted_recipients:
        address = str(email_person.preferredemail.email)
        # Choosing the first source is a bit arbitrary, but it
        # is simple for the user to understand.  We may want to reconsider
        # this in the future.
        reason = data["sources"][0].reason_body
        rationale = data["sources"][0].reason_header

        if data["filter descriptions"]:
            # There are some filter descriptions as well. Add them to
            # the email body.
            filters_text = u"\nMatching subscriptions: %s" % ", ".join(data["filter descriptions"])
        else:
            filters_text = u""

        # In the rare case of a bug with no bugtasks, we can't generate the
        # subscription management URL so just leave off the subscription
        # management message entirely.
        if len(bug.bugtasks):
            bug_url = canonical_url(bug.bugtasks[0])
            notification_url = bug_url + "/+subscriptions"
            subscriptions_message = "To manage notifications about this bug go to:\n%s" % notification_url
        else:
            subscriptions_message = ""

        data_wrapper = MailWrapper(width=72, indent="  ")
        body_data = {
            "content": mail_wrapper.format(content),
            "bug_title": data_wrapper.format(bug.title),
            "bug_url": canonical_url(bug),
            "notification_rationale": mail_wrapper.format(reason),
            "subscription_filters": filters_text,
            "subscriptions_message": subscriptions_message,
        }

        # If the person we're sending to receives verbose notifications
        # we include the description and status of the bug in the email
        # footer.
        if email_person.verbose_bugnotifications:
            email_template = "bug-notification-verbose.txt"
            body_data["bug_description"] = data_wrapper.format(bug.description)

            status_base = "Status in %s:\n  %s"
            status_strings = []
            for bug_task in bug.bugtasks:
                status_strings.append(status_base % (bug_task.target.title, bug_task.status.title))

            body_data["bug_statuses"] = "\n".join(status_strings)
        else:
            email_template = "bug-notification.txt"

        body_template = get_email_template(email_template, "bugs")
        body = (body_template % body_data).strip()
        msg = bug_notification_builder.build(
            from_address,
            address,
            body,
            subject,
            email_date,
            rationale,
            references,
            msgid,
            filters=data["filter descriptions"],
        )
        messages.append(msg)

    return filtered_notifications, omitted_notifications, messages
Example #8
0
class TestBugNotificationBuilder(TestCaseWithFactory):
    """Test emails sent when subscribed by someone else."""

    layer = DatabaseFunctionalLayer

    def setUp(self):
        # Run the tests as a logged-in user.
        super(TestBugNotificationBuilder,
              self).setUp(user='******')
        self.bug = self.factory.makeBug()
        self.builder = BugNotificationBuilder(self.bug)

    def test_build_filters_empty(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build('from',
                                     self.bug.owner,
                                     'body',
                                     'subject',
                                     utc_now,
                                     filters=[])
        self.assertIs(None, message.get("X-Launchpad-Subscription", None))

    def test_build_filters_single(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build('from',
                                     self.bug.owner,
                                     'body',
                                     'subject',
                                     utc_now,
                                     filters=[u"Testing filter"])
        self.assertContentEqual([u"Testing filter"],
                                message.get_all("X-Launchpad-Subscription"))

    def test_build_filters_multiple(self):
        """Filters are added."""
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build(
            'from',
            self.bug.owner,
            'body',
            'subject',
            utc_now,
            filters=[u"Testing filter", u"Second testing filter"])
        self.assertContentEqual([u"Testing filter", u"Second testing filter"],
                                message.get_all("X-Launchpad-Subscription"))

    def test_mails_contain_notification_type_header(self):
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build('from',
                                     self.bug.owner,
                                     'body',
                                     'subject',
                                     utc_now,
                                     filters=[])
        self.assertEqual("bug",
                         message.get("X-Launchpad-Notification-Type", None))

    def test_mails_no_expanded_footer(self):
        # Recipients without expanded_notification_footers do not receive an
        # expanded footer on messages.
        utc_now = datetime.now(pytz.UTC)
        message = self.builder.build('from',
                                     self.bug.owner,
                                     'body',
                                     'subject',
                                     utc_now,
                                     filters=[])
        self.assertNotIn("Launchpad-Notification-Type",
                         message.get_payload(decode=True))

    def test_mails_append_expanded_footer(self):
        # Recipients with expanded_notification_footers receive an expanded
        # footer on messages.
        utc_now = datetime.now(pytz.UTC)
        with person_logged_in(self.bug.owner):
            self.bug.owner.expanded_notification_footers = True
        message = self.builder.build('from',
                                     self.bug.owner,
                                     'body',
                                     'subject',
                                     utc_now,
                                     filters=[])
        self.assertIn("\n-- \nLaunchpad-Notification-Type: bug\n",
                      message.get_payload(decode=True))

    def test_private_team(self):
        # Recipients can be invisible private teams, as
        # BugNotificationBuilder runs in the context of the user making
        # the change. They work fine.
        private_team = self.factory.makeTeam(
            visibility=PersonVisibility.PRIVATE, email="*****@*****.**")
        random = self.factory.makePerson()
        with person_logged_in(random):
            self.assertRaises(Unauthorized, getattr, private_team,
                              'expanded_notification_footers')
            utc_now = datetime.now(pytz.UTC)
            message = self.builder.build('from',
                                         private_team,
                                         'body',
                                         'subject',
                                         utc_now,
                                         filters=[])
        self.assertIn("*****@*****.**", str(message))
Example #9
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_persons = set()
    for new_sub in new_subs:
        to_persons.update(get_recipients(new_sub))

    if not to_persons:
        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_person in sorted(to_persons):
        reason, rationale = recipients.getReason(
            str(removeSecurityProxy(to_person).preferredemail.email))
        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_person,
                                             contents,
                                             subject,
                                             email_date,
                                             rationale=rationale,
                                             references=references)
        sendmail(msg)

    return True