Exemple #1
0
    def __init__(self,
                 subject,
                 template_name,
                 recipients,
                 from_address,
                 delta=None,
                 message_id=None,
                 notification_type=None,
                 mail_controller_class=None,
                 request=None,
                 wrap=False,
                 force_wrap=False):
        """Constructor.

        :param subject: A Python dict-replacement template for the subject
            line of the email.
        :param template: Name of the template to use for the message body.
        :param recipients: A dict of recipient to Subscription.
        :param from_address: The from_address to use on emails.
        :param delta: A Delta object with members "delta_values", "interface"
            and "new_values", such as BranchMergeProposalDelta.
        :param message_id: The Message-Id to use for generated emails.  If
            not supplied, random message-ids will be used.
        :param mail_controller_class: The class of the mail controller to
            use to send the mails.  Defaults to `MailController`.
        :param request: An optional `IErrorReportRequest` to use when
            logging OOPSes.
        :param wrap: Wrap body text using `MailWrapper`.
        :param force_wrap: See `MailWrapper.format`.
        """
        # Running mail notifications with web security is too fragile: it's
        # easy to end up with subtle bugs due to such things as
        # subscriptions from private teams that are inaccessible to the user
        # with the current interaction.  BaseMailer always sends one mail
        # per recipient and thus never leaks information to other users, so
        # it's safer to require a permissive security policy.
        #
        # When converting other notification code to BaseMailer, it may be
        # necessary to move notifications into jobs, to move unit tests to a
        # Zopeless-based layer, or to use the permissive_security_policy
        # context manager.
        assert getSecurityPolicy() == LaunchpadPermissiveSecurityPolicy, (
            "BaseMailer may only be used with a permissive security policy.")

        self._subject_template = subject
        self._template_name = template_name
        self._recipients = NotificationRecipientSet()
        for recipient, reason in recipients.iteritems():
            self._recipients.add(recipient, reason, reason.mail_header)
        self.from_address = from_address
        self.delta = delta
        self.message_id = message_id
        self.notification_type = notification_type
        self.logger = logging.getLogger('lp.services.mail.basemailer')
        if mail_controller_class is None:
            mail_controller_class = MailController
        self._mail_controller_class = mail_controller_class
        self.request = request
        self._wrap = wrap
        self._force_wrap = force_wrap
 def test_send_message(self):
     self.factory.makePerson(email='*****@*****.**', name='me')
     user = self.factory.makePerson(email='*****@*****.**', name='him')
     subject = 'test subject'
     body = 'test body'
     recipients_set = NotificationRecipientSet()
     recipients_set.add(user, 'test reason', 'test rationale')
     pop_notifications()
     send_direct_contact_email('*****@*****.**', recipients_set, subject, body)
     notifications = pop_notifications()
     notification = notifications[0]
     self.assertEqual(1, len(notifications))
     self.assertEqual('Me <*****@*****.**>', notification['From'])
     self.assertEqual('Him <*****@*****.**>', notification['To'])
     self.assertEqual(subject, notification['Subject'])
     self.assertEqual('test rationale',
                      notification['X-Launchpad-Message-Rationale'])
     self.assertIs(None, notification['Precedence'])
     self.assertTrue('launchpad' in notification['Message-ID'])
     self.assertEqual(
         '\n'.join([
             '%s' % body, '-- ', 'This message was sent from Launchpad by',
             'Me (http://launchpad.dev/~me)', 'test reason.',
             'For more information see',
             'https://help.launchpad.net/YourAccount/ContactingPeople'
         ]), notification.get_payload())
 def recipients(self):
     """See `IQuestionEmailJob`."""
     term = QuestionRecipientSet.getTermByToken(
         self.metadata['recipient_set'])
     question_recipient_set = term.value
     if question_recipient_set == QuestionRecipientSet.ASKER:
         recipients = NotificationRecipientSet()
         owner = self.question.owner
         original_recipients = self.question.direct_recipients
         if owner in original_recipients:
             rationale, header = original_recipients.getReason(owner)
             recipients.add(owner, rationale, header)
         return recipients
     elif question_recipient_set == QuestionRecipientSet.SUBSCRIBER:
         recipients = self.question.getRecipients()
         if self.question.owner in recipients:
             recipients.remove(self.question.owner)
         return recipients
     elif question_recipient_set == QuestionRecipientSet.ASKER_SUBSCRIBER:
         return self.question.getRecipients()
     elif question_recipient_set == QuestionRecipientSet.CONTACT:
         return self.question.target.getAnswerContactRecipients(None)
     else:
         raise ValueError(
             'Unsupported QuestionRecipientSet value: %s' %
             question_recipient_set)
 def test_send_message(self):
     self.factory.makePerson(email='*****@*****.**', name='me')
     user = self.factory.makePerson(email='*****@*****.**', name='him')
     subject = 'test subject'
     body = 'test body'
     recipients_set = NotificationRecipientSet()
     recipients_set.add(user, 'test reason', 'test rationale')
     pop_notifications()
     send_direct_contact_email('*****@*****.**', recipients_set, subject, body)
     notifications = pop_notifications()
     notification = notifications[0]
     self.assertEqual(1, len(notifications))
     self.assertEqual('Me <*****@*****.**>', notification['From'])
     self.assertEqual('Him <*****@*****.**>', notification['To'])
     self.assertEqual(subject, notification['Subject'])
     self.assertEqual(
         'test rationale', notification['X-Launchpad-Message-Rationale'])
     self.assertIs(None, notification['Precedence'])
     self.assertTrue('launchpad' in notification['Message-ID'])
     self.assertEqual(
         '\n'.join([
             '%s' % body,
             '-- ',
             'This message was sent from Launchpad by',
             'Me (http://launchpad.dev/~me)',
             'test reason.',
             'For more information see',
             'https://help.launchpad.net/YourAccount/ContactingPeople']),
         notification.get_payload())
 def test_add_doesnt_break_on_private_teams(self):
     # Since notifications are not exposed to UI, they should handle
     # protected preferred emails fine.
     email = self.factory.getUniqueEmailAddress()
     notified_team = self.factory.makeTeam(
         email=email, visibility=PersonVisibility.PRIVATE)
     recipients = NotificationRecipientSet()
     notifier = self.factory.makePerson()
     with person_logged_in(notifier):
         recipients.add([notified_team], 'some reason', 'some header')
     with celebrity_logged_in("admin"):
         self.assertEqual([notified_team], recipients.getRecipients())
Exemple #6
0
 def test_add_doesnt_break_on_private_teams(self):
     # Since notifications are not exposed to UI, they should handle
     # protected preferred emails fine.
     email = self.factory.getUniqueEmailAddress()
     notified_team = self.factory.makeTeam(
         email=email, visibility=PersonVisibility.PRIVATE)
     recipients = NotificationRecipientSet()
     notifier = self.factory.makePerson()
     with person_logged_in(notifier):
         recipients.add([notified_team], 'some reason', 'some header')
     with celebrity_logged_in("admin"):
         self.assertEqual([notified_team], recipients.getRecipients())
 def test_name_utf8_encoding(self):
     # Names are encoded in the From and To headers.
     self.factory.makePerson(email='*****@*****.**', displayname=u'sn\xefrf')
     user = self.factory.makePerson(
         email='*****@*****.**', displayname=u'pti\xedng')
     recipients_set = NotificationRecipientSet()
     recipients_set.add(user, 'test reason', 'test rationale')
     pop_notifications()
     send_direct_contact_email('*****@*****.**', recipients_set, 'test', 'test')
     notifications = pop_notifications()
     notification = notifications[0]
     self.assertEqual(
         '=?utf-8?b?c27Dr3Jm?= <*****@*****.**>', notification['From'])
     self.assertEqual(
         '=?utf-8?q?pti=C3=ADng?= <*****@*****.**>', notification['To'])
 def test_name_utf8_encoding(self):
     # Names are encoded in the From and To headers.
     self.factory.makePerson(email='*****@*****.**', displayname=u'sn\xefrf')
     user = self.factory.makePerson(email='*****@*****.**',
                                    displayname=u'pti\xedng')
     recipients_set = NotificationRecipientSet()
     recipients_set.add(user, 'test reason', 'test rationale')
     pop_notifications()
     send_direct_contact_email('*****@*****.**', recipients_set, 'test', 'test')
     notifications = pop_notifications()
     notification = notifications[0]
     self.assertEqual('=?utf-8?b?c27Dr3Jm?= <*****@*****.**>',
                      notification['From'])
     self.assertEqual('=?utf-8?q?pti=C3=ADng?= <*****@*****.**>',
                      notification['To'])
 def test_wrapping(self):
     self.factory.makePerson(email='*****@*****.**')
     user = self.factory.makePerson()
     recipients_set = NotificationRecipientSet()
     recipients_set.add(user, 'test reason', 'test rationale')
     pop_notifications()
     body = 'Can you help me? ' * 8
     send_direct_contact_email('*****@*****.**', recipients_set, 'subject', body)
     notifications = pop_notifications()
     body, footer = notifications[0].get_payload().split('-- ')
     self.assertEqual(
         'Can you help me? Can you help me? Can you help me? '
         'Can you help me? Can\n'
         'you help me? Can you help me? Can you help me? '
         'Can you help me?\n', body)
 def test_wrapping(self):
     self.factory.makePerson(email='*****@*****.**')
     user = self.factory.makePerson()
     recipients_set = NotificationRecipientSet()
     recipients_set.add(user, 'test reason', 'test rationale')
     pop_notifications()
     body = 'Can you help me? ' * 8
     send_direct_contact_email('*****@*****.**', recipients_set, 'subject', body)
     notifications = pop_notifications()
     body, footer = notifications[0].get_payload().split('-- ')
     self.assertEqual(
         'Can you help me? Can you help me? Can you help me? '
         'Can you help me? Can\n'
         'you help me? Can you help me? Can you help me? '
         'Can you help me?\n',
         body)
 def recipients(self):
     """See `IProductNotificationJob`."""
     maintainer = self.product.owner
     if maintainer.is_team:
         team_name = maintainer.displayname
         role = "an admin of %s which is the maintainer" % team_name
         users = maintainer.adminmembers
     else:
         role = "the maintainer"
         users = maintainer
     reason = (
         "You received this notification because you are %s of %s.\n%s" %
         (role, self.product.displayname, self.message_data['product_url']))
     header = 'Maintainer'
     notification_set = NotificationRecipientSet()
     notification_set.add(users, reason, header)
     return notification_set
Exemple #12
0
 def recipients(self):
     """See `IProductNotificationJob`."""
     maintainer = self.product.owner
     if maintainer.is_team:
         team_name = maintainer.displayname
         role = "an admin of %s which is the maintainer" % team_name
         users = maintainer.adminmembers
     else:
         role = "the maintainer"
         users = maintainer
     reason = (
         "You received this notification because you are %s of %s.\n%s" %
         (role, self.product.displayname, self.message_data['product_url']))
     header = 'Maintainer'
     notification_set = NotificationRecipientSet()
     notification_set.add(users, reason, header)
     return notification_set
Exemple #13
0
 def recipients(self):
     """See `IQuestionEmailJob`."""
     term = QuestionRecipientSet.getTermByToken(
         self.metadata['recipient_set'])
     question_recipient_set = term.value
     if question_recipient_set == QuestionRecipientSet.ASKER:
         recipients = NotificationRecipientSet()
         owner = self.question.owner
         original_recipients = self.question.direct_recipients
         for recipient in original_recipients:
             reason, header = original_recipients.getReason(recipient)
             if reason.subscriber == owner:
                 recipients.add(recipient, reason, header)
         return recipients
     elif question_recipient_set == QuestionRecipientSet.SUBSCRIBER:
         recipients = self.question.getRecipients()
         owner = self.question.owner
         asker_recipients = [
             recipient for recipient in recipients
             if recipients.getReason(recipient)[0].subscriber == owner
         ]
         recipients.remove(asker_recipients)
         return recipients
     elif question_recipient_set == QuestionRecipientSet.ASKER_SUBSCRIBER:
         return self.question.getRecipients()
     elif question_recipient_set == QuestionRecipientSet.CONTACT:
         return self.question.target.getAnswerContactRecipients(None)
     else:
         raise ValueError('Unsupported QuestionRecipientSet value: %s' %
                          question_recipient_set)
 def test_quota_reached_error(self):
     # An error is raised if the user has reached the daily quota.
     self.factory.makePerson(email='*****@*****.**', name='me')
     user = self.factory.makePerson(email='*****@*****.**', name='him')
     recipients_set = NotificationRecipientSet()
     old_message = self.factory.makeSignedMessage(email_address='*****@*****.**')
     authorization = IDirectEmailAuthorization(user)
     for action in xrange(authorization.message_quota):
         authorization.record(old_message)
     self.assertRaises(QuotaReachedError, send_direct_contact_email,
                       '*****@*****.**', recipients_set, 'subject', 'body')
    def __init__(self,
                 subject,
                 template_name,
                 recipients,
                 from_address,
                 delta=None,
                 message_id=None,
                 notification_type=None,
                 mail_controller_class=None):
        """Constructor.

        :param subject: A Python dict-replacement template for the subject
            line of the email.
        :param template: Name of the template to use for the message body.
        :param recipients: A dict of recipient to Subscription.
        :param from_address: The from_address to use on emails.
        :param delta: A Delta object with members "delta_values", "interface"
            and "new_values", such as BranchMergeProposalDelta.
        :param message_id: The Message-Id to use for generated emails.  If
            not supplied, random message-ids will be used.
        :param mail_controller_class: The class of the mail controller to
            use to send the mails.  Defaults to `MailController`.
        """
        self._subject_template = subject
        self._template_name = template_name
        self._recipients = NotificationRecipientSet()
        for recipient, reason in recipients.iteritems():
            self._recipients.add(recipient, reason, reason.mail_header)
        self.from_address = from_address
        self.delta = delta
        self.message_id = message_id
        self.notification_type = notification_type
        self.logger = logging.getLogger('lp.services.mail.basemailer')
        if mail_controller_class is None:
            mail_controller_class = MailController
        self._mail_controller_class = mail_controller_class
 def test_empty_recipient_set(self):
     # The recipient set can be empty. No messages are sent and the
     # action does not count toward the daily quota.
     self.factory.makePerson(email='*****@*****.**', name='me')
     user = self.factory.makePerson(email='*****@*****.**', name='him')
     recipients_set = NotificationRecipientSet()
     old_message = self.factory.makeSignedMessage(email_address='*****@*****.**')
     authorization = IDirectEmailAuthorization(user)
     for action in xrange(authorization.message_quota - 1):
         authorization.record(old_message)
     pop_notifications()
     send_direct_contact_email('*****@*****.**', recipients_set, 'subject',
                               'body')
     notifications = pop_notifications()
     self.assertEqual(0, len(notifications))
     self.assertTrue(authorization.is_allowed)
Exemple #17
0
class BaseMailer:
    """Base class for notification mailers.

    Subclasses must provide getReason (or reimplement _getTemplateParameters
    or generateEmail).

    It is expected that subclasses may override _getHeaders,
    _getTemplateParams, and perhaps _getBody.
    """

    app = None

    def __init__(self,
                 subject,
                 template_name,
                 recipients,
                 from_address,
                 delta=None,
                 message_id=None,
                 notification_type=None,
                 mail_controller_class=None,
                 request=None,
                 wrap=False,
                 force_wrap=False):
        """Constructor.

        :param subject: A Python dict-replacement template for the subject
            line of the email.
        :param template: Name of the template to use for the message body.
        :param recipients: A dict of recipient to Subscription.
        :param from_address: The from_address to use on emails.
        :param delta: A Delta object with members "delta_values", "interface"
            and "new_values", such as BranchMergeProposalDelta.
        :param message_id: The Message-Id to use for generated emails.  If
            not supplied, random message-ids will be used.
        :param mail_controller_class: The class of the mail controller to
            use to send the mails.  Defaults to `MailController`.
        :param request: An optional `IErrorReportRequest` to use when
            logging OOPSes.
        :param wrap: Wrap body text using `MailWrapper`.
        :param force_wrap: See `MailWrapper.format`.
        """
        # Running mail notifications with web security is too fragile: it's
        # easy to end up with subtle bugs due to such things as
        # subscriptions from private teams that are inaccessible to the user
        # with the current interaction.  BaseMailer always sends one mail
        # per recipient and thus never leaks information to other users, so
        # it's safer to require a permissive security policy.
        #
        # When converting other notification code to BaseMailer, it may be
        # necessary to move notifications into jobs, to move unit tests to a
        # Zopeless-based layer, or to use the permissive_security_policy
        # context manager.
        assert getSecurityPolicy() == LaunchpadPermissiveSecurityPolicy, (
            "BaseMailer may only be used with a permissive security policy.")

        self._subject_template = subject
        self._template_name = template_name
        self._recipients = NotificationRecipientSet()
        for recipient, reason in recipients.iteritems():
            self._recipients.add(recipient, reason, reason.mail_header)
        self.from_address = from_address
        self.delta = delta
        self.message_id = message_id
        self.notification_type = notification_type
        self.logger = logging.getLogger('lp.services.mail.basemailer')
        if mail_controller_class is None:
            mail_controller_class = MailController
        self._mail_controller_class = mail_controller_class
        self.request = request
        self._wrap = wrap
        self._force_wrap = force_wrap

    def _getFromAddress(self, email, recipient):
        return self.from_address

    def _getToAddresses(self, email, recipient):
        return [format_address(recipient.displayname, email)]

    def generateEmail(self, email, recipient, force_no_attachments=False):
        """Generate the email for this recipient.

        :param email: Email address of the recipient to send to.
        :param recipient: The Person to send to.
        :return: (headers, subject, body) of the email.
        """
        from_address = self._getFromAddress(email, recipient)
        to_addresses = self._getToAddresses(email, recipient)
        headers = self._getHeaders(email, recipient)
        subject = self._getSubject(email, recipient)
        body = self._getBody(email, recipient)
        expanded_footer = self._getExpandedFooter(headers, recipient)
        if expanded_footer:
            body = append_footer(body, expanded_footer)
        ctrl = self._mail_controller_class(from_address,
                                           to_addresses,
                                           subject,
                                           body,
                                           headers,
                                           envelope_to=[email])
        if force_no_attachments:
            ctrl.addAttachment('Excessively large attachments removed.',
                               content_type='text/plain',
                               inline=True)
        else:
            self._addAttachments(ctrl, email)
        return ctrl

    def _getSubject(self, email, recipient):
        """The subject template expanded with the template params."""
        return (self._subject_template %
                self._getTemplateParams(email, recipient))

    def _getReplyToAddress(self, email, recipient):
        """Return the address to use for the reply-to header."""
        return None

    def _getHeaders(self, email, recipient):
        """Return the mail headers to use."""
        reason, rationale = self._recipients.getReason(email)
        headers = OrderedDict()
        headers['X-Launchpad-Message-Rationale'] = reason.mail_header
        if reason.subscriber.name is not None:
            headers['X-Launchpad-Message-For'] = reason.subscriber.name
        if self.notification_type is not None:
            headers['X-Launchpad-Notification-Type'] = self.notification_type
        reply_to = self._getReplyToAddress(email, recipient)
        if reply_to is not None:
            headers['Reply-To'] = reply_to
        if self.message_id is not None:
            headers['Message-Id'] = self.message_id
        return headers

    def _addAttachments(self, ctrl, email):
        """Add any appropriate attachments to a MailController.

        Default implementation does nothing.
        :param ctrl: The MailController to add attachments to.
        :param email: The email address of the recipient.
        """
        pass

    def _getTemplateName(self, email, recipient):
        """Return the name of the template to use for this email body."""
        return self._template_name

    def _getTemplateParams(self, email, recipient):
        """Return a dict of values to use in the body and subject."""
        reason, rationale = self._recipients.getReason(email)
        params = {'reason': reason.getReason()}
        if self.delta is not None:
            params['delta'] = self.textDelta()
        return params

    def textDelta(self):
        """Return a textual version of the class delta."""
        return text_delta(self.delta, self.delta.delta_values,
                          self.delta.new_values, self.delta.interface)

    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

    def _getFooter(self, email, recipient, params):
        """Provide a footer to attach to the body, or None."""
        return None

    def _getExpandedFooter(self, headers, recipient):
        """Provide an expanded footer for recipients who have requested it."""
        if not recipient.expanded_notification_footers:
            return None
        lines = []
        for key, value in headers.items():
            if key.startswith('X-Launchpad-'):
                lines.append('%s: %s\n' % (key[2:], value))
        return ''.join(lines)

    def sendOne(self, email, recipient):
        """Send notification to one recipient."""
        # We never want SMTP errors to propagate from this function.
        ctrl = self.generateEmail(email, recipient)
        try:
            ctrl.send()
        except SMTPException:
            # If the initial sending failed, try again without
            # attachments.
            ctrl = self.generateEmail(email,
                                      recipient,
                                      force_no_attachments=True)
            try:
                ctrl.send()
            except SMTPException:
                error_utility = getUtility(IErrorReportingUtility)
                oops_vars = {
                    "message_id": ctrl.headers.get("Message-Id"),
                    "notification_type": self.notification_type,
                    "recipient": ", ".join(ctrl.to_addrs),
                    "subject": ctrl.subject,
                }
                with error_utility.oopsMessage(oops_vars):
                    oops = error_utility.raising(sys.exc_info(), self.request)
                self.logger.info("Mail resulted in OOPS: %s" % oops.get("id"))

    def sendAll(self):
        """Send notifications to all recipients."""
        for email, recipient in sorted(self._recipients.getRecipientPersons()):
            self.sendOne(email, recipient)
class BaseMailer:
    """Base class for notification mailers.

    Subclasses must provide getReason (or reimplement _getTemplateParameters
    or generateEmail).

    It is expected that subclasses may override _getHeaders,
    _getTemplateParams, and perhaps _getBody.
    """

    app = None

    def __init__(self,
                 subject,
                 template_name,
                 recipients,
                 from_address,
                 delta=None,
                 message_id=None,
                 notification_type=None,
                 mail_controller_class=None):
        """Constructor.

        :param subject: A Python dict-replacement template for the subject
            line of the email.
        :param template: Name of the template to use for the message body.
        :param recipients: A dict of recipient to Subscription.
        :param from_address: The from_address to use on emails.
        :param delta: A Delta object with members "delta_values", "interface"
            and "new_values", such as BranchMergeProposalDelta.
        :param message_id: The Message-Id to use for generated emails.  If
            not supplied, random message-ids will be used.
        :param mail_controller_class: The class of the mail controller to
            use to send the mails.  Defaults to `MailController`.
        """
        self._subject_template = subject
        self._template_name = template_name
        self._recipients = NotificationRecipientSet()
        for recipient, reason in recipients.iteritems():
            self._recipients.add(recipient, reason, reason.mail_header)
        self.from_address = from_address
        self.delta = delta
        self.message_id = message_id
        self.notification_type = notification_type
        self.logger = logging.getLogger('lp.services.mail.basemailer')
        if mail_controller_class is None:
            mail_controller_class = MailController
        self._mail_controller_class = mail_controller_class

    def _getToAddresses(self, recipient, email):
        return [format_address(recipient.displayname, email)]

    def generateEmail(self, email, recipient, force_no_attachments=False):
        """Generate the email for this recipient.

        :param email: Email address of the recipient to send to.
        :param recipient: The Person to send to.
        :return: (headers, subject, body) of the email.
        """
        to_addresses = self._getToAddresses(recipient, email)
        headers = self._getHeaders(email)
        subject = self._getSubject(email, recipient)
        body = self._getBody(email, recipient)
        ctrl = self._mail_controller_class(self.from_address,
                                           to_addresses,
                                           subject,
                                           body,
                                           headers,
                                           envelope_to=[email])
        if force_no_attachments:
            ctrl.addAttachment('Excessively large attachments removed.',
                               content_type='text/plain',
                               inline=True)
        else:
            self._addAttachments(ctrl, email)
        return ctrl

    def _getSubject(self, email, recipient):
        """The subject template expanded with the template params."""
        return (self._subject_template %
                self._getTemplateParams(email, recipient))

    def _getReplyToAddress(self):
        """Return the address to use for the reply-to header."""
        return None

    def _getHeaders(self, email):
        """Return the mail headers to use."""
        reason, rationale = self._recipients.getReason(email)
        headers = {'X-Launchpad-Message-Rationale': reason.mail_header}
        if self.notification_type is not None:
            headers['X-Launchpad-Notification-Type'] = self.notification_type
        reply_to = self._getReplyToAddress()
        if reply_to is not None:
            headers['Reply-To'] = reply_to
        if self.message_id is not None:
            headers['Message-Id'] = self.message_id
        return headers

    def _addAttachments(self, ctrl, email):
        """Add any appropriate attachments to a MailController.

        Default implementation does nothing.
        :param ctrl: The MailController to add attachments to.
        :param email: The email address of the recipient.
        """
        pass

    def _getTemplateParams(self, email, recipient):
        """Return a dict of values to use in the body and subject."""
        reason, rationale = self._recipients.getReason(email)
        params = {'reason': reason.getReason()}
        if self.delta is not None:
            params['delta'] = self.textDelta()
        return params

    def textDelta(self):
        """Return a textual version of the class delta."""
        return text_delta(self.delta, self.delta.delta_values,
                          self.delta.new_values, self.delta.interface)

    def _getBody(self, email, recipient):
        """Return the complete body to use for this email."""
        template = get_email_template(self._template_name, app=self.app)
        params = self._getTemplateParams(email, recipient)
        body = template % params
        footer = self._getFooter(params)
        if footer is not None:
            body = append_footer(body, footer)
        return body

    def _getFooter(self, params):
        """Provide a footer to attach to the body, or None."""
        return None

    def sendAll(self):
        """Send notifications to all recipients."""
        # We never want SMTP errors to propagate from this function.
        for email, recipient in self._recipients.getRecipientPersons():
            try:
                ctrl = self.generateEmail(email, recipient)
                ctrl.send()
            except SMTPException as e:
                # If the initial sending failed, try again without
                # attachments.
                try:
                    ctrl = self.generateEmail(email,
                                              recipient,
                                              force_no_attachments=True)
                    ctrl.send()
                except SMTPException as e:
                    # Don't want an entire stack trace, just some details.
                    self.logger.warning('send failed for %s, %s' % (email, e))