|
| 1 | +from __future__ import annotations |
| 2 | + |
1 | 3 | import io |
2 | 4 | import json |
| 5 | +import typing |
3 | 6 | from base64 import b64decode |
4 | 7 |
|
5 | 8 | from django.http import HttpResponse |
@@ -144,6 +147,21 @@ def parse_events(self, request): |
144 | 147 | def esp_to_anymail_events(self, ses_event, sns_message): |
145 | 148 | raise NotImplementedError() |
146 | 149 |
|
| 150 | + def get_boto_client(self, service_name: str, **kwargs): |
| 151 | + """ |
| 152 | + Return a boto3 client for service_name, using session_params and |
| 153 | + client_params from settings. Any kwargs are treated as additional |
| 154 | + client_params (overriding settings values). |
| 155 | + """ |
| 156 | + if kwargs: |
| 157 | + client_params = self.client_params.copy() |
| 158 | + client_params.update(kwargs) |
| 159 | + else: |
| 160 | + client_params = self.client_params |
| 161 | + return boto3.session.Session(**self.session_params).client( |
| 162 | + service_name, **client_params |
| 163 | + ) |
| 164 | + |
147 | 165 | def auto_confirm_sns_subscription(self, sns_message): |
148 | 166 | """ |
149 | 167 | Automatically accept a subscription to Amazon SNS topics, |
@@ -193,15 +211,14 @@ def auto_confirm_sns_subscription(self, sns_message): |
193 | 211 | raise ValueError( |
194 | 212 | "Invalid ARN format '{topic_arn!s}'".format(topic_arn=topic_arn) |
195 | 213 | ) |
196 | | - client_params = self.client_params.copy() |
197 | | - client_params["region_name"] = region |
198 | 214 |
|
199 | | - sns_client = boto3.session.Session(**self.session_params).client( |
200 | | - "sns", **client_params |
201 | | - ) |
202 | | - sns_client.confirm_subscription( |
203 | | - TopicArn=topic_arn, Token=token, AuthenticateOnUnsubscribe="true" |
204 | | - ) |
| 215 | + sns_client = self.get_boto_client("sns", region_name=region) |
| 216 | + try: |
| 217 | + sns_client.confirm_subscription( |
| 218 | + TopicArn=topic_arn, Token=token, AuthenticateOnUnsubscribe="true" |
| 219 | + ) |
| 220 | + finally: |
| 221 | + sns_client.close() |
205 | 222 |
|
206 | 223 |
|
207 | 224 | class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView): |
@@ -371,28 +388,16 @@ def esp_to_anymail_events(self, ses_event, sns_message): |
371 | 388 | else: |
372 | 389 | message = AnymailInboundMessage.parse_raw_mime(content) |
373 | 390 | elif action_type == "S3": |
374 | | - # download message from s3 into memory, then parse. (SNS has 15s limit |
| 391 | + # Download message from s3 and parse. (SNS has 15s limit |
375 | 392 | # for an http response; hope download doesn't take that long) |
376 | | - bucket_name = action_object["bucketName"] |
377 | | - object_key = action_object["objectKey"] |
378 | | - s3 = boto3.session.Session(**self.session_params).client( |
379 | | - "s3", **self.client_params |
| 393 | + fp = self.download_s3_object( |
| 394 | + bucket_name=action_object["bucketName"], |
| 395 | + object_key=action_object["objectKey"], |
380 | 396 | ) |
381 | | - content = io.BytesIO() |
382 | 397 | try: |
383 | | - s3.download_fileobj(bucket_name, object_key, content) |
384 | | - content.seek(0) |
385 | | - message = AnymailInboundMessage.parse_raw_mime_file(content) |
386 | | - except ClientError as err: |
387 | | - # improve the botocore error message |
388 | | - raise AnymailBotoClientAPIError( |
389 | | - "Anymail AmazonSESInboundWebhookView couldn't download" |
390 | | - " S3 object '{bucket_name}:{object_key}'" |
391 | | - "".format(bucket_name=bucket_name, object_key=object_key), |
392 | | - client_error=err, |
393 | | - ) from err |
| 398 | + message = AnymailInboundMessage.parse_raw_mime_file(fp) |
394 | 399 | finally: |
395 | | - content.close() |
| 400 | + fp.close() |
396 | 401 | else: |
397 | 402 | raise AnymailConfigurationError( |
398 | 403 | "Anymail's Amazon SES inbound webhook works only with 'SNS' or 'S3'" |
@@ -432,6 +437,30 @@ def esp_to_anymail_events(self, ses_event, sns_message): |
432 | 437 | ) |
433 | 438 | ] |
434 | 439 |
|
| 440 | + def download_s3_object(self, bucket_name: str, object_key: str) -> typing.IO: |
| 441 | + """ |
| 442 | + Download bucket_name/object_key from S3. Must return a file-like object |
| 443 | + (bytes or text) opened for reading. Caller is responsible for closing it. |
| 444 | + """ |
| 445 | + s3_client = self.get_boto_client("s3") |
| 446 | + bytesio = io.BytesIO() |
| 447 | + try: |
| 448 | + s3_client.download_fileobj(bucket_name, object_key, bytesio) |
| 449 | + except ClientError as err: |
| 450 | + bytesio.close() |
| 451 | + # improve the botocore error message |
| 452 | + raise AnymailBotoClientAPIError( |
| 453 | + "Anymail AmazonSESInboundWebhookView couldn't download" |
| 454 | + " S3 object '{bucket_name}:{object_key}'" |
| 455 | + "".format(bucket_name=bucket_name, object_key=object_key), |
| 456 | + client_error=err, |
| 457 | + ) from err |
| 458 | + else: |
| 459 | + bytesio.seek(0) |
| 460 | + return bytesio |
| 461 | + finally: |
| 462 | + s3_client.close() |
| 463 | + |
435 | 464 |
|
436 | 465 | class AnymailBotoClientAPIError(AnymailAPIError, ClientError): |
437 | 466 | """An AnymailAPIError that is also a Boto ClientError""" |
|
0 commit comments