mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-22 08:21:13 -05:00
Previously we used a combination of "provider.name" and "get_name()" which was confusing. Using a non-property function also gave me the impression that the name was more dynamic than it actually is.
135 lines
4.2 KiB
Python
135 lines
4.2 KiB
Python
from time import monotonic
|
|
|
|
import boto3
|
|
import botocore
|
|
from flask import current_app
|
|
|
|
from app.clients import STATISTICS_DELIVERED, STATISTICS_FAILURE
|
|
from app.clients.email import (
|
|
EmailClient,
|
|
EmailClientException,
|
|
EmailClientNonRetryableException,
|
|
)
|
|
|
|
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 AwsSesClientThrottlingSendRateException(AwsSesClientException):
|
|
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.statsd_client = statsd_client
|
|
|
|
@property
|
|
def name(self):
|
|
return 'ses'
|
|
|
|
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 EmailClientNonRetryableException(e.response['Error']['Message'])
|
|
elif (
|
|
e.response['Error']['Code'] == 'Throttling'
|
|
and e.response['Error']['Message'] == 'Maximum sending rate exceeded.'
|
|
):
|
|
raise AwsSesClientThrottlingSendRateException(str(e))
|
|
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'))
|