def main(self):
        notifications_sent = False
        bug_notification_set = getUtility(IBugNotificationSet)
        deferred_notifications = \
            bug_notification_set.getDeferredNotifications()
        process_deferred_notifications(deferred_notifications)
        pending_notifications = get_email_notifications(
            bug_notification_set.getNotificationsToSend())
        for (bug_notifications,
             omitted_notifications,
             messages) in pending_notifications:
            for message in messages:
                self.logger.info("Notifying %s about bug %d." % (
                    message['To'], bug_notifications[0].bug.id))
                sendmail(message)
                self.logger.debug(message.as_string())
            for notification in bug_notifications:
                notification.date_emailed = UTC_NOW
                notification.status = BugNotificationStatus.SENT
            for notification in omitted_notifications:
                notification.date_emailed = UTC_NOW
                notification.status = BugNotificationStatus.OMITTED
            notifications_sent = True
            # Commit after each batch of email sent, so that we won't
            # re-mail the notifications in case of something going wrong
            # in the middle.
            self.txn.commit()

        if not notifications_sent:
            self.logger.debug("No notifications are pending to be sent.")
Example #2
0
    def main(self):
        notifications_sent = False
        bug_notification_set = getUtility(IBugNotificationSet)
        deferred_notifications = \
            bug_notification_set.getDeferredNotifications()
        process_deferred_notifications(deferred_notifications)
        pending_notifications = get_email_notifications(
            bug_notification_set.getNotificationsToSend())
        for (bug_notifications,
             omitted_notifications,
             messages) in pending_notifications:
            try:
                for message in messages:
                    try:
                        self.logger.info("Notifying %s about bug %d." % (
                            message['To'], bug_notifications[0].bug.id))
                        sendmail(message)
                        self.logger.debug(message.as_string())
                    except SMTPException:
                        request = ScriptRequest([
                            ("script_name", self.name),
                            ("path", sys.argv[0]),
                            ])
                        error_utility = getUtility(IErrorReportingUtility)
                        oops_vars = {
                            "message_id": message.get("Message-Id"),
                            "notification_type": "bug",
                            "recipient": message["To"],
                            "subject": message["Subject"],
                            }
                        with error_utility.oopsMessage(oops_vars):
                            error_utility.raising(sys.exc_info(), request)
                        self.logger.info(request.oopsid)
                        self.txn.abort()
                        # Re-raise to get out of this loop and go to the
                        # next iteration of the outer loop.
                        raise
            except SMTPException:
                continue
            for notification in bug_notifications:
                notification.date_emailed = UTC_NOW
                notification.status = BugNotificationStatus.SENT
            for notification in omitted_notifications:
                notification.date_emailed = UTC_NOW
                notification.status = BugNotificationStatus.OMITTED
            notifications_sent = True
            # Commit after each batch of email sent, so that we won't
            # re-mail the notifications in case of something going wrong
            # in the middle.
            self.txn.commit()

        if not notifications_sent:
            self.logger.debug("No notifications are pending to be sent.")
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)
Example #4
0
def send_bug_details_to_new_bug_subscribers(
    bug, previous_subscribers, current_subscribers, subscribed_by=None,
    event_creator=None):
    """Send an email containing full bug details to new bug subscribers.

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

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

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

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

    if not to_addrs:
        return False

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

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

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

    return True
Example #5
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)
Example #6
0
    def test_sendmail_with_email_header(self):
        """Check the timeline is ok even if there is an email.header.Header.

        See https://bugs.launchpad.net/launchpad/+bug/885972
        """
        fake_mailer = RecordingMailer()
        self.useFixture(ZopeUtilityFixture(fake_mailer, IMailDelivery, 'Mail'))
        subject_str = self.getUniqueString('subject')
        subject_header = email.header.Header(subject_str)
        message = Message()
        message.add_header('From', '*****@*****.**')
        message['Subject'] = subject_header
        message.add_header('To', '*****@*****.**')
        with CaptureTimeline() as ctl:
            sendmail.sendmail(message)
        self.assertEqual(fake_mailer.from_addr, '*****@*****.**')
        self.assertEqual(fake_mailer.to_addr, ['*****@*****.**'])
        self.checkTimelineHasOneMailAction(ctl.timeline, subject=subject_str)
    def test_sendmail_with_email_header(self):
        """Check the timeline is ok even if there is an email.Header.

        See https://bugs.launchpad.net/launchpad/+bug/885972
        """
        fake_mailer = RecordingMailer()
        self.useFixture(ZopeUtilityFixture(
            fake_mailer, IMailDelivery, 'Mail'))
        subject_str = self.getUniqueString('subject')
        subject_header = email.header.Header(subject_str)
        message = Message()
        message.add_header('From', '*****@*****.**')
        message['Subject'] = subject_header
        message.add_header('To', '*****@*****.**')
        with CaptureTimeline() as ctl:
            sendmail.sendmail(message)
        self.assertEquals(fake_mailer.from_addr, '*****@*****.**')
        self.assertEquals(fake_mailer.to_addr, ['*****@*****.**'])
        self.checkTimelineHasOneMailAction(ctl.timeline, subject=subject_str)
Example #8
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 send_mail(spr,
              archive,
              to_addrs,
              subject,
              mail_text,
              dry_run,
              from_addr=None,
              bcc=None,
              changesfile_content=None,
              attach_changes=False,
              logger=None):
    """Send an email to to_addrs with the given text and subject.

    :param spr: The `ISourcePackageRelease` to be notified about.
    :param archive: The target `IArchive`.
    :param to_addrs: A list of email addresses to be used as recipients.
        Each email must be a valid ASCII str instance or a unicode one.
    :param subject: The email's subject.
    :param mail_text: The text body of the email. Unicode is preserved in the
        email.
    :param dry_run: Whether or not an email should actually be sent. But
        please note that this flag is (largely) ignored.
    :param from_addr: The email address to be used as the sender. Must be a
        valid ASCII str instance or a unicode one.  Defaults to the email
        for config.uploader.
    :param bcc: Optional email Blind Carbon Copy address(es).
    :param param changesfile_content: The content of the actual changesfile.
    :param attach_changes: A flag governing whether the original changesfile
        content shall be attached to the email.
    """
    extra_headers = {'X-Katie': 'Launchpad actually'}

    # Include the 'X-Launchpad-PPA' header for PPA upload notfications
    # containing the PPA owner name.
    if archive.is_ppa:
        extra_headers['X-Launchpad-PPA'] = get_ppa_reference(archive)

    # Include a 'X-Launchpad-Component' header with the component and
    # the section of the source package uploaded in order to facilitate
    # filtering on the part of the email recipients.
    if spr:
        xlp_component_header = 'component=%s, section=%s' % (
            spr.component.name, spr.section.name)
        extra_headers['X-Launchpad-Component'] = xlp_component_header

    if from_addr is None:
        from_addr = format_address(config.uploader.default_sender_name,
                                   config.uploader.default_sender_address)

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

    # All emails from here have a Bcc to the default recipient.
    bcc_text = format_address(config.uploader.default_recipient_name,
                              config.uploader.default_recipient_address)
    if bcc:
        bcc_text = "%s, %s" % (bcc_text, bcc)
    extra_headers['Bcc'] = ascii_smash(bcc_text)

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

    if dry_run and logger is not None:
        debug(logger, "Would have sent a mail:")
    else:
        debug(logger, "Sent a mail:")
    debug(logger, "  Subject: %s" % subject)
    debug(logger, "  Sender: %s" % from_addr)
    debug(logger, "  Recipients: %s" % recipients)
    if 'Bcc' in extra_headers:
        debug(logger, "  Bcc: %s" % extra_headers['Bcc'])
    debug(logger, "  Body:")
    for line in mail_text.splitlines():
        if isinstance(line, str):
            line = line.decode('utf-8', 'replace')
        debug(logger, line)

    if not dry_run:
        # Since we need to send the original changesfile as an
        # attachment the sendmail() method will be used as opposed to
        # simple_sendmail().
        message = MIMEMultipart()
        message['from'] = from_addr
        message['subject'] = subject
        message['to'] = recipients

        # Set the extra headers if any are present.
        for key, value in extra_headers.iteritems():
            message.add_header(key, value)

        # Add the email body.
        message.attach(
            MIMEText(
                sanitize_string(mail_text).encode('utf-8'), 'plain', 'utf-8'))

        if attach_changes:
            # Add the original changesfile as an attachment.
            if changesfile_content is not None:
                changesfile_text = sanitize_string(changesfile_content)
            else:
                changesfile_text = ("Sorry, changesfile not available.")

            attachment = MIMEText(changesfile_text.encode('utf-8'), 'plain',
                                  'utf-8')
            attachment.add_header('Content-Disposition',
                                  'attachment; filename="changesfile"')
            message.attach(attachment)

        # And finally send the message.
        sendmail(message)
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)
def send_mail(
    spr, archive, to_addrs, subject, mail_text, dry_run, from_addr=None,
    bcc=None, changesfile_content=None, attach_changes=False, logger=None):
    """Send an email to to_addrs with the given text and subject.

    :param spr: The `ISourcePackageRelease` to be notified about.
    :param archive: The target `IArchive`.
    :param to_addrs: A list of email addresses to be used as recipients.
        Each email must be a valid ASCII str instance or a unicode one.
    :param subject: The email's subject.
    :param mail_text: The text body of the email. Unicode is preserved in the
        email.
    :param dry_run: Whether or not an email should actually be sent. But
        please note that this flag is (largely) ignored.
    :param from_addr: The email address to be used as the sender. Must be a
        valid ASCII str instance or a unicode one.  Defaults to the email
        for config.uploader.
    :param bcc: Optional email Blind Carbon Copy address(es).
    :param param changesfile_content: The content of the actual changesfile.
    :param attach_changes: A flag governing whether the original changesfile
        content shall be attached to the email.
    """
    extra_headers = {'X-Katie': 'Launchpad actually'}

    # Include the 'X-Launchpad-PPA' header for PPA upload notfications
    # containing the PPA owner name.
    if archive.is_ppa:
        extra_headers['X-Launchpad-PPA'] = get_ppa_reference(archive)

    # Include a 'X-Launchpad-Component' header with the component and
    # the section of the source package uploaded in order to facilitate
    # filtering on the part of the email recipients.
    if spr:
        xlp_component_header = 'component=%s, section=%s' % (
            spr.component.name, spr.section.name)
        extra_headers['X-Launchpad-Component'] = xlp_component_header

    if from_addr is None:
        from_addr = format_address(
            config.uploader.default_sender_name,
            config.uploader.default_sender_address)

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

    # All emails from here have a Bcc to the default recipient.
    bcc_text = format_address(
        config.uploader.default_recipient_name,
        config.uploader.default_recipient_address)
    if bcc:
        bcc_text = "%s, %s" % (bcc_text, bcc)
    extra_headers['Bcc'] = ascii_smash(bcc_text)

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

    if dry_run and logger is not None:
        debug(logger, "Would have sent a mail:")
    else:
        debug(logger, "Sent a mail:")
    debug(logger, "  Subject: %s" % subject)
    debug(logger, "  Sender: %s" % from_addr)
    debug(logger, "  Recipients: %s" % recipients)
    if 'Bcc' in extra_headers:
        debug(logger, "  Bcc: %s" % extra_headers['Bcc'])
    debug(logger, "  Body:")
    for line in mail_text.splitlines():
        if isinstance(line, str):
            line = line.decode('utf-8', 'replace')
        debug(logger, line)

    if not dry_run:
        # Since we need to send the original changesfile as an
        # attachment the sendmail() method will be used as opposed to
        # simple_sendmail().
        message = MIMEMultipart()
        message['from'] = from_addr
        message['subject'] = subject
        message['to'] = recipients

        # Set the extra headers if any are present.
        for key, value in extra_headers.iteritems():
            message.add_header(key, value)

        # Add the email body.
        message.attach(
            MIMEText(sanitize_string(mail_text).encode('utf-8'),
                'plain', 'utf-8'))

        if attach_changes:
            # Add the original changesfile as an attachment.
            if changesfile_content is not None:
                changesfile_text = sanitize_string(changesfile_content)
            else:
                changesfile_text = ("Sorry, changesfile not available.")

            attachment = MIMEText(
                changesfile_text.encode('utf-8'), 'plain', 'utf-8')
            attachment.add_header(
                'Content-Disposition',
                'attachment; filename="changesfile"')
            message.attach(attachment)

        # And finally send the message.
        sendmail(message)
Example #12
0
def send_bug_details_to_new_bug_subscribers(bug,
                                            previous_subscribers,
                                            current_subscribers,
                                            subscribed_by=None,
                                            event_creator=None):
    """Send an email containing full bug details to new bug subscribers.

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

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

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

    to_persons = set()
    for new_sub in new_subs:
        to_persons.update(get_recipients(new_sub))

    if not to_persons:
        return False

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

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

    bug_notification_builder = BugNotificationBuilder(bug, event_creator)
    for to_person in sorted(to_persons):
        reason, rationale = recipients.getReason(
            str(removeSecurityProxy(to_person).preferredemail.email))
        subject, contents = generate_bug_add_email(bug,
                                                   new_recipients=True,
                                                   subscribed_by=subscribed_by,
                                                   reason=reason,
                                                   event_creator=event_creator)
        msg = bug_notification_builder.build(from_addr,
                                             to_person,
                                             contents,
                                             subject,
                                             email_date,
                                             rationale=rationale,
                                             references=references)
        sendmail(msg)

    return True