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)
Esempio n. 2
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
Esempio n. 3
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)
Esempio n. 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)
Esempio n. 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
Esempio n. 6
0
 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)
Esempio n. 7
0
 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)
Esempio n. 8
0
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)
Esempio n. 9
0
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)
Esempio n. 10
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)
Esempio n. 11
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
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)])
Esempio n. 13
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
Esempio n. 15
0
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
Esempio n. 16
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)
Esempio n. 17
0
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)
Esempio n. 18
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)
Esempio n. 19
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)])