def get_unified_diff(old_text, new_text, text_width):
    r"""Return a unified diff of the two texts.

    Before the diff is produced, the texts are wrapped to the given text
    width.

        >>> print get_unified_diff(
        ...     'Some text\nAnother line\n',get_un
        ...     'Some more text\nAnother line\n',
        ...     text_width=72)
        - Some text
        + Some more text
          Another line

    """
    mailwrapper = MailWrapper(width=72)
    old_text_wrapped = mailwrapper.format(old_text or "")
    new_text_wrapped = mailwrapper.format(new_text or "")

    lines_of_context = len(old_text_wrapped.splitlines())
    text_diff = unified_diff(old_text_wrapped.splitlines(), new_text_wrapped.splitlines(), n=lines_of_context)
    # Remove the diff header, which consists of the first three
    # lines.
    text_diff = list(text_diff)[3:]
    # Let's simplify the diff output by removing the helper lines,
    # which begin with '?'.
    text_diff = [diff_line for diff_line in text_diff if not diff_line.startswith("?")]
    # Add a whitespace between the +/- and the text line.
    text_diff = [re.sub("^([\+\- ])(.*)", r"\1 \2", line) for line in text_diff]
    text_diff = "\n".join(text_diff)
    return text_diff
Exemple #2
0
 def buildBody(self, rationale):
     """See `IQuestionEmailJob`."""
     wrapper = MailWrapper()
     body_parts = [self.body, wrapper.format(rationale)]
     if '\n-- ' not in self.body:
         body_parts.insert(1, '-- ')
     return '\n'.join(body_parts)
 def buildBody(self, rationale):
     """See `IQuestionEmailJob`."""
     wrapper = MailWrapper()
     body_parts = [self.body, wrapper.format(rationale)]
     if '\n-- ' not in self.body:
         body_parts.insert(1, '-- ')
     return '\n'.join(body_parts)
Exemple #4
0
def notify_specification_subscription_modified(specsub, event):
    """Notify a subscriber to a blueprint that their
    subscription has changed.
    """
    user = IPerson(event.user)
    spec = specsub.specification
    person = specsub.person
    # Only send a notification if the
    # subscription changed by someone else.
    if person == user:
        return
    subject = specification_notification_subject(spec)
    if specsub.essential:
        specsub_type = 'Participation essential'
    else:
        specsub_type = 'Participation non-essential'
    mailwrapper = MailWrapper(width=72)
    body = mailwrapper.format(
        'Your subscription to the blueprint '
        '%(blueprint_name)s - %(blueprint_title)s '
        'has changed to [%(specsub_type)s].\n\n'
        '--\n  %(blueprint_url)s' %
        {'blueprint_name': spec.name,
         'blueprint_title': spec.title,
         'specsub_type': specsub_type,
         'blueprint_url': canonical_url(spec)})
    for address in get_contact_email_addresses(person):
        simple_sendmail_from_person(user, address, subject, body)
Exemple #5
0
 def build_message(text):
     mailwrapper = MailWrapper(width=72)
     text = mailwrapper.format(text)
     message = MIMEText(text.encode('utf-8'),
         'plain', 'utf-8')
     # This is redundant and makes the template noisy
     del message['MIME-Version']
     return message
 def sendHelpEmail(self, to_address):
     """Send usage help to `to_address`."""
     # Get the help text (formatted as MoinMoin markup)
     help_text = get_email_template('help.txt', app='bugs')
     help_text = reformat_wiki_text(help_text)
     # Wrap text
     mailwrapper = MailWrapper(width=72)
     help_text = mailwrapper.format(help_text)
     simple_sendmail('*****@*****.**', to_address,
                     'Launchpad Bug Tracker Email Interface Help',
                     help_text)
 def sendHelpEmail(self, to_address):
     """Send usage help to `to_address`."""
     # Get the help text (formatted as MoinMoin markup)
     help_text = get_email_template('help.txt', app='bugs')
     help_text = reformat_wiki_text(help_text)
     # Wrap text
     mailwrapper = MailWrapper(width=72)
     help_text = mailwrapper.format(help_text)
     simple_sendmail(
         '*****@*****.**', to_address,
         'Launchpad Bug Tracker Email Interface Help',
         help_text)
def send_process_error_notification(to_address,
                                    subject,
                                    error_msg,
                                    original_msg,
                                    failing_command=None,
                                    max_return_size=MAX_RETURN_MESSAGE_SIZE):
    """Send a mail about an error occurring while using the email interface.

    Tells the user that an error was encountered while processing his
    request and attaches the original email which caused the error to
    happen.  The original message will be truncated to
    max_return_size bytes.

        :to_address: The address to send the notification to.
        :subject: The subject of the notification.
        :error_msg: The error message that explains the error.
        :original_msg: The original message sent by the user.
        :failing_command: The command that caused the error to happen.
        :max_return_size: The maximum size returned for the original message.
    """
    if isinstance(failing_command, list):
        failing_commands = failing_command
    elif failing_command is None:
        failing_commands = []
    else:
        failing_commands = [failing_command]
    failed_commands_information = ''
    if len(failing_commands) > 0:
        failed_commands_information = 'Failing command:'
        for failing_command in failing_commands:
            failed_commands_information += '\n    %s' % str(failing_command)

    body = get_email_template(
        'email-processing-error.txt', app='services/mail') % {
            'failed_command_information': failed_commands_information,
            'error_msg': error_msg
        }
    mailwrapper = MailWrapper(width=72)
    body = mailwrapper.format(body)
    error_part = MIMEText(body.encode('utf-8'), 'plain', 'utf-8')

    msg = MIMEMultipart()
    msg['To'] = to_address
    msg['From'] = get_bugmail_error_address()
    msg['Subject'] = subject
    msg.attach(error_part)
    original_msg_str = str(original_msg)
    if len(original_msg_str) > max_return_size:
        truncated_msg_str = original_msg_str[:max_return_size]
        original_msg = message_from_string(truncated_msg_str)
    msg.attach(MIMEMessage(original_msg))
    sendmail(msg)
def send_process_error_notification(
    to_address, subject, error_msg, original_msg, failing_command=None, max_return_size=MAX_RETURN_MESSAGE_SIZE
):
    """Send a mail about an error occurring while using the email interface.

    Tells the user that an error was encountered while processing his
    request and attaches the original email which caused the error to
    happen.  The original message will be truncated to
    max_return_size bytes.

        :to_address: The address to send the notification to.
        :subject: The subject of the notification.
        :error_msg: The error message that explains the error.
        :original_msg: The original message sent by the user.
        :failing_command: The command that caused the error to happen.
        :max_return_size: The maximum size returned for the original message.
    """
    if isinstance(failing_command, list):
        failing_commands = failing_command
    elif failing_command is None:
        failing_commands = []
    else:
        failing_commands = [failing_command]
    failed_commands_information = ""
    if len(failing_commands) > 0:
        failed_commands_information = "Failing command:"
        for failing_command in failing_commands:
            failed_commands_information += "\n    %s" % str(failing_command)

    body = get_email_template("email-processing-error.txt", app="services/mail") % {
        "failed_command_information": failed_commands_information,
        "error_msg": error_msg,
    }
    mailwrapper = MailWrapper(width=72)
    body = mailwrapper.format(body)
    error_part = MIMEText(body.encode("utf-8"), "plain", "utf-8")

    msg = MIMEMultipart()
    msg["To"] = to_address
    msg["From"] = get_bugmail_error_address()
    msg["Subject"] = subject
    msg.attach(error_part)
    original_msg_str = str(original_msg)
    if len(original_msg_str) > max_return_size:
        truncated_msg_str = original_msg_str[:max_return_size]
        original_msg = message_from_string(truncated_msg_str)
    msg.attach(MIMEMessage(original_msg))
    sendmail(msg)
Exemple #10
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)
Exemple #11
0
def notify_specification_subscription_created(specsub, event):
    """Notify a user that they have been subscribed to a blueprint."""
    user = IPerson(event.user)
    spec = specsub.specification
    person = specsub.person
    subject = specification_notification_subject(spec)
    mailwrapper = MailWrapper(width=72)
    body = mailwrapper.format(
        'You are now subscribed to the blueprint '
        '%(blueprint_name)s - %(blueprint_title)s.\n\n'
        '-- \n%(blueprint_url)s' %
        {'blueprint_name': spec.name,
         'blueprint_title': spec.title,
         'blueprint_url': canonical_url(spec)})
    for address in get_contact_email_addresses(person):
        simple_sendmail_from_person(user, address, subject, body)
Exemple #12
0
    def initialize(self):
        """Save the old question for comparison. It also set the new_message
        attribute if a new message was added.
        """
        self.old_question = self.event.object_before_modification

        new_messages = set(self.question.messages).difference(
            self.old_question.messages)
        assert len(new_messages) <= 1, (
            "There shouldn't be more than one message for a "
            "notification.")
        if new_messages:
            self.new_message = new_messages.pop()
        else:
            self.new_message = None

        self.wrapper = MailWrapper()
Exemple #13
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)
Exemple #14
0
 def _getBody(self, email, recipient):
     """Return the complete body to use for this email."""
     template = get_email_template(self._getTemplateName(email, recipient),
                                   app=self.app)
     params = self._getTemplateParams(email, recipient)
     body = template % params
     if self._wrap:
         body = MailWrapper().format(body,
                                     force_wrap=self._force_wrap) + "\n"
     footer = self._getFooter(email, recipient, params)
     if footer is not None:
         body = append_footer(body, footer)
     return body
Exemple #15
0
def get_unified_diff(old_text, new_text, text_width):
    r"""Return a unified diff of the two texts.

    Before the diff is produced, the texts are wrapped to the given text
    width.

        >>> print get_unified_diff(
        ...     'Some text\nAnother line\n',get_un
        ...     'Some more text\nAnother line\n',
        ...     text_width=72)
        - Some text
        + Some more text
          Another line

    """
    mailwrapper = MailWrapper(width=72)
    old_text_wrapped = mailwrapper.format(old_text or '')
    new_text_wrapped = mailwrapper.format(new_text or '')

    lines_of_context = len(old_text_wrapped.splitlines())
    text_diff = unified_diff(
        old_text_wrapped.splitlines(),
        new_text_wrapped.splitlines(),
        n=lines_of_context)
    # Remove the diff header, which consists of the first three
    # lines.
    text_diff = list(text_diff)[3:]
    # Let's simplify the diff output by removing the helper lines,
    # which begin with '?'.
    text_diff = [
        diff_line for diff_line in text_diff
        if not diff_line.startswith('?')]
    # Add a whitespace between the +/- and the text line.
    text_diff = [
        re.sub('^([\+\- ])(.*)', r'\1 \2', line)
        for line in text_diff]
    text_diff = '\n'.join(text_diff)
    return text_diff
def send_team_email(from_addr,
                    address,
                    subject,
                    template,
                    replacements,
                    rationale,
                    headers=None):
    """Send a team message with a rationale."""
    if headers is None:
        headers = {}
    body = MailWrapper().format(template % replacements, force_wrap=True)
    footer = "-- \n%s" % rationale
    message = '%s\n\n%s' % (body, footer)
    simple_sendmail(from_addr, address, subject, message, headers)
    def initialize(self):
        """Save the old question for comparison. It also set the new_message
        attribute if a new message was added.
        """
        self.old_question = self.event.object_before_modification

        new_messages = set(
            self.question.messages).difference(self.old_question.messages)
        assert len(new_messages) <= 1, (
                "There shouldn't be more than one message for a "
                "notification.")
        if new_messages:
            self.new_message = new_messages.pop()
        else:
            self.new_message = None

        self.wrapper = MailWrapper()
Exemple #18
0
 def getBodyAndHeaders(self, email_template, address, reply_to=None):
     """See `IProductNotificationJob`."""
     reason, rationale = self.recipients.getReason(address)
     maintainer = self.recipients._emailToPerson[address]
     message_data = dict(self.message_data)
     message_data['user_name'] = maintainer.name
     message_data['user_displayname'] = maintainer.displayname
     raw_body = email_template % message_data
     raw_body += '\n\n-- \n%s' % reason
     body = MailWrapper().format(raw_body, force_wrap=True)
     headers = {
         'X-Launchpad-Project':
         '%(product_displayname)s (%(product_name)s)' % message_data,
         'X-Launchpad-Message-Rationale': rationale,
     }
     if reply_to is not None:
         headers['Reply-To'] = reply_to
     return body, headers
    def _importEntry(self, entry):
        """Perform the import of one entry, and notify the uploader."""
        target = entry.import_into
        self.logger.info('Importing: %s' % target.title)
        (mail_subject, mail_body) = target.importFromQueue(entry, self.logger)

        if mail_subject is not None and self._shouldNotify(entry.importer):
            # A `mail_subject` of None indicates that there
            # is no notification worth sending out.
            from_email = config.rosetta.notification_address
            katie = getUtility(ILaunchpadCelebrities).katie
            if entry.importer == katie:
                # Email import state to Debian imports email.
                to_email = None
            else:
                to_email = get_contact_email_addresses(entry.importer)

            if to_email:
                text = MailWrapper().format(mail_body)
                simple_sendmail(from_email, to_email, mail_subject, text)
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)
Exemple #21
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)
    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)
Exemple #23
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 run(self):
        self.logger.info("Starting verification of POFile stats at id %d" %
                         self.start_at_id)
        loop = Verifier(self.transaction, self.logger, self.start_at_id)

        # Since the script can run for a long time, our deployment
        # process might remove the Launchpad tree the script was run
        # from, thus the script failing to find the email template
        # if it was attempted after DBLoopTuner run is completed.
        # See bug #811447 for OOPS we used to get then.
        template = get_email_template('pofile-stats.txt', 'translations')

        # Each iteration of our loop collects all statistics first, before
        # modifying any rows in the database.  With any locks on the database
        # acquired only at the very end of the iteration, we can afford to
        # make relatively long, low-overhead iterations without disrupting
        # application response times.
        iteration_duration = (
            config.rosetta_pofile_stats.looptuner_iteration_duration)
        DBLoopTuner(loop, iteration_duration).run()

        if loop.total_incorrect > 0 or loop.total_exceptions > 0:
            # Not all statistics were correct, or there were failures while
            # checking them.  Email the admins.
            message = template % {
                'exceptions': loop.total_exceptions,
                'errors': loop.total_incorrect,
                'total': loop.total_checked
            }
            simple_sendmail(from_addr=config.canonical.noreply_from_address,
                            to_addrs=[config.launchpad.errors_address],
                            subject="POFile statistics errors",
                            body=MailWrapper().format(message))
            self.transaction.commit()

        self.logger.info("Done.")
Exemple #25
0
def notify_specification_modified(spec, event):
    """Notify the related people that a specification has been modifed."""
    user = IPerson(event.user)
    spec_delta = spec.getDelta(event.object_before_modification, user)
    if spec_delta is None:
        # XXX: Bjorn Tillenius 2006-03-08:
        #      Ideally, if an IObjectModifiedEvent event is generated,
        #      spec_delta shouldn't be None. I'm not confident that we
        #      have enough test yet to assert this, though.
        return

    subject = specification_notification_subject(spec)
    indent = ' ' * 4
    info_lines = []
    if spec_delta.name:
        info_lines.append('%sName: %s => %s' % (
            indent, spec_delta.name['old'], spec_delta.name['new']))
    for dbitem_name in ('definition_status', 'priority'):
        title = ISpecification[dbitem_name].title
        assert ISpecification[dbitem_name].required, (
            "The mail notification assumes %s can't be None" % dbitem_name)
        dbitem_delta = getattr(spec_delta, dbitem_name)
        if dbitem_delta is not None:
            old_item = dbitem_delta['old']
            new_item = dbitem_delta['new']
            info_lines.append("%s%s: %s => %s" % (
                indent, title, old_item.title, new_item.title))

    for person_attrname in ('approver', 'assignee', 'drafter'):
        title = ISpecification[person_attrname].title
        person_delta = getattr(spec_delta, person_attrname)
        if person_delta is not None:
            old_person = person_delta['old']
            if old_person is None:
                old_value = "(none)"
            else:
                old_value = old_person.displayname
            new_person = person_delta['new']
            if new_person is None:
                new_value = "(none)"
            else:
                new_value = new_person.displayname
            info_lines.append(
                "%s%s: %s => %s" % (indent, title, old_value, new_value))

    mail_wrapper = MailWrapper(width=72)
    if spec_delta.whiteboard is not None:
        if info_lines:
            info_lines.append('')
        whiteboard_delta = spec_delta.whiteboard
        if whiteboard_delta['old'] is None:
            info_lines.append('Whiteboard set to:')
            info_lines.append(mail_wrapper.format(whiteboard_delta['new']))
        else:
            whiteboard_diff = get_unified_diff(
                whiteboard_delta['old'], whiteboard_delta['new'], 72)
            info_lines.append('Whiteboard changed:')
            info_lines.append(whiteboard_diff)
    if spec_delta.workitems_text is not None:
        if info_lines:
            info_lines.append('')
        workitems_delta = spec_delta.workitems_text
        if workitems_delta['old'] is '':
            info_lines.append('Work items set to:')
            info_lines.append(mail_wrapper.format(workitems_delta['new']))
        else:
            workitems_diff = get_unified_diff(
                workitems_delta['old'], workitems_delta['new'], 72)
            info_lines.append('Work items changed:')
            info_lines.append(workitems_diff)
    if not info_lines:
        # The specification was modified, but we don't yet support
        # sending notification for the change.
        return
    body = get_email_template('specification-modified.txt', 'blueprints') % {
        'editor': user.displayname,
        'info_fields': '\n'.join(info_lines),
        'spec_title': spec.title,
        'spec_url': canonical_url(spec)}

    for address in spec.notificationRecipientAddresses():
        simple_sendmail_from_person(user, address, subject, body)
Exemple #26
0
class QuestionModifiedDefaultNotification(QuestionNotification):
    """Base implementation of a notification when a question is modified."""

    recipient_set = QuestionRecipientSet.SUBSCRIBER
    # Email template used to render the body.
    body_template = "question-modified-notification.txt"

    def initialize(self):
        """Save the old question for comparison. It also set the new_message
        attribute if a new message was added.
        """
        self.old_question = self.event.object_before_modification

        new_messages = set(self.question.messages).difference(
            self.old_question.messages)
        assert len(new_messages) <= 1, (
            "There shouldn't be more than one message for a "
            "notification.")
        if new_messages:
            self.new_message = new_messages.pop()
        else:
            self.new_message = None

        self.wrapper = MailWrapper()

    @cachedproperty
    def metadata_changes_text(self):
        """Textual representation of the changes to the question metadata."""
        question = self.question
        old_question = self.old_question
        indent = 4 * ' '
        info_fields = []
        if question.status != old_question.status:
            info_fields.append(
                indent + 'Status: %s => %s' %
                (old_question.status.title, question.status.title))
        if question.target != old_question.target:
            info_fields.append(
                indent + 'Project: %s => %s' %
                (old_question.target.displayname, question.target.displayname))

        if question.assignee != old_question.assignee:
            if old_question.assignee is None:
                old_assignee = None
            else:
                old_assignee = old_question.assignee.displayname
            if question.assignee is None:
                assignee = None
            else:
                assignee = question.assignee.displayname
            info_fields.append(indent + 'Assignee: %s => %s' %
                               (old_assignee, assignee))

        old_bugs = set(old_question.bugs)
        bugs = set(question.bugs)
        for linked_bug in bugs.difference(old_bugs):
            info_fields.append(indent +
                               'Linked to bug: #%s\n' % linked_bug.id +
                               indent + '%s\n' % canonical_url(linked_bug) +
                               indent + '"%s"' % linked_bug.title)
        for unlinked_bug in old_bugs.difference(bugs):
            info_fields.append(indent +
                               'Removed link to bug: #%s\n' % unlinked_bug.id +
                               indent + '%s\n' % canonical_url(unlinked_bug) +
                               indent + '"%s"' % unlinked_bug.title)

        if question.faq != old_question.faq:
            if question.faq is None:
                info_fields.append(indent + 'Related FAQ was removed:\n' +
                                   indent + old_question.faq.title + '\n' +
                                   indent + canonical_url(old_question.faq))
            else:
                info_fields.append(indent + 'Related FAQ set to:\n' + indent +
                                   question.faq.title + '\n' + indent +
                                   canonical_url(question.faq))

        if question.title != old_question.title:
            info_fields.append('Summary changed to:\n%s' % question.title)
        if question.description != old_question.description:
            info_fields.append('Description changed to:\n%s' %
                               (self.wrapper.format(question.description)))

        question_changes = '\n\n'.join(info_fields)
        return question_changes

    def getSubject(self):
        """The reply subject line."""
        line = super(QuestionModifiedDefaultNotification, self).getSubject()
        return 'Re: %s' % line

    def getHeaders(self):
        """Add a References header."""
        headers = QuestionNotification.getHeaders(self)
        if self.new_message:
            # XXX flacoste 2007-02-02 bug=83846:
            # The first message cannot contain a References
            # because we don't create a Message instance for the
            # question description, so we don't have a Message-ID.
            messages = list(self.question.messages)
            assert self.new_message in messages, (
                "Question %s: message id %s not in %s." %
                (self.question.id, self.new_message.id,
                 [m.id for m in messages]))
            index = messages.index(self.new_message)
            if index > 0:
                headers['References'] = (self.question.messages[index -
                                                                1].rfc822msgid)
        return headers

    def shouldNotify(self):
        """Only send a notification when a message was added or some
        metadata was changed.
        """
        return self.new_message or self.metadata_changes_text

    def getBody(self):
        """See QuestionNotification."""
        body = self.metadata_changes_text
        replacements = dict(question_id=self.question.id,
                            target_name=self.question.target.displayname,
                            question_url=canonical_url(self.question))

        if self.new_message:
            if body:
                body += '\n\n'
            body += self.getNewMessageText()
            replacements['new_message_id'] = list(
                self.question.messages).index(self.new_message)

        replacements['body'] = body

        return get_email_template(self.body_template) % replacements

    # Header template used when a new message is added to the question.
    action_header_template = {
        QuestionAction.REQUESTINFO: '%(person)s requested more information:',
        QuestionAction.CONFIRM:
        '%(person)s confirmed that the question is solved:',
        QuestionAction.COMMENT: '%(person)s posted a new comment:',
        QuestionAction.GIVEINFO:
        '%(person)s gave more information on the question:',
        QuestionAction.REOPEN: '%(person)s is still having a problem:',
        QuestionAction.ANSWER: '%(person)s proposed the following answer:',
        QuestionAction.EXPIRE: '%(person)s expired the question:',
        QuestionAction.REJECT: '%(person)s rejected the question:',
        QuestionAction.SETSTATUS: '%(person)s changed the question status:',
    }

    def getNewMessageText(self):
        """Should return the notification text related to a new message."""
        if not self.new_message:
            return ''

        header = self.action_header_template.get(
            self.new_message.action, '%(person)s posted a new message:') % {
                'person': self.new_message.owner.displayname
            }

        return '\n'.join(
            [header,
             self.wrapper.format(self.new_message.text_contents)])
Exemple #27
0
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 and event_creator is not None:
            if event_creator == bugtask.assignee:
                contents += (
                    "You have assigned this bug to yourself for %(target)s")
            else:
                contents += (
                    "%(assigner)s has assigned this bug to you for " +
                    "%(target)s")
            content_substitutions['assigner'] = (
                event_creator.unique_displayname)
            content_substitutions['target'] = bugtask.target.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 send_direct_contact_email(
    sender_email, recipients_set, subject, body):
    """Send a direct user-to-user email.

    :param sender_email: The email address of the sender.
    :type sender_email: string
    :param recipients_set: The recipients.
    :type recipients_set:' A ContactViaWebNotificationSet
    :param subject: The Subject header.
    :type subject: unicode
    :param body: The message body.
    :type body: unicode
    :return: The sent message.
    :rtype: `email.Message.Message`
    """
    # Craft the email message.  Start by checking whether the subject and
    # message bodies are ASCII or not.
    subject_header = encode(subject)
    try:
        body.encode('us-ascii')
        charset = 'us-ascii'
    except UnicodeEncodeError:
        charset = 'utf-8'
    # Get the sender's real name, encoded as per RFC 2047.
    person_set = getUtility(IPersonSet)
    sender = person_set.getByEmail(sender_email)
    assert sender is not None, 'No person for sender %s' % sender_email
    sender_name = str(encode(sender.displayname))
    # Do a single authorization/quota check for the sender.  We consume one
    # quota credit per contact, not per recipient.
    authorization = IDirectEmailAuthorization(sender)
    if not authorization.is_allowed:
        raise QuotaReachedError(sender.displayname, authorization)
    # Add the footer as a unicode string, then encode the body if necessary.
    # This is not entirely optimal if the body has non-ascii characters in it,
    # since the footer may get garbled in a non-MIME aware mail reader.  Who
    # uses those anyway!?  The only alternative is to attach the footer as a
    # MIME attachment with a us-ascii charset, but that has it's own set of
    # problems (and user complaints).  Email sucks.
    additions = u'\n'.join([
        u'',
        u'-- ',
        u'This message was sent from Launchpad by',
        u'%s (%s)' % (sender_name, canonical_url(sender)),
        u'%s.',
        u'For more information see',
        u'https://help.launchpad.net/YourAccount/ContactingPeople',
        ])
    # Craft and send one message per recipient.
    mailwrapper = MailWrapper(width=72)
    message = None
    for recipient_email, recipient in recipients_set.getRecipientPersons():
        recipient_name = str(encode(recipient.displayname))
        reason, rational_header = recipients_set.getReason(recipient_email)
        reason = str(encode(reason)).replace('\n ', '\n')
        formatted_body = mailwrapper.format(body, force_wrap=True)
        formatted_body += additions % reason
        formatted_body = formatted_body.encode(charset)
        message = MIMEText(formatted_body, _charset=charset)
        message['From'] = formataddr((sender_name, sender_email))
        message['To'] = formataddr((recipient_name, recipient_email))
        message['Subject'] = subject_header
        message['Message-ID'] = make_msgid('launchpad')
        message['X-Launchpad-Message-Rationale'] = rational_header
        # Send the message.
        sendmail(message, bulk=False)
    # Use the information from the last message sent to record the action
    # taken. The record will be used to throttle user-to-user emails.
    if message is not None:
        authorization.record(message)
Exemple #29
0
    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')
Exemple #30
0
 def getReason(self):
     """See `RecipientReason`."""
     return MailWrapper(width=72).format(
         super(PackageUploadRecipientReason, self).getReason())
class QuestionModifiedDefaultNotification(QuestionNotification):
    """Base implementation of a notification when a question is modified."""

    recipient_set = QuestionRecipientSet.SUBSCRIBER
    # Email template used to render the body.
    body_template = "question-modified-notification.txt"

    def initialize(self):
        """Save the old question for comparison. It also set the new_message
        attribute if a new message was added.
        """
        self.old_question = self.event.object_before_modification

        new_messages = set(
            self.question.messages).difference(self.old_question.messages)
        assert len(new_messages) <= 1, (
                "There shouldn't be more than one message for a "
                "notification.")
        if new_messages:
            self.new_message = new_messages.pop()
        else:
            self.new_message = None

        self.wrapper = MailWrapper()

    @cachedproperty
    def metadata_changes_text(self):
        """Textual representation of the changes to the question metadata."""
        question = self.question
        old_question = self.old_question
        indent = 4 * ' '
        info_fields = []
        if question.status != old_question.status:
            info_fields.append(indent + 'Status: %s => %s' % (
                old_question.status.title, question.status.title))
        if question.target != old_question.target:
            info_fields.append(
                indent + 'Project: %s => %s' % (
                old_question.target.displayname, question.target.displayname))

        if question.assignee != old_question.assignee:
            if old_question.assignee is None:
                old_assignee = None
            else:
                old_assignee = old_question.assignee.displayname
            if question.assignee is None:
                assignee = None
            else:
                assignee = question.assignee.displayname
            info_fields.append(indent + 'Assignee: %s => %s' % (
               old_assignee, assignee))

        old_bugs = set(old_question.bugs)
        bugs = set(question.bugs)
        for linked_bug in bugs.difference(old_bugs):
            info_fields.append(
                indent + 'Linked to bug: #%s\n' % linked_bug.id +
                indent + '%s\n' % canonical_url(linked_bug) +
                indent + '"%s"' % linked_bug.title)
        for unlinked_bug in old_bugs.difference(bugs):
            info_fields.append(
                indent + 'Removed link to bug: #%s\n' % unlinked_bug.id +
                indent + '%s\n' % canonical_url(unlinked_bug) +
                indent + '"%s"' % unlinked_bug.title)

        if question.faq != old_question.faq:
            if question.faq is None:
                info_fields.append(
                    indent + 'Related FAQ was removed:\n' +
                    indent + old_question.faq.title + '\n' +
                    indent + canonical_url(old_question.faq))
            else:
                info_fields.append(
                    indent + 'Related FAQ set to:\n' +
                    indent + question.faq.title + '\n' +
                    indent + canonical_url(question.faq))

        if question.title != old_question.title:
            info_fields.append('Summary changed to:\n%s' % question.title)
        if question.description != old_question.description:
            info_fields.append(
                'Description changed to:\n%s' % (
                    self.wrapper.format(question.description)))

        question_changes = '\n\n'.join(info_fields)
        return question_changes

    def getSubject(self):
        """The reply subject line."""
        line = super(QuestionModifiedDefaultNotification, self).getSubject()
        return 'Re: %s' % line

    def getHeaders(self):
        """Add a References header."""
        headers = QuestionNotification.getHeaders(self)
        if self.new_message:
            # XXX flacoste 2007-02-02 bug=83846:
            # The first message cannot contain a References
            # because we don't create a Message instance for the
            # question description, so we don't have a Message-ID.
            messages = list(self.question.messages)
            assert self.new_message in messages, (
                "Question %s: message id %s not in %s." % (
                    self.question.id, self.new_message.id,
                    [m.id for m in messages]))
            index = messages.index(self.new_message)
            if index > 0:
                headers['References'] = (
                    self.question.messages[index - 1].rfc822msgid)
        return headers

    def shouldNotify(self):
        """Only send a notification when a message was added or some
        metadata was changed.
        """
        return self.new_message or self.metadata_changes_text

    def getBody(self):
        """See QuestionNotification."""
        body = self.metadata_changes_text
        replacements = dict(
            question_id=self.question.id,
            target_name=self.question.target.displayname,
            question_url=canonical_url(self.question))

        if self.new_message:
            if body:
                body += '\n\n'
            body += self.getNewMessageText()
            replacements['new_message_id'] = list(
                self.question.messages).index(self.new_message)

        replacements['body'] = body

        return get_email_template(self.body_template) % replacements

    # Header template used when a new message is added to the question.
    action_header_template = {
        QuestionAction.REQUESTINFO:
            '%(person)s requested more information:',
        QuestionAction.CONFIRM:
            '%(person)s confirmed that the question is solved:',
        QuestionAction.COMMENT:
            '%(person)s posted a new comment:',
        QuestionAction.GIVEINFO:
            '%(person)s gave more information on the question:',
        QuestionAction.REOPEN:
            '%(person)s is still having a problem:',
        QuestionAction.ANSWER:
            '%(person)s proposed the following answer:',
        QuestionAction.EXPIRE:
            '%(person)s expired the question:',
        QuestionAction.REJECT:
            '%(person)s rejected the question:',
        QuestionAction.SETSTATUS:
            '%(person)s changed the question status:',
    }

    def getNewMessageText(self):
        """Should return the notification text related to a new message."""
        if not self.new_message:
            return ''

        header = self.action_header_template.get(
            self.new_message.action, '%(person)s posted a new message:') % {
            'person': self.new_message.owner.displayname}

        return '\n'.join([
            header, self.wrapper.format(self.new_message.text_contents)])
Exemple #32
0
def send_direct_contact_email(sender_email, recipients_set, person_or_team,
                              subject, body):
    """Send a direct user-to-user email.

    :param sender_email: The email address of the sender.
    :type sender_email: string
    :param recipients_set: The recipients.
    :type recipients_set: `ContactViaWebNotificationSet`
    :param person_or_team: The party that is the context of the email.
    :type person_or_team: `IPerson`
    :param subject: The Subject header.
    :type subject: unicode
    :param body: The message body.
    :type body: unicode
    :return: The sent message.
    :rtype: `email.message.Message`
    """
    # Craft the email message.  Start by checking whether the subject and
    # message bodies are ASCII or not.
    subject_header = encode(subject)
    try:
        body.encode('us-ascii')
        charset = 'us-ascii'
    except UnicodeEncodeError:
        charset = 'utf-8'
    # Get the sender's real name, encoded as per RFC 2047.
    person_set = getUtility(IPersonSet)
    sender = person_set.getByEmail(sender_email)
    assert sender is not None, 'No person for sender %s' % sender_email
    sender_name = str(encode(sender.displayname))
    # Do a single authorization/quota check for the sender.  We consume one
    # quota credit per contact, not per recipient.
    authorization = IDirectEmailAuthorization(sender)
    if not authorization.is_allowed:
        raise QuotaReachedError(sender.displayname, authorization)
    # Add the footer as a unicode string, then encode the body if necessary.
    # This is not entirely optimal if the body has non-ascii characters in it,
    # since the footer may get garbled in a non-MIME aware mail reader.  Who
    # uses those anyway!?  The only alternative is to attach the footer as a
    # MIME attachment with a us-ascii charset, but that has it's own set of
    # problems (and user complaints).  Email sucks.
    additions = u'\n'.join([
        u'',
        u'-- ',
        u'This message was sent from Launchpad by',
        u'%s (%s)' % (sender_name, canonical_url(sender)),
        u'%s.',
        u'For more information see',
        u'https://help.launchpad.net/YourAccount/ContactingPeople',
    ])
    # Craft and send one message per recipient.
    mailwrapper = MailWrapper(width=72)
    message = None
    for recipient_email, recipient in recipients_set.getRecipientPersons():
        recipient_name = str(encode(recipient.displayname))
        reason, rationale_header = recipients_set.getReason(recipient_email)
        reason = str(encode(reason)).replace('\n ', '\n')
        formatted_body = mailwrapper.format(body, force_wrap=True)
        formatted_body += additions % reason
        formatted_body = formatted_body.encode(charset)
        message = MIMEText(formatted_body, _charset=charset)
        message['From'] = formataddr((sender_name, sender_email))
        message['To'] = formataddr((recipient_name, recipient_email))
        message['Subject'] = subject_header
        message['Message-ID'] = make_msgid('launchpad')
        message['X-Launchpad-Message-Rationale'] = rationale_header
        message['X-Launchpad-Message-For'] = person_or_team.name
        # Send the message.
        sendmail(message, bulk=False)
    # Use the information from the last message sent to record the action
    # taken. The record will be used to throttle user-to-user emails.
    if message is not None:
        authorization.record(message)
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