← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/refactor-mailnotification into lp:launchpad/devel

 

Gavin Panella has proposed merging lp:~allenap/launchpad/refactor-mailnotification into lp:launchpad/devel with lp:~allenap/launchpad/refactor-get-email-notifications as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


Moves almost all of the remaining bug related code out of c/l/mailnotification.py. Also removes a few unused functions.
-- 
https://code.launchpad.net/~allenap/launchpad/refactor-mailnotification/+merge/32923
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/refactor-mailnotification into lp:launchpad/devel.
=== removed file 'lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt'
--- lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt	2005-10-31 18:29:12 +0000
+++ lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt	1970-01-01 00:00:00 +0000
@@ -1,7 +0,0 @@
-The following email was unhandled:    
-
-%(url)s
-
-Error message:
-
-%(error_msg)s

=== modified file 'lib/canonical/launchpad/mailnotification.py'
--- lib/canonical/launchpad/mailnotification.py	2010-07-28 22:33:59 +0000
+++ lib/canonical/launchpad/mailnotification.py	2010-08-17 20:02:42 +0000
@@ -8,18 +8,15 @@
 
 __metaclass__ = type
 
-import datetime
+import re
+
 from difflib import unified_diff
-import operator
-
 from email.Header import Header
 from email.MIMEText import MIMEText
 from email.MIMEMultipart import MIMEMultipart
 from email.MIMEMessage import MIMEMessage
 from email.Utils import formataddr, make_msgid
 
-import re
-
 from zope.component import getAdapter, getUtility
 
 from canonical.config import config
@@ -28,111 +25,30 @@
     get_contact_email_addresses, get_email_template)
 from canonical.launchpad.interfaces import (
     IHeldMessageDetails, IPerson, IPersonSet, ISpecification,
-    IStructuralSubscriptionTarget, ITeamMembershipSet, IUpstreamBugTask,
-    TeamMembershipStatus)
+    ITeamMembershipSet, TeamMembershipStatus)
 from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot
 from canonical.launchpad.interfaces.message import (
     IDirectEmailAuthorization, QuotaReachedError)
 from canonical.launchpad.mail import (
-    sendmail, simple_sendmail, simple_sendmail_from_person, format_address)
+    format_address, sendmail, simple_sendmail, simple_sendmail_from_person)
 from canonical.launchpad.webapp.publisher import canonical_url
 from canonical.launchpad.webapp.url import urlappend
 
-from lp.bugs.adapters.bugdelta import BugDelta
-from lp.bugs.adapters.bugchange import (
-    BugDuplicateChange, get_bug_changes, BugTaskAssigneeChange)
-from lp.bugs.interfaces.bugchange import IBugChange
 from lp.bugs.mail.bugnotificationbuilder import get_bugmail_error_address
-from lp.registry.interfaces.structuralsubscription import (
-    BugNotificationLevel)
 from lp.services.mail.mailwrapper import MailWrapper
 
 # XXX 2010-06-16 gmb bug=594985
 #     This shouldn't be here, but if we take it out lots of things cry,
 #     which is sad.
-from lp.services.mail.notificationrecipientset import (
-    NotificationRecipientSet)
-
-from lp.bugs.mail.bugnotificationbuilder import (
-    BugNotificationBuilder)
-from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
+from lp.services.mail.notificationrecipientset import NotificationRecipientSet
+
+# Silence lint warnings.
+NotificationRecipientSet
+
 
 CC = "CC"
 
 
-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.
-    """
-    prev_subs_set = set(previous_subscribers)
-    cur_subs_set = set(current_subscribers)
-    new_subs = cur_subs_set.difference(prev_subs_set)
-
-    to_addrs = set()
-    for new_sub in new_subs:
-        to_addrs.update(get_contact_email_addresses(new_sub))
-
-    if not to_addrs:
-        return
-
-    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)
-
-
-@block_implicit_flushes
-def update_security_contact_subscriptions(modified_bugtask, event):
-    """Subscribe the new security contact when a bugtask's product changes.
-
-    Only subscribes the new security contact if the bug was marked a
-    security issue originally.
-
-    No change is made for private bugs.
-    """
-    if event.object.bug.private:
-        return
-
-    if not IUpstreamBugTask.providedBy(event.object):
-        return
-
-    bugtask_before_modification = event.object_before_modification
-    bugtask_after_modification = event.object
-
-    if (bugtask_before_modification.product !=
-        bugtask_after_modification.product):
-        new_product = bugtask_after_modification.product
-        if (bugtask_before_modification.bug.security_related and
-            new_product.security_contact):
-            bugtask_after_modification.bug.subscribe(
-                new_product.security_contact, IPerson(event.user))
-
-
 def send_process_error_notification(to_address, subject, error_msg,
                                     original_msg, failing_command=None):
     """Send a mail about an error occurring while using the email interface.
@@ -175,100 +91,6 @@
     sendmail(msg)
 
 
-def notify_errors_list(message, file_alias_url):
-    """Sends an error to the Launchpad errors list."""
-    template = get_email_template('notify-unhandled-email.txt')
-    # We add the error message in as a header too
-    # (X-Launchpad-Unhandled-Email) so we can create filters in the
-    # Launchpad-Error-Reports Mailman mailing list.
-    simple_sendmail(
-        get_bugmail_error_address(), [config.launchpad.errors_address],
-        'Unhandled Email: %s' % file_alias_url,
-        template % {'url': file_alias_url, 'error_msg': message},
-        headers={'X-Launchpad-Unhandled-Email': message})
-
-def generate_bug_add_email(bug, new_recipients=False, reason=None,
-                           subscribed_by=None, event_creator=None):
-    """Generate a new bug notification from the given IBug.
-
-    If new_recipients is supplied we generate a notification explaining
-    that the new recipients have been subscribed to the bug. Otherwise
-    it's just a notification of a new bug report.
-    """
-    subject = u"[Bug %d] [NEW] %s" % (bug.id, bug.title)
-    contents = ''
-
-    if bug.private:
-        # This is a confidential bug.
-        visibility = u"Private"
-    else:
-        # This is a public bug.
-        visibility = u"Public"
-
-    if bug.security_related:
-        visibility += ' security'
-        contents += '*** This bug is a security vulnerability ***\n\n'
-
-    bug_info = []
-    # Add information about the affected upstreams and packages.
-    for bugtask in bug.bugtasks:
-        bug_info.append(u"** Affects: %s" % bugtask.bugtargetname)
-        bug_info.append(u"     Importance: %s" % bugtask.importance.title)
-
-        if bugtask.assignee:
-            # There's a person assigned to fix this task, so show that
-            # information too.
-            bug_info.append(
-                u"     Assignee: %s" % bugtask.assignee.unique_displayname)
-        bug_info.append(u"         Status: %s\n" % bugtask.status.title)
-
-    if bug.tags:
-        bug_info.append('\n** Tags: %s' % ' '.join(bug.tags))
-
-    mailwrapper = MailWrapper(width=72)
-    content_substitutions = {
-        'visibility': visibility,
-        'bug_url': canonical_url(bug),
-        'bug_info': "\n".join(bug_info),
-        'bug_title': bug.title,
-        'description': mailwrapper.format(bug.description),
-        'notification_rationale': reason,
-        }
-
-    if new_recipients:
-        if "assignee" in reason:
-            contents += "You have been assigned a bug task for a %(visibility)s bug"
-            if event_creator is not None:
-                contents += " by %(assigner)s"
-                content_substitutions['assigner'] = (
-                    event_creator.unique_displayname)
-        else:
-            contents += "You have been subscribed to a %(visibility)s bug"
-        if subscribed_by is not None:
-            contents += " by %(subscribed_by)s"
-            content_substitutions['subscribed_by'] = (
-                subscribed_by.unique_displayname)
-        contents += (":\n\n"
-                     "%(description)s\n\n%(bug_info)s")
-        # The visibility appears mid-phrase so.. hack hack.
-        content_substitutions['visibility'] = visibility.lower()
-        # XXX: kiko, 2007-03-21:
-        # We should really have a centralized way of adding this
-        # footer, but right now we lack a INotificationRecipientSet
-        # for this particular situation.
-        contents += (
-            "\n-- \n%(bug_title)s\n%(bug_url)s\n%(notification_rationale)s")
-    else:
-        contents += ("%(visibility)s bug reported:\n\n"
-                     "%(description)s\n\n%(bug_info)s")
-
-    contents = contents % content_substitutions
-
-    contents = contents.rstrip()
-
-    return (subject, contents)
-
-
 def get_unified_diff(old_text, new_text, text_width):
     r"""Return a unified diff of the two texts.
 
@@ -309,254 +131,6 @@
     return text_diff
 
 
-def _get_task_change_row(label, oldval_display, newval_display):
-    """Return a row formatted for display in task change info."""
-    return u"%(label)13s: %(oldval)s => %(newval)s\n" % {
-        'label': label.capitalize(),
-        'oldval': oldval_display,
-        'newval': newval_display}
-
-
-def _get_task_change_values(task_change, displayattrname):
-    """Return the old value and the new value for a task field change."""
-    oldval = task_change.get('old')
-    newval = task_change.get('new')
-
-    oldval_display = None
-    newval_display = None
-
-    if oldval:
-        oldval_display = getattr(oldval, displayattrname)
-    if newval:
-        newval_display = getattr(newval, displayattrname)
-
-    return (oldval_display, newval_display)
-
-
-def get_bug_delta(old_bug, new_bug, user):
-    """Compute the delta from old_bug to new_bug.
-
-    old_bug and new_bug are IBug's. user is an IPerson. Returns an
-    IBugDelta if there are changes, or None if there were no changes.
-    """
-    changes = {}
-
-    for field_name in ("title", "description", "name", "private",
-                       "security_related", "duplicateof", "tags"):
-        # fields for which we show old => new when their values change
-        old_val = getattr(old_bug, field_name)
-        new_val = getattr(new_bug, field_name)
-        if old_val != new_val:
-            changes[field_name] = {}
-            changes[field_name]["old"] = old_val
-            changes[field_name]["new"] = new_val
-
-    if changes:
-        changes["bug"] = new_bug
-        changes["bug_before_modification"] = old_bug
-        changes["bugurl"] = canonical_url(new_bug)
-        changes["user"] = user
-
-        return BugDelta(**changes)
-    else:
-        return None
-
-
-@block_implicit_flushes
-def notify_bug_added(bug, event):
-    """Send an email notification that a bug was added.
-
-    Event must be an IObjectCreatedEvent.
-    """
-
-    bug.addCommentNotification(bug.initial_message)
-
-
-@block_implicit_flushes
-def notify_bug_modified(modified_bug, event):
-    """Notify the Cc'd list that this bug has been modified.
-
-    modified_bug bug must be an IBug. event must be an
-    IObjectModifiedEvent.
-    """
-    bug_delta = get_bug_delta(
-        old_bug=event.object_before_modification,
-        new_bug=event.object, user=IPerson(event.user))
-
-    if bug_delta is not None:
-        add_bug_change_notifications(bug_delta)
-
-
-def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
-    """Return the indirect subscribers for a bug task.
-
-    Return the list of people who should get notifications about
-    changes to the task because of having an indirect subscription
-    relationship with it (by subscribing to its target, being an
-    assignee or owner, etc...)
-
-    If `recipients` is present, add the subscribers to the set of
-    bug notification recipients.
-    """
-    if bugtask.bug.private:
-        return set()
-
-    also_notified_subscribers = set()
-
-    # Assignees are indirect subscribers.
-    if bugtask.assignee:
-        also_notified_subscribers.add(bugtask.assignee)
-        if recipients is not None:
-            recipients.addAssignee(bugtask.assignee)
-
-    if IStructuralSubscriptionTarget.providedBy(bugtask.target):
-        also_notified_subscribers.update(
-            bugtask.target.getBugNotificationsRecipients(
-                recipients, level=level))
-
-    if bugtask.milestone is not None:
-        also_notified_subscribers.update(
-            bugtask.milestone.getBugNotificationsRecipients(
-                recipients, level=level))
-
-    # If the target's bug supervisor isn't set,
-    # we add the owner as a subscriber.
-    pillar = bugtask.pillar
-    if pillar.bug_supervisor is None:
-        also_notified_subscribers.add(pillar.owner)
-        if recipients is not None:
-            recipients.addRegistrant(pillar.owner, pillar)
-
-    return sorted(
-        also_notified_subscribers,
-        key=operator.attrgetter('displayname'))
-
-
-def add_bug_change_notifications(bug_delta, old_bugtask=None,
-                                 new_subscribers=None):
-    """Generate bug notifications and add them to the bug."""
-    changes = get_bug_changes(bug_delta)
-    recipients = bug_delta.bug.getBugNotificationRecipients(
-        old_bug=bug_delta.bug_before_modification,
-        level=BugNotificationLevel.METADATA)
-    if old_bugtask is not None:
-        old_bugtask_recipients = BugNotificationRecipients()
-        get_bugtask_indirect_subscribers(
-            old_bugtask, recipients=old_bugtask_recipients,
-            level=BugNotificationLevel.METADATA)
-        recipients.update(old_bugtask_recipients)
-    for change in changes:
-        # XXX 2009-03-17 gmb [bug=344125]
-        #     This if..else should be removed once the new BugChange API
-        #     is complete and ubiquitous.
-        if IBugChange.providedBy(change):
-            if isinstance(change, BugDuplicateChange):
-                no_dupe_master_recipients = (
-                    bug_delta.bug.getBugNotificationRecipients(
-                        old_bug=bug_delta.bug_before_modification,
-                        level=BugNotificationLevel.METADATA,
-                        include_master_dupe_subscribers=False))
-                bug_delta.bug.addChange(
-                    change, recipients=no_dupe_master_recipients)
-            elif (isinstance(change, BugTaskAssigneeChange) and
-                  new_subscribers is not None):
-                for person in new_subscribers:
-                    reason, rationale = recipients.getReason(person)
-                    if 'Assignee' in rationale:
-                        recipients.remove(person)
-                bug_delta.bug.addChange(change, recipients=recipients)
-            else:
-                bug_delta.bug.addChange(change, recipients=recipients)
-        else:
-            bug_delta.bug.addChangeNotification(
-                change, person=bug_delta.user, recipients=recipients)
-
-
-@block_implicit_flushes
-def notify_bugtask_edited(modified_bugtask, event):
-    """Notify CC'd subscribers of this bug that something has changed
-    on this task.
-
-    modified_bugtask must be an IBugTask. event must be an
-    IObjectModifiedEvent.
-    """
-    bugtask_delta = event.object.getDelta(event.object_before_modification)
-    bug_delta = BugDelta(
-        bug=event.object.bug,
-        bugurl=canonical_url(event.object.bug),
-        bugtask_deltas=bugtask_delta,
-        user=IPerson(event.user))
-
-    event_creator = IPerson(event.user)
-    previous_subscribers = event.object_before_modification.bug_subscribers
-    current_subscribers = event.object.bug_subscribers
-    prev_subs_set = set(previous_subscribers)
-    cur_subs_set = set(current_subscribers)
-    new_subs = cur_subs_set.difference(prev_subs_set)
-
-    add_bug_change_notifications(
-        bug_delta, old_bugtask=event.object_before_modification,
-        new_subscribers=new_subs)
-
-    _send_bug_details_to_new_bug_subscribers(
-        event.object.bug, previous_subscribers, current_subscribers,
-        event_creator=event_creator)
-    update_security_contact_subscriptions(modified_bugtask, event)
-
-
-@block_implicit_flushes
-def notify_bug_comment_added(bugmessage, event):
-    """Notify CC'd list that a message was added to this bug.
-
-    bugmessage must be an IBugMessage. event must be an
-    IObjectCreatedEvent. If bugmessage.bug is a duplicate the
-    comment will also be sent to the dup target's subscribers.
-    """
-    bug = bugmessage.bug
-    bug.addCommentNotification(bugmessage.message)
-
-
-@block_implicit_flushes
-def notify_bug_attachment_added(bugattachment, event):
-    """Notify CC'd list that a new attachment has been added.
-
-    bugattachment must be an IBugAttachment. event must be an
-    IObjectCreatedEvent.
-    """
-    bug = bugattachment.bug
-    bug_delta = BugDelta(
-        bug=bug,
-        bugurl=canonical_url(bug),
-        user=IPerson(event.user),
-        attachment={'new': bugattachment, 'old': None})
-
-    add_bug_change_notifications(bug_delta)
-
-
-@block_implicit_flushes
-def notify_bug_attachment_removed(bugattachment, event):
-    """Notify that an attachment has been removed."""
-    bug = bugattachment.bug
-    bug_delta = BugDelta(
-        bug=bug,
-        bugurl=canonical_url(bug),
-        user=IPerson(event.user),
-        attachment={'old': bugattachment, 'new': None})
-
-    add_bug_change_notifications(bug_delta)
-
-
-@block_implicit_flushes
-def notify_bug_subscription_added(bug_subscription, event):
-    """Notify that a new bug subscription was added."""
-    # When a user is subscribed to a bug by someone other
-    # than themselves, we send them a notification email.
-    if bug_subscription.person != bug_subscription.subscribed_by:
-        _send_bug_details_to_new_bug_subscribers(
-            bug_subscription.bug, [], [bug_subscription.person],
-            subscribed_by=bug_subscription.subscribed_by)
-
-
 @block_implicit_flushes
 def notify_invitation_to_join_team(event):
     """Notify team admins that the team has been invited to join another team.

=== modified file 'lib/canonical/launchpad/subscribers/karma.py'
--- lib/canonical/launchpad/subscribers/karma.py	2010-01-22 01:53:02 +0000
+++ lib/canonical/launchpad/subscribers/karma.py	2010-08-17 20:02:42 +0000
@@ -6,9 +6,9 @@
 
 from canonical.database.sqlbase import block_implicit_flushes
 from canonical.launchpad.interfaces import BugTaskStatus
+from lp.bugs.subscribers.bug import get_bug_delta
 from lp.code.enums import BranchMergeProposalStatus
 from lp.registry.interfaces.person import IPerson
-from canonical.launchpad.mailnotification import get_bug_delta
 
 
 @block_implicit_flushes
@@ -18,6 +18,7 @@
     assert len(bug.bugtasks) >= 1
     _assignKarmaUsingBugContext(IPerson(event.user), bug, 'bugcreated')
 
+
 def _assign_karma_using_bugtask_context(person, bugtask, actionname):
     """Extract the right context from the bugtask and assign karma."""
     distribution = bugtask.distribution
@@ -157,6 +158,7 @@
     """Assign karma to the user who registered the branch."""
     branch.target.assignKarma(branch.registrant, 'branchcreated')
 
+
 @block_implicit_flushes
 def bug_branch_created(bug_branch, event):
     """Assign karma to the user who linked the bug to the branch."""

=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2010-08-02 17:48:13 +0000
+++ lib/lp/bugs/configure.zcml	2010-08-17 20:02:42 +0000
@@ -51,25 +51,22 @@
         handler="lp.bugs.subscribers.bugcreation.at_least_one_task"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBug                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
-        handler="canonical.launchpad.mailnotification.notify_bug_added"/>
+        handler="lp.bugs.subscribers.bug.notify_bug_added"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBug                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
         handler="canonical.launchpad.subscribers.karma.bug_created"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBug                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
-        handler="canonical.launchpad.mailnotification.notify_bug_modified"/>
-    <subscriber
-        for="canonical.launchpad.interfaces.IBug                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
         handler="canonical.launchpad.subscribers.karma.bug_modified"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBug                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
         handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugAttachment                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
-        handler="canonical.launchpad.mailnotification.notify_bug_attachment_added"/>
+        handler="lp.bugs.subscribers.bug.notify_bug_attachment_added"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugAttachment                 lazr.lifecycle.interfaces.IObjectDeletedEvent"
-        handler="canonical.launchpad.mailnotification.notify_bug_attachment_removed"/>
+        handler="lp.bugs.subscribers.bug.notify_bug_attachment_removed"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugAttachment                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
         handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
@@ -90,7 +87,7 @@
         handler="canonical.launchpad.subscribers.karma.cve_added"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugMessage                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
-        handler="canonical.launchpad.mailnotification.notify_bug_comment_added"/>
+        handler="lp.bugs.subscribers.bug.notify_bug_comment_added"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugMessage                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
         handler="canonical.launchpad.subscribers.karma.bug_comment_added"/>
@@ -111,7 +108,7 @@
         handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugSubscription                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
-        handler="canonical.launchpad.mailnotification.notify_bug_subscription_added"/>
+        handler="lp.bugs.subscribers.bug.notify_bug_subscription_added"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBug                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
         handler="lp.bugs.subscribers.bug.notify_bug_modified"/>
@@ -928,7 +925,7 @@
         handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugTask                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
-        handler="canonical.launchpad.mailnotification.notify_bugtask_edited"/>
+        handler="lp.bugs.subscribers.bugtask.notify_bugtask_edited"/>
     <subscriber
         for="canonical.launchpad.interfaces.IBugTask                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
         handler="canonical.launchpad.subscribers.karma.bugtask_modified"/>

=== modified file 'lib/lp/bugs/doc/bugnotification-email.txt'
--- lib/lp/bugs/doc/bugnotification-email.txt	2010-08-04 09:42:07 +0000
+++ lib/lp/bugs/doc/bugnotification-email.txt	2010-08-17 20:02:42 +0000
@@ -19,10 +19,8 @@
 object it gets passed, the formatting logic has been cut into two
 pieces: get_bug_changes and generate_bug_add_email.
 
-    >>> from lp.bugs.adapters.bugchange import (
-    ...     get_bug_changes)
-    >>> from canonical.launchpad.mailnotification import (
-    ...     generate_bug_add_email)
+    >>> from lp.bugs.adapters.bugchange import get_bug_changes
+    >>> from lp.bugs.mail.newbug import generate_bug_add_email
 
 Let's demonstrate what the bugmails will look like, by going through
 the various events that can happen that would cause a notification to
@@ -479,8 +477,7 @@
 mailnotification.py contains a class, BugNotificationBuilder, which is
 used to construct bug notification emails.
 
-    >>> from canonical.launchpad.mailnotification import (
-    ...     BugNotificationBuilder)
+    >>> from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
 
 When instantiatiated it derives a list of common unchanging headers
 from the bug so that they are not calculated for every recipient.

=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt	2010-08-02 17:48:13 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt	2010-08-17 20:02:42 +0000
@@ -101,10 +101,8 @@
 It is also possible to get the list of indirect subscribers for an
 individual bug task.
 
-    >>> from canonical.launchpad.mailnotification import (
-    ...     get_bugtask_indirect_subscribers)
-    >>> get_bugtask_indirect_subscribers(
-    ...     linux_source_bug.bugtasks[0])
+    >>> from lp.bugs.subscribers.bug import get_bugtask_indirect_subscribers
+    >>> get_bugtask_indirect_subscribers(linux_source_bug.bugtasks[0])
     [<Person at ...>]
 
 The list of all bug subscribers can also be accessed via

=== added file 'lib/lp/bugs/mail/newbug.py'
--- lib/lp/bugs/mail/newbug.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/mail/newbug.py	2010-08-17 20:02:42 +0000
@@ -0,0 +1,96 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Mail for new bugs."""
+
+__metaclass__ = type
+__all__ = [
+    'generate_bug_add_email',
+    ]
+
+from canonical.launchpad.webapp.publisher import canonical_url
+
+from lp.services.mail.mailwrapper import MailWrapper
+
+
+def generate_bug_add_email(bug, new_recipients=False, reason=None,
+                           subscribed_by=None, event_creator=None):
+    """Generate a new bug notification from the given IBug.
+
+    If new_recipients is supplied we generate a notification explaining
+    that the new recipients have been subscribed to the bug. Otherwise
+    it's just a notification of a new bug report.
+    """
+    subject = u"[Bug %d] [NEW] %s" % (bug.id, bug.title)
+    contents = ''
+
+    if bug.private:
+        # This is a confidential bug.
+        visibility = u"Private"
+    else:
+        # This is a public bug.
+        visibility = u"Public"
+
+    if bug.security_related:
+        visibility += ' security'
+        contents += '*** This bug is a security vulnerability ***\n\n'
+
+    bug_info = []
+    # Add information about the affected upstreams and packages.
+    for bugtask in bug.bugtasks:
+        bug_info.append(u"** Affects: %s" % bugtask.bugtargetname)
+        bug_info.append(u"     Importance: %s" % bugtask.importance.title)
+
+        if bugtask.assignee:
+            # There's a person assigned to fix this task, so show that
+            # information too.
+            bug_info.append(
+                u"     Assignee: %s" % bugtask.assignee.unique_displayname)
+        bug_info.append(u"         Status: %s\n" % bugtask.status.title)
+
+    if bug.tags:
+        bug_info.append('\n** Tags: %s' % ' '.join(bug.tags))
+
+    mailwrapper = MailWrapper(width=72)
+    content_substitutions = {
+        'visibility': visibility,
+        'bug_url': canonical_url(bug),
+        'bug_info': "\n".join(bug_info),
+        'bug_title': bug.title,
+        'description': mailwrapper.format(bug.description),
+        'notification_rationale': reason,
+        }
+
+    if new_recipients:
+        if "assignee" in reason:
+            contents += (
+                "You have been assigned a bug task for a %(visibility)s bug")
+            if event_creator is not None:
+                contents += " by %(assigner)s"
+                content_substitutions['assigner'] = (
+                    event_creator.unique_displayname)
+        else:
+            contents += "You have been subscribed to a %(visibility)s bug"
+        if subscribed_by is not None:
+            contents += " by %(subscribed_by)s"
+            content_substitutions['subscribed_by'] = (
+                subscribed_by.unique_displayname)
+        contents += (":\n\n"
+                     "%(description)s\n\n%(bug_info)s")
+        # The visibility appears mid-phrase so.. hack hack.
+        content_substitutions['visibility'] = visibility.lower()
+        # XXX: kiko, 2007-03-21:
+        # We should really have a centralized way of adding this
+        # footer, but right now we lack a INotificationRecipientSet
+        # for this particular situation.
+        contents += (
+            "\n-- \n%(bug_title)s\n%(bug_url)s\n%(notification_rationale)s")
+    else:
+        contents += ("%(visibility)s bug reported:\n\n"
+                     "%(description)s\n\n%(bug_info)s")
+
+    contents = contents % content_substitutions
+
+    contents = contents.rstrip()
+
+    return (subject, contents)

=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py	2010-08-17 20:02:41 +0000
+++ lib/lp/bugs/scripts/bugnotification.py	2010-08-17 20:02:42 +0000
@@ -22,15 +22,15 @@
 from canonical.config import config
 from canonical.launchpad.helpers import emailPeople, get_email_template
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
-from canonical.launchpad.mailnotification import (
-    MailWrapper, generate_bug_add_email)
 from canonical.launchpad.scripts.logger import log
 from canonical.launchpad.webapp import canonical_url
 
 from lp.bugs.interfaces.bugmessage import IBugMessageSet
 from lp.bugs.mail.bugnotificationbuilder import (
     BugNotificationBuilder, get_bugmail_from_address)
+from lp.bugs.mail.newbug import generate_bug_add_email
 from lp.registry.interfaces.person import IPersonSet
+from lp.services.mail.mailwrapper import MailWrapper
 
 
 def construct_email_notifications(bug_notifications):

=== modified file 'lib/lp/bugs/subscribers/bug.py'
--- lib/lp/bugs/subscribers/bug.py	2010-01-08 03:12:30 +0000
+++ lib/lp/bugs/subscribers/bug.py	2010-08-17 20:02:42 +0000
@@ -2,19 +2,57 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
-__all__ = ['notify_bug_modified']
-
-
+__all__ = [
+    'add_bug_change_notifications',
+    'get_bug_delta',
+    'get_bugtask_indirect_subscribers',
+    'notify_bug_added',
+    'notify_bug_attachment_added',
+    'notify_bug_attachment_removed',
+    'notify_bug_comment_added',
+    'notify_bug_modified',
+    'notify_bug_subscription_added',
+    'send_bug_details_to_new_bug_subscribers',
+    ]
+
+
+import datetime
+
+from operator import attrgetter
+
+from canonical.config import config
 from canonical.database.sqlbase import block_implicit_flushes
+from canonical.launchpad.helpers import get_contact_email_addresses
+from canonical.launchpad.mail import format_address, sendmail
+from canonical.launchpad.webapp.publisher import canonical_url
+
+from lp.bugs.adapters.bugdelta import BugDelta
+from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
+from lp.bugs.mail.newbug import generate_bug_add_email
 from lp.registry.interfaces.person import IPerson
+from lp.bugs.adapters.bugchange import (
+    BugDuplicateChange, BugTaskAssigneeChange, get_bug_changes)
+from lp.bugs.interfaces.bugchange import IBugChange
+from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
+from lp.registry.interfaces.structuralsubscription import (
+    BugNotificationLevel, IStructuralSubscriptionTarget)
+
+
+@block_implicit_flushes
+def notify_bug_added(bug, event):
+    """Send an email notification that a bug was added.
+
+    Event must be an IObjectCreatedEvent.
+    """
+    bug.addCommentNotification(bug.initial_message)
 
 
 @block_implicit_flushes
 def notify_bug_modified(bug, event):
     """Handle bug change events.
 
-    Subscribe the security contacts for a bug when it
-    becomes security-related.
+    Subscribe the security contacts for a bug when it becomes
+    security-related, and add notifications for the changes.
     """
     if (event.object.security_related and
         not event.object_before_modification.security_related):
@@ -23,3 +61,222 @@
         for pillar in bug.affected_pillars:
             if pillar.security_contact is not None:
                 bug.subscribe(pillar.security_contact, IPerson(event.user))
+
+    bug_delta = get_bug_delta(
+        old_bug=event.object_before_modification,
+        new_bug=event.object, user=IPerson(event.user))
+
+    if bug_delta is not None:
+        add_bug_change_notifications(bug_delta)
+
+
+@block_implicit_flushes
+def notify_bug_comment_added(bugmessage, event):
+    """Notify CC'd list that a message was added to this bug.
+
+    bugmessage must be an IBugMessage. event must be an
+    IObjectCreatedEvent. If bugmessage.bug is a duplicate the
+    comment will also be sent to the dup target's subscribers.
+    """
+    bug = bugmessage.bug
+    bug.addCommentNotification(bugmessage.message)
+
+
+@block_implicit_flushes
+def notify_bug_attachment_added(bugattachment, event):
+    """Notify CC'd list that a new attachment has been added.
+
+    bugattachment must be an IBugAttachment. event must be an
+    IObjectCreatedEvent.
+    """
+    bug = bugattachment.bug
+    bug_delta = BugDelta(
+        bug=bug,
+        bugurl=canonical_url(bug),
+        user=IPerson(event.user),
+        attachment={'new': bugattachment, 'old': None})
+
+    add_bug_change_notifications(bug_delta)
+
+
+@block_implicit_flushes
+def notify_bug_attachment_removed(bugattachment, event):
+    """Notify that an attachment has been removed."""
+    bug = bugattachment.bug
+    bug_delta = BugDelta(
+        bug=bug,
+        bugurl=canonical_url(bug),
+        user=IPerson(event.user),
+        attachment={'old': bugattachment, 'new': None})
+
+    add_bug_change_notifications(bug_delta)
+
+
+@block_implicit_flushes
+def notify_bug_subscription_added(bug_subscription, event):
+    """Notify that a new bug subscription was added."""
+    # When a user is subscribed to a bug by someone other
+    # than themselves, we send them a notification email.
+    if bug_subscription.person != bug_subscription.subscribed_by:
+        send_bug_details_to_new_bug_subscribers(
+            bug_subscription.bug, [], [bug_subscription.person],
+            subscribed_by=bug_subscription.subscribed_by)
+
+
+def get_bug_delta(old_bug, new_bug, user):
+    """Compute the delta from old_bug to new_bug.
+
+    old_bug and new_bug are IBug's. user is an IPerson. Returns an
+    IBugDelta if there are changes, or None if there were no changes.
+    """
+    changes = {}
+
+    for field_name in ("title", "description", "name", "private",
+                       "security_related", "duplicateof", "tags"):
+        # fields for which we show old => new when their values change
+        old_val = getattr(old_bug, field_name)
+        new_val = getattr(new_bug, field_name)
+        if old_val != new_val:
+            changes[field_name] = {}
+            changes[field_name]["old"] = old_val
+            changes[field_name]["new"] = new_val
+
+    if changes:
+        changes["bug"] = new_bug
+        changes["bug_before_modification"] = old_bug
+        changes["bugurl"] = canonical_url(new_bug)
+        changes["user"] = user
+        return BugDelta(**changes)
+    else:
+        return None
+
+
+def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
+    """Return the indirect subscribers for a bug task.
+
+    Return the list of people who should get notifications about
+    changes to the task because of having an indirect subscription
+    relationship with it (by subscribing to its target, being an
+    assignee or owner, etc...)
+
+    If `recipients` is present, add the subscribers to the set of
+    bug notification recipients.
+    """
+    if bugtask.bug.private:
+        return set()
+
+    also_notified_subscribers = set()
+
+    # Assignees are indirect subscribers.
+    if bugtask.assignee:
+        also_notified_subscribers.add(bugtask.assignee)
+        if recipients is not None:
+            recipients.addAssignee(bugtask.assignee)
+
+    if IStructuralSubscriptionTarget.providedBy(bugtask.target):
+        also_notified_subscribers.update(
+            bugtask.target.getBugNotificationsRecipients(
+                recipients, level=level))
+
+    if bugtask.milestone is not None:
+        also_notified_subscribers.update(
+            bugtask.milestone.getBugNotificationsRecipients(
+                recipients, level=level))
+
+    # If the target's bug supervisor isn't set,
+    # we add the owner as a subscriber.
+    pillar = bugtask.pillar
+    if pillar.bug_supervisor is None:
+        also_notified_subscribers.add(pillar.owner)
+        if recipients is not None:
+            recipients.addRegistrant(pillar.owner, pillar)
+
+    return sorted(
+        also_notified_subscribers,
+        key=attrgetter('displayname'))
+
+
+def add_bug_change_notifications(bug_delta, old_bugtask=None,
+                                 new_subscribers=None):
+    """Generate bug notifications and add them to the bug."""
+    changes = get_bug_changes(bug_delta)
+    recipients = bug_delta.bug.getBugNotificationRecipients(
+        old_bug=bug_delta.bug_before_modification,
+        level=BugNotificationLevel.METADATA)
+    if old_bugtask is not None:
+        old_bugtask_recipients = BugNotificationRecipients()
+        get_bugtask_indirect_subscribers(
+            old_bugtask, recipients=old_bugtask_recipients,
+            level=BugNotificationLevel.METADATA)
+        recipients.update(old_bugtask_recipients)
+    for change in changes:
+        # XXX 2009-03-17 gmb [bug=344125]
+        #     This if..else should be removed once the new BugChange API
+        #     is complete and ubiquitous.
+        if IBugChange.providedBy(change):
+            if isinstance(change, BugDuplicateChange):
+                no_dupe_master_recipients = (
+                    bug_delta.bug.getBugNotificationRecipients(
+                        old_bug=bug_delta.bug_before_modification,
+                        level=BugNotificationLevel.METADATA,
+                        include_master_dupe_subscribers=False))
+                bug_delta.bug.addChange(
+                    change, recipients=no_dupe_master_recipients)
+            elif (isinstance(change, BugTaskAssigneeChange) and
+                  new_subscribers is not None):
+                for person in new_subscribers:
+                    reason, rationale = recipients.getReason(person)
+                    if 'Assignee' in rationale:
+                        recipients.remove(person)
+                bug_delta.bug.addChange(change, recipients=recipients)
+            else:
+                bug_delta.bug.addChange(change, recipients=recipients)
+        else:
+            bug_delta.bug.addChangeNotification(
+                change, person=bug_delta.user, recipients=recipients)
+
+
+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.
+    """
+    prev_subs_set = set(previous_subscribers)
+    cur_subs_set = set(current_subscribers)
+    new_subs = cur_subs_set.difference(prev_subs_set)
+
+    to_addrs = set()
+    for new_sub in new_subs:
+        to_addrs.update(get_contact_email_addresses(new_sub))
+
+    if not to_addrs:
+        return
+
+    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)

=== modified file 'lib/lp/bugs/subscribers/bugcreation.py'
--- lib/lp/bugs/subscribers/bugcreation.py	2009-06-25 00:40:31 +0000
+++ lib/lp/bugs/subscribers/bugcreation.py	2010-08-17 20:02:42 +0000
@@ -2,9 +2,13 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
+__all__ = [
+    'at_least_one_task',
+    ]
 
-from canonical.database.sqlbase import block_implicit_flushes
 from lp.bugs.interfaces.bug import CreatedBugWithNoBugTasksError
+
+
 def at_least_one_task(bug, event):
     """Make sure that the created bug has at least one task.
 

=== added file 'lib/lp/bugs/subscribers/bugtask.py'
--- lib/lp/bugs/subscribers/bugtask.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/subscribers/bugtask.py	2010-08-17 20:02:42 +0000
@@ -0,0 +1,78 @@
+# Copyright 2009, 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+    'notify_bugtask_edited',
+    'update_security_contact_subscriptions',
+    ]
+
+
+from canonical.database.sqlbase import block_implicit_flushes
+from canonical.launchpad.webapp.publisher import canonical_url
+
+from lp.bugs.adapters.bugdelta import BugDelta
+from lp.bugs.interfaces.bugtask import IUpstreamBugTask
+from lp.bugs.subscribers.bug import (
+    add_bug_change_notifications, send_bug_details_to_new_bug_subscribers)
+from lp.registry.interfaces.person import IPerson
+
+
+@block_implicit_flushes
+def update_security_contact_subscriptions(event):
+    """Subscribe the new security contact when a bugtask's product changes.
+
+    Only subscribes the new security contact if the bug was marked a
+    security issue originally.
+
+    No change is made for private bugs.
+    """
+    if event.object.bug.private:
+        return
+
+    if not IUpstreamBugTask.providedBy(event.object):
+        return
+
+    bugtask_before_modification = event.object_before_modification
+    bugtask_after_modification = event.object
+
+    if (bugtask_before_modification.product !=
+        bugtask_after_modification.product):
+        new_product = bugtask_after_modification.product
+        if (bugtask_before_modification.bug.security_related and
+            new_product.security_contact):
+            bugtask_after_modification.bug.subscribe(
+                new_product.security_contact, IPerson(event.user))
+
+
+@block_implicit_flushes
+def notify_bugtask_edited(modified_bugtask, event):
+    """Notify CC'd subscribers of this bug that something has changed
+    on this task.
+
+    modified_bugtask must be an IBugTask. event must be an
+    IObjectModifiedEvent.
+    """
+    bugtask_delta = event.object.getDelta(event.object_before_modification)
+    bug_delta = BugDelta(
+        bug=event.object.bug,
+        bugurl=canonical_url(event.object.bug),
+        bugtask_deltas=bugtask_delta,
+        user=IPerson(event.user))
+
+    event_creator = IPerson(event.user)
+    previous_subscribers = event.object_before_modification.bug_subscribers
+    current_subscribers = event.object.bug_subscribers
+    prev_subs_set = set(previous_subscribers)
+    cur_subs_set = set(current_subscribers)
+    new_subs = cur_subs_set.difference(prev_subs_set)
+
+    add_bug_change_notifications(
+        bug_delta, old_bugtask=event.object_before_modification,
+        new_subscribers=new_subs)
+
+    send_bug_details_to_new_bug_subscribers(
+        event.object.bug, previous_subscribers, current_subscribers,
+        event_creator=event_creator)
+
+    update_security_contact_subscriptions(event)


Follow ups