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()')
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))
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
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
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
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))
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)
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)