Exemplo n.º 1
0
    def deliver(self, message, rendered_message):
        # Compress spaces and remove newlines to make it easier to author templates.
        subject = re.sub(u'\\s+', u' ', rendered_message.subject, re.UNICODE).strip()
        default_from_address = getattr(settings, u'DEFAULT_FROM_EMAIL', None)
        reply_to = message.options.get(u'reply_to', None)
        from_address = message.options.get(u'from_address', default_from_address)
        if not from_address:
            raise FatalChannelDeliveryError(
                u'from_address must be included in message delivery options or as the DEFAULT_FROM_EMAIL settings'
            )

        rendered_template = TEMPLATE.format(
            head_html=rendered_message.head_html,
            body_html=rendered_message.body_html,
        )
        try:
            mail = EmailMultiAlternatives(
                subject=subject,
                body=rendered_message.body,
                from_email=from_address,
                to=[message.recipient.email_address],
                reply_to=reply_to,
            )

            mail.attach_alternative(rendered_template, u'text/html')
            mail.send()
        except SMTPException as e:
            LOG.exception(e)
            raise FatalChannelDeliveryError(u'An SMTP error occurred (and logged) from Django send_email()')
Exemplo n.º 2
0
    def _handle_error_response(self, response):
        u"""
        Handle an error response from SailThru, either by retrying or failing
        with an appropriate exception.

        Arguments:
            response: The HTTP response recieved from SailThru.
        """
        error = response.get_error()
        error_code = error.get_error_code()
        error_message = error.get_message()
        http_status_code = response.get_status_code()
        if error_code in RecoverableErrorCodes:
            next_attempt_time = None
            if error_code == RecoverableErrorCodes.RATE_LIMIT:
                next_attempt_time = self._get_rate_limit_reset_time(
                    sailthru_response=response)

            if next_attempt_time is None:
                # Sailthru advises waiting "a moment" and then trying again.
                next_attempt_time = get_current_time() + timedelta(
                    seconds=NEXT_ATTEMPT_DELAY_SECONDS + random.uniform(-2, 2))

            raise RecoverableChannelDeliveryError(
                u'Recoverable Sailthru error (error_code={error_code} status_code={http_status_code}): '
                u'{message}'.format(error_code=error_code,
                                    http_status_code=http_status_code,
                                    message=error_message), next_attempt_time)
        else:
            raise FatalChannelDeliveryError(
                u'Fatal Sailthru error (error_code={error_code} status_code={http_status_code}): '
                u'{message}'.format(error_code=error_code,
                                    http_status_code=http_status_code,
                                    message=error_message))
Exemplo n.º 3
0
 def get_from_address(message):
     """Grabs the from_address from the message with fallback and error handling"""
     default_from_address = getattr(settings, 'DEFAULT_FROM_EMAIL', None)
     from_address = message.options.get('from_address',
                                        default_from_address)
     if not from_address:
         raise FatalChannelDeliveryError(
             'from_address must be included in message delivery options or as the DEFAULT_FROM_EMAIL settings'
         )
     return from_address
Exemplo n.º 4
0
    def _handle_error_response(self, response, message, exception):
        """
        Handle an error response from Braze, either by retrying or failing
        with an appropriate exception.

        Arguments:
            response: The HTTP response received from Braze.
            message: An error message from Braze.
            exception: The exception that triggered this error.
        """
        if response.status_code == 429 or 500 <= response.status_code < 600:
            next_attempt_time = get_current_time() + timedelta(
                seconds=NEXT_ATTEMPT_DELAY_SECONDS + random.uniform(-2, 2))
            raise RecoverableChannelDeliveryError(
                'Recoverable Braze error (status_code={http_status_code}): {message}'
                .format(http_status_code=response.status_code,
                        message=message), next_attempt_time) from exception

        raise FatalChannelDeliveryError(
            'Fatal Braze error (status_code={http_status_code}): {message}'.
            format(http_status_code=response.status_code,
                   message=message)) from exception
Exemplo n.º 5
0
    def deliver(self, message, rendered_message):
        subject = self.get_subject(rendered_message)
        from_address = self.get_from_address(message)
        reply_to = message.options.get('reply_to', None)

        rendered_template = self.make_simple_html_template(
            rendered_message.head_html, rendered_message.body_html)
        try:
            mail = EmailMultiAlternatives(
                subject=subject,
                body=rendered_message.body,
                from_email=from_address,
                to=[message.recipient.email_address],
                reply_to=reply_to,
            )

            mail.attach_alternative(rendered_template, 'text/html')
            mail.send()
        except SMTPException as e:
            LOG.exception(e)
            raise FatalChannelDeliveryError(
                'An SMTP error occurred (and logged) from Django send_email()'
            ) from e
Exemplo n.º 6
0
    def deliver(self, message, rendered_message):
        if message.recipient.email_address is None:
            raise InvalidMessageError(
                u'No email address specified for recipient %s while sending message %s',
                message.recipient,
                message.log_id
            )

        template_vars = {}
        for key, value in six.iteritems(attr.asdict(rendered_message)):
            if value is not None:
                # Sailthru will silently fail to send the email if the from name or subject line contain new line
                # characters at the beginning or end of the string
                template_vars[u'ace_template_' + key] = value.strip()

        logger = message.get_message_specific_logger(LOG)

        if getattr(settings, u'ACE_CHANNEL_SAILTHRU_DEBUG', False):
            logger.info(
                # TODO(later): Do our splunk parsers do the right thing with multi-line log messages like this?
                textwrap.dedent(u"""\
                    Would have emailed using:
                        template: %s
                        recipient: %s
                        variables: %s
                """),
                self.template_name,
                message.recipient.email_address,
                six.text_type(template_vars),
            )
            return

        if not self.enabled():
            raise FatalChannelDeliveryError(
                textwrap.dedent(u"""\
                    Sailthru channel is disabled, unable to send:
                        template: %s
                        recipient: %s
                        variables: %s
                """),
                self.template_name,
                message.recipient.email_address,
                six.text_type(template_vars),
            )

        try:
            logger.debug(u'Sending to Sailthru')

            response = self.sailthru_client.send(
                self.template_name,
                message.recipient.email_address,
                _vars=template_vars,
            )

            if response.is_ok():
                logger.debug(u'Successfully send to Sailthru')
                # TODO(later): emit some sort of analytics event?
                return
            else:
                logger.debug(u'Failed to send to Sailthru')
                self._handle_error_response(response)

        except SailthruClientError as exc:
            raise FatalChannelDeliveryError(u'Unable to communicate with the Sailthru API: ' + six.text_type(exc))
Exemplo n.º 7
0
 def test_fatal_error(self):
     self.mock_channel.deliver.side_effect = FatalChannelDeliveryError(
         u'testing')
     with self.assertRaises(FatalChannelDeliveryError):
         deliver(self.mock_channel, sentinel.rendered_email, self.message)
Exemplo n.º 8
0
    def deliver(self, message, rendered_message):
        if not self.enabled():
            raise FatalChannelDeliveryError(
                'Braze channel is disabled, unable to send')

        if not message.recipient.lms_user_id:
            # This channel assumes that you have Braze configured with LMS user_ids as your external_user_id in Braze.
            # Unfortunately, that means that we can't send emails to users that aren't registered with the LMS,
            # which some callers of ACE may attempt to do (despite the lms_user_id being a required Recipient field).
            # In these cases, we fall back to a simple Django smtp email.
            DjangoEmailChannel().deliver(message, rendered_message)
            return

        transactional = message.options.get('transactional', False)
        body_html = self.make_simple_html_template(rendered_message.head_html,
                                                   rendered_message.body_html)

        # Allow our settings to override the from address, because Braze requires specific configured from addresses,
        # which are tied to specific ip addresses that are "ip warmed" to help delivery of the emails not get sent
        # to promotional/spam inboxes.
        from_address = getattr(settings, self._FROM_EMAIL_SETTING,
                               None) or self.get_from_address(message)

        logger = message.get_message_specific_logger(LOG)
        logger.debug('Sending to Braze')

        # https://www.braze.com/docs/api/endpoints/messaging/send_messages/post_send_messages/
        # https://www.braze.com/docs/api/objects_filters/email_object/
        response = requests.post(
            self._send_url(),
            headers=self._auth_headers(),
            json={
                'external_user_ids': [str(message.recipient.lms_user_id)],
                'recipient_subscription_state':
                'all' if transactional else 'subscribed',
                'campaign_id': self._campaign_id(message.name),
                'messages': {
                    'email': {
                        'app_id': getattr(settings, self._APP_ID_SETTING),
                        'subject': self.get_subject(rendered_message),
                        'from': from_address,
                        'reply_to': message.options.get('reply_to'),
                        'body': body_html,
                        'plaintext_body': rendered_message.body,
                        'message_variation_id':
                        self._variation_id(message.name),
                        'should_inline_css':
                        False,  # this feature messes with inline CSS already in ACE templates
                    },
                },
            },
        )

        try:
            response.raise_for_status()
            logger.debug('Successfully sent to Braze (dispatch ID %s)',
                         response.json()['dispatch_id'])

        except requests.exceptions.HTTPError as exc:
            # https://www.braze.com/docs/api/errors/
            message = response.json().get('message', 'Unknown error')
            logger.debug('Failed to send to Braze: %s', message)
            self._handle_error_response(response, message, exc)