def send_email(self, source, to_addresses, subject, body, html_body='', reply_to_address=None): try: if isinstance(to_addresses, str): to_addresses = [to_addresses] reply_to_addresses = [reply_to_address] if reply_to_address else [] body = { 'Text': {'Data': body} } if html_body: body.update({ 'Html': {'Data': html_body} }) start_time = monotonic() response = self._client.send_email( Source=source, Destination={ 'ToAddresses': [punycode_encode_email(addr) for addr in to_addresses], 'CcAddresses': [], 'BccAddresses': [] }, Message={ 'Subject': { 'Data': subject, }, 'Body': body }, ReplyToAddresses=[punycode_encode_email(addr) for addr in reply_to_addresses] ) except botocore.exceptions.ClientError as e: self.statsd_client.incr("clients.ses.error") # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html if e.response['Error']['Code'] == 'InvalidParameterValue': raise InvalidEmailError('email: "{}" message: "{}"'.format( to_addresses[0], e.response['Error']['Message'] )) else: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) except Exception as e: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) else: elapsed_time = monotonic() - start_time current_app.logger.info("AWS SES request finished in {}".format(elapsed_time)) self.statsd_client.timing("clients.ses.request-time", elapsed_time) self.statsd_client.incr("clients.ses.success") return response['MessageId']
def test_should_technical_error_and_not_retry_if_invalid_email( sample_notification, mocker): mocker.patch('app.delivery.send_to_providers.send_email_to_provider', side_effect=InvalidEmailError('bad email')) mocker.patch('app.celery.provider_tasks.deliver_email.retry') deliver_email(sample_notification.id) assert provider_tasks.deliver_email.retry.called is False assert sample_notification.status == 'technical-failure'
def send_email(self, source, to_addresses, subject, body, html_body, reply_to_address=None, attachments=[]): try: if isinstance(to_addresses, str): to_addresses = [to_addresses] # Sometimes the source is "Foo <*****@*****.**> vs just [email protected]" # TODO: Possibly revisit this to take in sender name and sender email address separately if "<" in source: source = source.split("<")[1].split(">")[0] recipients = [ {"email": to_address} for to_address in to_addresses ] payload = { "subject": subject, "body": html_body, "recipients": recipients, "from_email": source, "click_tracking_enabled": False } start_time = monotonic() response = requests.post( self.govdelivery_url, json=payload, headers={ "X-AUTH-TOKEN": self.token } ) response.raise_for_status() except HTTPError as e: self.statsd_client.incr("clients.govdelivery.error") if e.response.status_code == 422: raise InvalidEmailError(str(e)) else: raise GovdeliveryClientException(str(e)) except Exception as e: self.statsd_client.incr("clients.govdelivery.error") raise GovdeliveryClientException(str(e)) else: elapsed_time = monotonic() - start_time current_app.logger.info("Govdelivery request finished in {}".format(elapsed_time)) self.statsd_client.timing("clients.govdelivery.request-time", elapsed_time) self.statsd_client.incr("clients.govdelivery.success") return response.json()["id"]
def _check_error_code(self, e, to_addresses): # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html if e.response['Error']['Code'] == 'InvalidParameterValue': self.statsd_client.incr("clients.ses.error.invalid-email") raise InvalidEmailError('message: "{}"'.format( e.response['Error']['Message'])) elif (e.response['Error']['Code'] == 'Throttling' and e.response['Error']['Message'] == 'Maximum sending rate exceeded.'): self.statsd_client.incr("clients.ses.error.throttling") raise AwsSesClientThrottlingSendRateException(str(e)) else: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e))
def test_should_technical_error_and_not_retry_if_invalid_email( sample_notification, mocker): mocker.patch( "app.delivery.send_to_providers.send_email_to_provider", side_effect=InvalidEmailError("bad email"), ) mocker.patch("app.celery.provider_tasks.deliver_email.retry") logger = mocker.patch("app.celery.provider_tasks.current_app.logger.info") queued_callback = mocker.patch( "app.celery.provider_tasks._check_and_queue_callback_task") deliver_email(sample_notification.id) assert provider_tasks.deliver_email.retry.called is False assert sample_notification.status == "technical-failure" assert (call( f"Cannot send notification {sample_notification.id}, got an invalid email address: bad email." ) in logger.call_args_list) queued_callback.assert_called_once_with(sample_notification)
def send_email(self, source, to_addresses, subject, body, html_body='', reply_to_address=None, attachments=[]): try: if isinstance(to_addresses, str): to_addresses = [to_addresses] source = unidecode(source) reply_to_addresses = [reply_to_address] if reply_to_address else [] multipart_content_subtype = 'alternative' if html_body else 'mixed' msg = MIMEMultipart(multipart_content_subtype) msg['Subject'] = subject msg['From'] = source msg['To'] = ",".join( [punycode_encode_email(addr) for addr in to_addresses]) if reply_to_addresses != []: msg.add_header( 'reply-to', ",".join([ punycode_encode_email(addr) for addr in reply_to_addresses ])) part = MIMEText(body, 'plain') msg.attach(part) if html_body: part = MIMEText(html_body, 'html') msg.attach(part) for attachment in attachments or []: part = MIMEApplication(attachment["data"]) part.add_header('Content-Disposition', 'attachment', filename=attachment["name"]) msg.attach(part) start_time = monotonic() response = self._client.send_raw_email( Source=source, RawMessage={'Data': msg.as_string()}) except botocore.exceptions.ClientError as e: self.statsd_client.incr("clients.ses.error") # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html if e.response['Error']['Code'] == 'InvalidParameterValue': raise InvalidEmailError('email: "{}" message: "{}"'.format( to_addresses[0], e.response['Error']['Message'])) else: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) except Exception as e: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) else: elapsed_time = monotonic() - start_time current_app.logger.info( "AWS SES request finished in {}".format(elapsed_time)) self.statsd_client.timing("clients.ses.request-time", elapsed_time) self.statsd_client.incr("clients.ses.success") return response['MessageId']
def send_email(self, source, sending_domain, to_addresses, subject, body, html_body='', reply_to_address=None, attachments=[], importance=None, cc_addresses=None): try: aws_ses_owner_account = current_app.config['AWS_SES_OWNER_ACCOUNT'] aws_ses_arn = 'arn:aws:ses:{}:{}:identity/{}'.format( current_app.config['AWS_SES_REGION'], aws_ses_owner_account, sending_domain) if aws_ses_owner_account else None if isinstance(to_addresses, str): to_addresses = [to_addresses] if isinstance(cc_addresses, str): cc_addresses = [cc_addresses] source = unicodedata.normalize('NFKD', source) friendly_name, match_string, from_address = source.partition("<") friendly_name = friendly_name.replace('"', '') h = Header(friendly_name, 'utf-8') encoded_friendly_name = h.encode() encoded_source = '{} {}{}'.format(encoded_friendly_name, match_string, from_address) reply_to_addresses = [reply_to_address] if reply_to_address else [] multipart_content_subtype = 'alternative' if html_body else 'mixed' msg = MIMEMultipart(multipart_content_subtype) msg['Subject'] = subject msg['From'] = encoded_source msg['To'] = ",".join([punycode_encode_email(addr) for addr in to_addresses]) if aws_ses_arn: msg.add_header('X-SES-SOURCE-ARN', aws_ses_arn) msg.add_header('X-SES-FROM-ARN', aws_ses_arn) if importance: msg.add_header('importance', importance) if cc_addresses: msg['CC'] = ",".join([punycode_encode_email(addr) for addr in cc_addresses]) if reply_to_addresses != []: msg.add_header('reply-to', ",".join([punycode_encode_email(addr) for addr in reply_to_addresses])) part = MIMEText(body.encode(self.charset), 'plain', self.charset) msg.attach(part) if html_body: part = MIMEText(html_body.encode(self.charset), 'html', self.charset) msg.attach(part) for attachment in attachments or []: part = MIMEApplication(attachment["data"]) part.add_header('Content-Disposition', 'attachment', filename=attachment["name"]) msg.attach(part) start_time = monotonic() response = self._client.send_raw_email( Source=source, RawMessage={'Data': msg.as_string()} ) except botocore.exceptions.ClientError as e: self.statsd_client.incr("clients.ses.error") # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html if e.response['Error']['Code'] == 'InvalidParameterValue': raise InvalidEmailError('email: "{}" message: "{}"'.format( to_addresses[0], e.response['Error']['Message'] )) else: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) except Exception as e: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) else: elapsed_time = monotonic() - start_time current_app.logger.info("AWS SES request finished in {}".format(elapsed_time)) self.statsd_client.timing("clients.ses.request-time", elapsed_time) self.statsd_client.incr("clients.ses.success") return response['MessageId']
def send_email( self, source, to_addresses, subject, body, html_body="", reply_to_address=None, attachments=None, ): def create_mime_base(attachments, html): msg_type = "mixed" if attachments or (not attachments and not html) else "alternative" ret = MIMEMultipart(msg_type) ret["Subject"] = subject ret["From"] = source ret["To"] = ",".join([punycode_encode_email(addr) for addr in to_addresses]) if reply_to_addresses: ret.add_header( "reply-to", ",".join([punycode_encode_email(addr) for addr in reply_to_addresses]), ) return ret def attach_html(m, content): if content: parts = MIMEText(content, "html") m.attach(parts) attachments = attachments or [] if isinstance(to_addresses, str): to_addresses = [to_addresses] source = unidecode(source) reply_to_addresses = [reply_to_address] if reply_to_address else [] # - If sending a TXT email without attachments: # => Multipart mixed # # - If sending a TXT + HTML email without attachments: # => Multipart alternative # # - If sending a TXT + HTML email with attachments # => Multipart Mixed (enclosing) # - Multipart alternative # - TXT # - HTML # - Attachment(s) try: msg = create_mime_base(attachments, html_body) txt_part = MIMEText(body, "plain") if attachments and html_body: msg_alternative = MIMEMultipart("alternative") msg_alternative.attach(txt_part) attach_html(msg_alternative, html_body) msg.attach(msg_alternative) else: msg.attach(txt_part) attach_html(msg, html_body) for attachment in attachments: # See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-raw.html#send-email-raw-mime attachment_part = MIMEApplication(attachment["data"]) if attachment.get("mime_type"): attachment_part.add_header("Content-Type", attachment["mime_type"], name=attachment["name"]) attachment_part.add_header("Content-Disposition", "attachment", filename=attachment["name"]) msg.attach(attachment_part) start_time = monotonic() response = self._client.send_raw_email(Source=source, RawMessage={"Data": msg.as_string()}) except botocore.exceptions.ClientError as e: self.statsd_client.incr("clients.ses.error") # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html if e.response["Error"]["Code"] == "InvalidParameterValue": raise InvalidEmailError('email: "{}" message: "{}"'.format(to_addresses[0], e.response["Error"]["Message"])) else: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) except Exception as e: self.statsd_client.incr("clients.ses.error") raise AwsSesClientException(str(e)) else: elapsed_time = monotonic() - start_time current_app.logger.info("AWS SES request finished in {}".format(elapsed_time)) self.statsd_client.timing("clients.ses.request-time", elapsed_time) self.statsd_client.incr("clients.ses.success") return response["MessageId"]