mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-22 16:31:15 -05:00
amazon SES only accepts domains encoded in punycode, an encoding that converts utf-8 into an ascii encoding that browsers and mailservers recognise. We currently just send through emails as we store them (in full unicode), which means any rogue characters break SES and cause us to put the email in technical-failure. Most of these appear to be typos and rogue control characters, but there is a small chance that it could be a real domain (eg https://🅂𝖍𝐤ₛᵖ𝒓.ⓜ𝕠𝒃𝓲/🆆🆃🅵/). We should encode to and reply-to-address emails as punycode to make sure that they can always be sent. The chance that anyone actually uses a unicode domain name for their email is probably pretty low, but we should allow it for completeness.
125 lines
4.0 KiB
Python
125 lines
4.0 KiB
Python
import boto3
|
|
import botocore
|
|
from flask import current_app
|
|
from time import monotonic
|
|
from notifications_utils.recipients import InvalidEmailError
|
|
|
|
from app.clients import STATISTICS_DELIVERED, STATISTICS_FAILURE
|
|
from app.clients.email import (EmailClientException, EmailClient)
|
|
|
|
ses_response_map = {
|
|
'Permanent': {
|
|
"message": 'Hard bounced',
|
|
"success": False,
|
|
"notification_status": 'permanent-failure',
|
|
"notification_statistics_status": STATISTICS_FAILURE
|
|
},
|
|
'Temporary': {
|
|
"message": 'Soft bounced',
|
|
"success": False,
|
|
"notification_status": 'temporary-failure',
|
|
"notification_statistics_status": STATISTICS_FAILURE
|
|
},
|
|
'Delivery': {
|
|
"message": 'Delivered',
|
|
"success": True,
|
|
"notification_status": 'delivered',
|
|
"notification_statistics_status": STATISTICS_DELIVERED
|
|
},
|
|
'Complaint': {
|
|
"message": 'Complaint',
|
|
"success": True,
|
|
"notification_status": 'delivered',
|
|
"notification_statistics_status": STATISTICS_DELIVERED
|
|
}
|
|
}
|
|
|
|
|
|
def get_aws_responses(status):
|
|
return ses_response_map[status]
|
|
|
|
|
|
class AwsSesClientException(EmailClientException):
|
|
pass
|
|
|
|
|
|
class AwsSesClient(EmailClient):
|
|
'''
|
|
Amazon SES email client.
|
|
'''
|
|
|
|
def init_app(self, region, statsd_client, *args, **kwargs):
|
|
self._client = boto3.client('ses', region_name=region)
|
|
super(AwsSesClient, self).__init__(*args, **kwargs)
|
|
self.name = 'ses'
|
|
self.statsd_client = statsd_client
|
|
|
|
def get_name(self):
|
|
return self.name
|
|
|
|
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 punycode_encode_email(email_address):
|
|
# only the hostname should ever be punycode encoded.
|
|
local, hostname = email_address.split('@')
|
|
return '{}@{}'.format(local, hostname.encode('idna').decode('utf-8'))
|