@@ -62,6 +62,7 @@ def __init__(self, fail_silently=False, aws_access_key=None,
62
62
aws_region_endpoint = None , aws_auto_throttle = None , aws_config = None ,
63
63
dkim_domain = None , dkim_key = None , dkim_selector = None , dkim_headers = None ,
64
64
ses_source_arn = None , ses_from_arn = None , ses_return_path_arn = None ,
65
+ use_ses_v2 = False ,
65
66
** kwargs ):
66
67
67
68
super (SESBackend , self ).__init__ (fail_silently = fail_silently , ** kwargs )
@@ -82,6 +83,8 @@ def __init__(self, fail_silently=False, aws_access_key=None,
82
83
self .ses_from_arn = ses_from_arn or settings .AWS_SES_FROM_ARN
83
84
self .ses_return_path_arn = ses_return_path_arn or settings .AWS_SES_RETURN_PATH_ARN
84
85
86
+ self ._use_ses_v2 = use_ses_v2 or settings .USE_SES_V2
87
+
85
88
self .connection = None
86
89
87
90
def open (self ):
@@ -93,7 +96,7 @@ def open(self):
93
96
94
97
try :
95
98
self .connection = boto3 .client (
96
- 'ses' ,
99
+ 'sesv2' if self . _use_ses_v2 else ' ses' ,
97
100
aws_access_key_id = self ._access_key_id ,
98
101
aws_secret_access_key = self ._access_key ,
99
102
aws_session_token = self ._session_token ,
@@ -154,66 +157,14 @@ def send_messages(self, email_messages):
154
157
# well below the actual SES throttle.
155
158
# Set the setting to 0 or None to disable throttling.
156
159
if self ._throttle :
157
- global recent_send_times
158
-
159
- now = datetime .now ()
160
-
161
- # Get and cache the current SES max-per-second rate limit
162
- # returned by the SES API.
163
- rate_limit = self .get_rate_limit ()
164
- logger .debug ("send_messages.throttle rate_limit='{}'" .format (rate_limit ))
165
-
166
- # Prune from recent_send_times anything more than a few seconds
167
- # ago. Even though SES reports a maximum per-second, the way
168
- # they enforce the limit may not be on a one-second window.
169
- # To be safe, we use a two-second window (but allow 2 times the
170
- # rate limit) and then also have a default rate limit factor of
171
- # 0.5 so that we really limit the one-second amount in two
172
- # seconds.
173
- window = 2.0 # seconds
174
- window_start = now - timedelta (seconds = window )
175
- new_send_times = []
176
- for time in recent_send_times :
177
- if time > window_start :
178
- new_send_times .append (time )
179
- recent_send_times = new_send_times
180
-
181
- # If the number of recent send times in the last 1/_throttle
182
- # seconds exceeds the rate limit, add a delay.
183
- # Since I'm not sure how Amazon determines at exactly what
184
- # point to throttle, better be safe than sorry and let in, say,
185
- # half of the allowed rate.
186
- if len (new_send_times ) > rate_limit * window * self ._throttle :
187
- # Sleep the remainder of the window period.
188
- delta = now - new_send_times [0 ]
189
- total_seconds = (delta .microseconds + (delta .seconds +
190
- delta .days * 24 * 3600 ) * 10 ** 6 ) / 10 ** 6
191
- delay = window - total_seconds
192
- if delay > 0 :
193
- sleep (delay )
194
-
195
- recent_send_times .append (now )
196
- # end of throttling
197
-
198
- kwargs = dict (
199
- Source = source or message .from_email ,
200
- Destinations = message .recipients (),
201
- # todo attachments?
202
- RawMessage = {'Data' : dkim_sign (message .message ().as_string (),
203
- dkim_key = self .dkim_key ,
204
- dkim_domain = self .dkim_domain ,
205
- dkim_selector = self .dkim_selector ,
206
- dkim_headers = self .dkim_headers )}
207
- )
208
- if self .ses_source_arn :
209
- kwargs ['SourceArn' ] = self .ses_source_arn
210
- if self .ses_from_arn :
211
- kwargs ['FromArn' ] = self .ses_from_arn
212
- if self .ses_return_path_arn :
213
- kwargs ['ReturnPathArn' ] = self .ses_return_path_arn
160
+ self ._update_throttling ()
161
+
162
+ kwargs = self ._get_send_email_parameters (message , source )
214
163
215
164
try :
216
- response = self .connection .send_raw_email (** kwargs )
165
+ response = (self .connection .send_email (** kwargs )
166
+ if self ._use_ses_v2
167
+ else self .connection .send_raw_email (** kwargs ))
217
168
message .extra_headers ['status' ] = 200
218
169
message .extra_headers ['message_id' ] = response ['MessageId' ]
219
170
message .extra_headers ['request_id' ] = response ['ResponseMetadata' ]['RequestId' ]
@@ -250,6 +201,94 @@ def send_messages(self, email_messages):
250
201
251
202
return num_sent
252
203
204
+ def _update_throttling (self ):
205
+ global recent_send_times
206
+ now = datetime .now ()
207
+ # Get and cache the current SES max-per-second rate limit
208
+ # returned by the SES API.
209
+ rate_limit = self .get_rate_limit ()
210
+ logger .debug ("send_messages.throttle rate_limit='{}'" .format (rate_limit ))
211
+ # Prune from recent_send_times anything more than a few seconds
212
+ # ago. Even though SES reports a maximum per-second, the way
213
+ # they enforce the limit may not be on a one-second window.
214
+ # To be safe, we use a two-second window (but allow 2 times the
215
+ # rate limit) and then also have a default rate limit factor of
216
+ # 0.5 so that we really limit the one-second amount in two
217
+ # seconds.
218
+ window = 2.0 # seconds
219
+ window_start = now - timedelta (seconds = window )
220
+ new_send_times = []
221
+ for time in recent_send_times :
222
+ if time > window_start :
223
+ new_send_times .append (time )
224
+ recent_send_times = new_send_times
225
+ # If the number of recent send times in the last 1/_throttle
226
+ # seconds exceeds the rate limit, add a delay.
227
+ # Since I'm not sure how Amazon determines at exactly what
228
+ # point to throttle, better be safe than sorry and let in, say,
229
+ # half of the allowed rate.
230
+ if len (new_send_times ) > rate_limit * window * self ._throttle :
231
+ # Sleep the remainder of the window period.
232
+ delta = now - new_send_times [0 ]
233
+ total_seconds = (delta .microseconds + (delta .seconds +
234
+ delta .days * 24 * 3600 ) * 10 ** 6 ) / 10 ** 6
235
+ delay = window - total_seconds
236
+ if delay > 0 :
237
+ sleep (delay )
238
+ recent_send_times .append (now )
239
+ # end of throttling
240
+
241
+ def _get_send_email_parameters (self , message , source ):
242
+ return (self ._get_v2_parameters (message , source )
243
+ if self ._use_ses_v2
244
+ else self ._get_v1_parameters (message , source ))
245
+
246
+ def _get_v2_parameters (self , message , source ):
247
+ """V2-Style raw payload for `send_email`.
248
+
249
+ https://boto3.amazonaws.com/v1/documentation/api/1.26.31/reference/services/sesv2.html#SESV2.Client.send_email
250
+ """
251
+ params = dict (
252
+ FromEmailAddress = source or message .from_email ,
253
+ Destination = {
254
+ 'ToAddresses' : message .recipients ()
255
+ },
256
+ Content = {
257
+ 'Raw' : {
258
+ 'Data' : dkim_sign (message .message ().as_string (),
259
+ dkim_key = self .dkim_key ,
260
+ dkim_domain = self .dkim_domain ,
261
+ dkim_selector = self .dkim_selector ,
262
+ dkim_headers = self .dkim_headers )
263
+ }
264
+ }
265
+ )
266
+ if self .ses_from_arn or self .ses_source_arn :
267
+ params ['FromEmailAddressIdentityArn' ] = self .ses_from_arn or self .ses_source_arn
268
+ return params
269
+
270
+ def _get_v1_parameters (self , message , source ):
271
+ """V1-Style raw payload for `send_raw_email`
272
+
273
+ https://boto3.amazonaws.com/v1/documentation/api/1.26.31/reference/services/ses.html#SES.Client.send_raw_email
274
+ """
275
+ params = dict (
276
+ Source = source or message .from_email ,
277
+ Destinations = message .recipients (),
278
+ RawMessage = {'Data' : dkim_sign (message .message ().as_string (),
279
+ dkim_key = self .dkim_key ,
280
+ dkim_domain = self .dkim_domain ,
281
+ dkim_selector = self .dkim_selector ,
282
+ dkim_headers = self .dkim_headers )}
283
+ )
284
+ if self .ses_source_arn :
285
+ params ['SourceArn' ] = self .ses_source_arn
286
+ if self .ses_from_arn :
287
+ params ['FromArn' ] = self .ses_from_arn
288
+ if self .ses_return_path_arn :
289
+ params ['ReturnPathArn' ] = self .ses_return_path_arn
290
+ return params
291
+
253
292
def get_rate_limit (self ):
254
293
if self ._access_key_id in cached_rate_limits :
255
294
return cached_rate_limits [self ._access_key_id ]
@@ -259,11 +298,26 @@ def get_rate_limit(self):
259
298
raise Exception (
260
299
"No connection is available to check current SES rate limit." )
261
300
try :
262
- quota_dict = self .connection .get_send_quota ()
263
- max_per_second = quota_dict ['MaxSendRate' ]
264
- ret = float (max_per_second )
265
- cached_rate_limits [self ._access_key_id ] = ret
266
- return ret
301
+ return self ._get_v2_send_quota () if self ._use_ses_v2 else self ._get_v1_send_quota ()
267
302
finally :
268
303
if new_conn_created :
269
304
self .close ()
305
+
306
+ def _get_v2_send_quota (self ):
307
+ """This needs to come from the `get_account` endpoint as a nested response in V2.
308
+
309
+ https://boto3.amazonaws.com/v1/documentation/api/1.26.31/reference/services/sesv2.html#SESV2.Client.get_account
310
+ """
311
+ account_dict = self .connection .get_account ()
312
+ quota_dict = account_dict ['SendQuota' ]
313
+ max_per_second = quota_dict ['MaxSendRate' ]
314
+ ret = float (max_per_second )
315
+ cached_rate_limits [self ._access_key_id ] = ret
316
+ return ret
317
+
318
+ def _get_v1_send_quota (self ):
319
+ quota_dict = self .connection .get_send_quota ()
320
+ max_per_second = quota_dict ['MaxSendRate' ]
321
+ ret = float (max_per_second )
322
+ cached_rate_limits [self ._access_key_id ] = ret
323
+ return ret
0 commit comments