2021-03-10 13:55:06 +00:00
|
|
|
from time import monotonic
|
|
|
|
|
|
2016-02-17 17:48:23 +00:00
|
|
|
import boto3
|
2016-10-13 15:27:47 +01:00
|
|
|
import botocore
|
2016-03-02 09:33:20 +00:00
|
|
|
from flask import current_app
|
2016-10-13 15:27:47 +01:00
|
|
|
|
2016-04-06 16:34:45 +01:00
|
|
|
from app.clients import STATISTICS_DELIVERED, STATISTICS_FAILURE
|
2021-03-10 13:55:06 +00:00
|
|
|
from app.clients.email import (
|
|
|
|
|
EmailClient,
|
|
|
|
|
EmailClientException,
|
|
|
|
|
EmailClientNonRetryableException,
|
|
|
|
|
)
|
2016-02-17 17:48:23 +00:00
|
|
|
|
2016-04-06 16:34:45 +01:00
|
|
|
ses_response_map = {
|
2016-05-17 15:38:49 +01:00
|
|
|
'Permanent': {
|
|
|
|
|
"message": 'Hard bounced',
|
2016-04-06 16:34:45 +01:00
|
|
|
"success": False,
|
2016-05-17 15:38:49 +01:00
|
|
|
"notification_status": 'permanent-failure',
|
|
|
|
|
"notification_statistics_status": STATISTICS_FAILURE
|
|
|
|
|
},
|
|
|
|
|
'Temporary': {
|
|
|
|
|
"message": 'Soft bounced',
|
|
|
|
|
"success": False,
|
|
|
|
|
"notification_status": 'temporary-failure',
|
2016-04-06 16:34:45 +01:00
|
|
|
"notification_statistics_status": STATISTICS_FAILURE
|
|
|
|
|
},
|
|
|
|
|
'Delivery': {
|
|
|
|
|
"message": 'Delivered',
|
|
|
|
|
"success": True,
|
|
|
|
|
"notification_status": 'delivered',
|
|
|
|
|
"notification_statistics_status": STATISTICS_DELIVERED
|
|
|
|
|
},
|
|
|
|
|
'Complaint': {
|
|
|
|
|
"message": 'Complaint',
|
2016-04-08 16:13:10 +01:00
|
|
|
"success": True,
|
|
|
|
|
"notification_status": 'delivered',
|
|
|
|
|
"notification_statistics_status": STATISTICS_DELIVERED
|
2016-04-06 16:34:45 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_aws_responses(status):
|
|
|
|
|
return ses_response_map[status]
|
2016-03-10 17:29:17 +00:00
|
|
|
|
2016-02-17 17:48:23 +00:00
|
|
|
|
|
|
|
|
class AwsSesClientException(EmailClientException):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2020-08-13 17:18:19 +01:00
|
|
|
class AwsSesClientThrottlingSendRateException(AwsSesClientException):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2016-02-17 17:48:23 +00:00
|
|
|
class AwsSesClient(EmailClient):
|
|
|
|
|
'''
|
|
|
|
|
Amazon SES email client.
|
|
|
|
|
'''
|
|
|
|
|
|
2016-05-13 17:15:39 +01:00
|
|
|
def init_app(self, region, statsd_client, *args, **kwargs):
|
2016-02-17 17:48:23 +00:00
|
|
|
self._client = boto3.client('ses', region_name=region)
|
|
|
|
|
super(AwsSesClient, self).__init__(*args, **kwargs)
|
2016-02-25 11:23:04 +00:00
|
|
|
self.name = 'ses'
|
2016-05-13 17:15:39 +01:00
|
|
|
self.statsd_client = statsd_client
|
2016-02-25 11:23:04 +00:00
|
|
|
|
add raw request timings to provider send functions
we're using statsd to monitor how long provider requests are taking.
However, there's lots of busy work that happens inside our statsd
metrics timing window. Things like json dumping and loading, building
headers, exception handling, etc.
for firetext/mmg, the response object from requests has an elapsed
property [1], which captures from sending raw data to parsing the
response headers. for ses, it's a bit trickier, but boto3 exposes a few
event hooks [2]. it's hard to find them without stepping through the
code, but the interesting ones are before-call, after-call,
after-call-error, request-created, and response-received. The
before-call and after-call involve some marshalling, built-in retrying,
etc, while request-created and response-received are much lower level.
They might be called more than once per ses request, if boto3 itself
retries the request on 5xx, 429 and low level socket errors [3].
Add these as new `raw-request-time` metrics rather than overwriting to
avoid changing the meaning of an existing metric, and to let us compare
the metrics to see if there's a noticeable difference at all
[1] https://requests.readthedocs.io/en/master/api/#requests.Response.elapsed
[2] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/events.html
[3] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode
2020-05-29 13:56:29 +01:00
|
|
|
# events are generally undocumented, but some that might be of interest are:
|
|
|
|
|
# before-call, after-call, after-call-error, request-created, response-received
|
2020-12-07 15:15:50 +00:00
|
|
|
self._client.meta.events.register('request-created.ses.SendEmail', self.ses_request_created_hook)
|
|
|
|
|
self._client.meta.events.register('response-received.ses.SendEmail', self.ses_response_received_hook)
|
add raw request timings to provider send functions
we're using statsd to monitor how long provider requests are taking.
However, there's lots of busy work that happens inside our statsd
metrics timing window. Things like json dumping and loading, building
headers, exception handling, etc.
for firetext/mmg, the response object from requests has an elapsed
property [1], which captures from sending raw data to parsing the
response headers. for ses, it's a bit trickier, but boto3 exposes a few
event hooks [2]. it's hard to find them without stepping through the
code, but the interesting ones are before-call, after-call,
after-call-error, request-created, and response-received. The
before-call and after-call involve some marshalling, built-in retrying,
etc, while request-created and response-received are much lower level.
They might be called more than once per ses request, if boto3 itself
retries the request on 5xx, 429 and low level socket errors [3].
Add these as new `raw-request-time` metrics rather than overwriting to
avoid changing the meaning of an existing metric, and to let us compare
the metrics to see if there's a noticeable difference at all
[1] https://requests.readthedocs.io/en/master/api/#requests.Response.elapsed
[2] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/events.html
[3] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode
2020-05-29 13:56:29 +01:00
|
|
|
|
|
|
|
|
def ses_request_created_hook(self, **kwargs):
|
|
|
|
|
# request created may be called multiple times if the request auto-retries. We want to count all these as the
|
|
|
|
|
# same request for timing purposes, so only reset the start time if it was cleared completely
|
|
|
|
|
if self.ses_start_time == 0:
|
|
|
|
|
self.ses_start_time = monotonic()
|
|
|
|
|
|
|
|
|
|
def ses_response_received_hook(self, **kwargs):
|
|
|
|
|
# response received may be called multiple times if the request auto-retries, however, we want to count the last
|
|
|
|
|
# time it triggers for timing purposes, so always reset the elapsed time
|
|
|
|
|
self.ses_elapsed_time = monotonic() - self.ses_start_time
|
|
|
|
|
|
2016-02-25 11:23:04 +00:00
|
|
|
def get_name(self):
|
|
|
|
|
return self.name
|
2016-02-17 17:48:23 +00:00
|
|
|
|
|
|
|
|
def send_email(self,
|
|
|
|
|
source,
|
|
|
|
|
to_addresses,
|
|
|
|
|
subject,
|
|
|
|
|
body,
|
2016-03-18 11:47:01 +00:00
|
|
|
html_body='',
|
2016-07-04 14:47:43 +01:00
|
|
|
reply_to_address=None):
|
add raw request timings to provider send functions
we're using statsd to monitor how long provider requests are taking.
However, there's lots of busy work that happens inside our statsd
metrics timing window. Things like json dumping and loading, building
headers, exception handling, etc.
for firetext/mmg, the response object from requests has an elapsed
property [1], which captures from sending raw data to parsing the
response headers. for ses, it's a bit trickier, but boto3 exposes a few
event hooks [2]. it's hard to find them without stepping through the
code, but the interesting ones are before-call, after-call,
after-call-error, request-created, and response-received. The
before-call and after-call involve some marshalling, built-in retrying,
etc, while request-created and response-received are much lower level.
They might be called more than once per ses request, if boto3 itself
retries the request on 5xx, 429 and low level socket errors [3].
Add these as new `raw-request-time` metrics rather than overwriting to
avoid changing the meaning of an existing metric, and to let us compare
the metrics to see if there's a noticeable difference at all
[1] https://requests.readthedocs.io/en/master/api/#requests.Response.elapsed
[2] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/events.html
[3] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode
2020-05-29 13:56:29 +01:00
|
|
|
self.ses_elapsed_time = 0
|
|
|
|
|
self.ses_start_time = 0
|
2016-02-17 17:48:23 +00:00
|
|
|
try:
|
|
|
|
|
if isinstance(to_addresses, str):
|
|
|
|
|
to_addresses = [to_addresses]
|
2016-07-04 14:47:43 +01:00
|
|
|
|
|
|
|
|
reply_to_addresses = [reply_to_address] if reply_to_address else []
|
2016-02-17 17:48:23 +00:00
|
|
|
|
2016-03-18 11:47:01 +00:00
|
|
|
body = {
|
|
|
|
|
'Text': {'Data': body}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if html_body:
|
|
|
|
|
body.update({
|
|
|
|
|
'Html': {'Data': html_body}
|
|
|
|
|
})
|
|
|
|
|
|
2016-03-02 09:33:20 +00:00
|
|
|
start_time = monotonic()
|
2016-02-17 17:48:23 +00:00
|
|
|
response = self._client.send_email(
|
|
|
|
|
Source=source,
|
|
|
|
|
Destination={
|
2019-08-12 13:53:22 +01:00
|
|
|
'ToAddresses': [punycode_encode_email(addr) for addr in to_addresses],
|
2016-02-17 17:48:23 +00:00
|
|
|
'CcAddresses': [],
|
|
|
|
|
'BccAddresses': []
|
|
|
|
|
},
|
|
|
|
|
Message={
|
|
|
|
|
'Subject': {
|
|
|
|
|
'Data': subject,
|
|
|
|
|
},
|
2016-03-18 11:47:01 +00:00
|
|
|
'Body': body
|
2016-02-17 17:48:23 +00:00
|
|
|
},
|
2019-08-12 13:53:22 +01:00
|
|
|
ReplyToAddresses=[punycode_encode_email(addr) for addr in reply_to_addresses]
|
2016-10-13 14:17:17 +01:00
|
|
|
)
|
2016-10-13 15:27:47 +01:00
|
|
|
except botocore.exceptions.ClientError as e:
|
2016-10-13 16:07:32 +01:00
|
|
|
self.statsd_client.incr("clients.ses.error")
|
|
|
|
|
|
|
|
|
|
# http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
|
2016-10-13 15:27:47 +01:00
|
|
|
if e.response['Error']['Code'] == 'InvalidParameterValue':
|
2020-12-31 11:08:09 +00:00
|
|
|
raise EmailClientNonRetryableException(e.response['Error']['Message'])
|
2020-08-13 17:18:19 +01:00
|
|
|
elif (
|
|
|
|
|
e.response['Error']['Code'] == 'Throttling'
|
|
|
|
|
and e.response['Error']['Message'] == 'Maximum sending rate exceeded.'
|
|
|
|
|
):
|
|
|
|
|
raise AwsSesClientThrottlingSendRateException(str(e))
|
2016-10-13 16:07:32 +01:00
|
|
|
else:
|
|
|
|
|
self.statsd_client.incr("clients.ses.error")
|
|
|
|
|
raise AwsSesClientException(str(e))
|
2016-02-17 17:48:23 +00:00
|
|
|
except Exception as e:
|
2016-08-08 11:23:58 +01:00
|
|
|
self.statsd_client.incr("clients.ses.error")
|
2016-02-17 17:48:23 +00:00
|
|
|
raise AwsSesClientException(str(e))
|
2016-10-13 15:27:47 +01:00
|
|
|
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)
|
add raw request timings to provider send functions
we're using statsd to monitor how long provider requests are taking.
However, there's lots of busy work that happens inside our statsd
metrics timing window. Things like json dumping and loading, building
headers, exception handling, etc.
for firetext/mmg, the response object from requests has an elapsed
property [1], which captures from sending raw data to parsing the
response headers. for ses, it's a bit trickier, but boto3 exposes a few
event hooks [2]. it's hard to find them without stepping through the
code, but the interesting ones are before-call, after-call,
after-call-error, request-created, and response-received. The
before-call and after-call involve some marshalling, built-in retrying,
etc, while request-created and response-received are much lower level.
They might be called more than once per ses request, if boto3 itself
retries the request on 5xx, 429 and low level socket errors [3].
Add these as new `raw-request-time` metrics rather than overwriting to
avoid changing the meaning of an existing metric, and to let us compare
the metrics to see if there's a noticeable difference at all
[1] https://requests.readthedocs.io/en/master/api/#requests.Response.elapsed
[2] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/events.html
[3] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode
2020-05-29 13:56:29 +01:00
|
|
|
if self.ses_elapsed_time != 0:
|
|
|
|
|
self.statsd_client.timing("clients.ses.raw-request-time", self.ses_elapsed_time)
|
2016-10-13 15:27:47 +01:00
|
|
|
self.statsd_client.incr("clients.ses.success")
|
|
|
|
|
return response['MessageId']
|
2019-08-12 13:53:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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'))
|