Exemple #1
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)
Exemple #2
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))