-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Expand file tree
/
Copy pathsesv2_wrapper.py
More file actions
352 lines (312 loc) · 13.3 KB
/
sesv2_wrapper.py
File metadata and controls
352 lines (312 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Encapsulates Amazon SESv2 actions for sending emails with attachments.
"""
import logging
from typing import Any, Dict, List, Optional
import boto3
from botocore.exceptions import ClientError
logger = logging.getLogger(__name__)
# snippet-start:[python.example_code.sesv2.SESv2Wrapper.class]
# snippet-start:[python.example_code.sesv2.SESv2Wrapper.decl]
class SESv2Wrapper:
"""Encapsulates Amazon SESv2 email sending actions."""
def __init__(self, sesv2_client: Any) -> None:
"""
Initializes the SESv2Wrapper with an SESv2 client.
:param sesv2_client: A Boto3 SESv2 client.
"""
self.sesv2_client = sesv2_client
@classmethod
def from_client(cls) -> "SESv2Wrapper":
"""
Creates an SESv2Wrapper instance with a default Boto3 SESv2 client.
:return: A new SESv2Wrapper instance.
"""
sesv2_client = boto3.client("sesv2")
return cls(sesv2_client)
# snippet-end:[python.example_code.sesv2.SESv2Wrapper.decl]
# snippet-start:[python.example_code.sesv2.GetEmailIdentity]
def get_email_identity(self, email_address: str) -> Dict[str, Any]:
"""
Gets information about an email identity, including its verification status.
:param email_address: The email address or domain to look up.
:return: A dictionary with identity information including verification status.
:raises ClientError: If the identity is not found (NotFoundException).
"""
try:
response = self.sesv2_client.get_email_identity(
EmailIdentity=email_address
)
logger.info("Got email identity for %s.", email_address)
return response
except ClientError as err:
if err.response["Error"]["Code"] == "NotFoundException":
logger.info(
"Email identity %s not found.", email_address
)
else:
logger.error(
"Couldn't get email identity %s. Here's why: %s: %s",
email_address,
err.response["Error"]["Code"],
err.response["Error"]["Message"],
)
raise
# snippet-end:[python.example_code.sesv2.GetEmailIdentity]
# snippet-start:[python.example_code.sesv2.CreateEmailIdentity]
def create_email_identity(self, email_address: str) -> Dict[str, Any]:
"""
Starts the process of verifying an email identity (email address or domain).
:param email_address: The email address or domain to verify.
:return: A dictionary with the identity type and verification status.
:raises ClientError: If the limit is exceeded (LimitExceededException).
"""
try:
response = self.sesv2_client.create_email_identity(
EmailIdentity=email_address
)
logger.info(
"Started verification for email identity %s.", email_address
)
return response
except ClientError as err:
if err.response["Error"]["Code"] == "LimitExceededException":
logger.error(
"Couldn't create email identity %s. You have exceeded "
"the maximum number of email identities. "
"Use an existing verified identity.",
email_address,
)
else:
logger.error(
"Couldn't create email identity %s. Here's why: %s: %s",
email_address,
err.response["Error"]["Code"],
err.response["Error"]["Message"],
)
raise
# snippet-end:[python.example_code.sesv2.CreateEmailIdentity]
# snippet-start:[python.example_code.sesv2.CreateEmailTemplate]
def create_email_template(
self,
template_name: str,
subject: str,
html_body: str,
text_body: str,
) -> None:
"""
Creates an email template for use with templated and bulk email sends.
:param template_name: The name for the new template.
:param subject: The subject line of the template. May include {{placeholders}}.
:param html_body: The HTML body of the template.
:param text_body: The plain text body of the template.
:raises ClientError: If the template limit is exceeded (LimitExceededException).
"""
try:
self.sesv2_client.create_email_template(
TemplateName=template_name,
TemplateContent={
"Subject": subject,
"Html": html_body,
"Text": text_body,
},
)
logger.info("Created email template %s.", template_name)
except ClientError as err:
if err.response["Error"]["Code"] == "LimitExceededException":
logger.error(
"Couldn't create email template %s. You have exceeded "
"the maximum number of email templates. "
"Delete unused templates first.",
template_name,
)
else:
logger.error(
"Couldn't create email template %s. Here's why: %s: %s",
template_name,
err.response["Error"]["Code"],
err.response["Error"]["Message"],
)
raise
# snippet-end:[python.example_code.sesv2.CreateEmailTemplate]
# snippet-start:[python.example_code.sesv2.SendEmail]
def send_email(
self,
from_address: str,
to_addresses: List[str],
subject: str,
html_body: str,
text_body: str,
attachments: Optional[List[Dict[str, Any]]] = None,
) -> str:
"""
Sends a simple email message with optional attachments.
SES handles MIME construction automatically when using attachments
with the Simple content type, so developers don't need to build
raw MIME messages.
:param from_address: The verified sender email address.
:param to_addresses: A list of recipient email addresses.
:param subject: The subject line of the email.
:param html_body: The HTML body content.
:param text_body: The plain text body content.
:param attachments: An optional list of attachment dictionaries. Each
attachment should contain 'RawContent' (bytes), 'FileName' (str),
and optionally 'ContentType', 'ContentDisposition', 'ContentId',
'ContentDescription', and 'ContentTransferEncoding'.
:return: The MessageId of the sent email.
:raises ClientError: If the message is rejected (MessageRejected).
"""
try:
simple_message: Dict[str, Any] = {
"Subject": {"Data": subject},
"Body": {
"Html": {"Data": html_body},
"Text": {"Data": text_body},
},
}
if attachments:
simple_message["Attachments"] = attachments
response = self.sesv2_client.send_email(
FromEmailAddress=from_address,
Destination={"ToAddresses": to_addresses},
Content={"Simple": simple_message},
)
message_id = response["MessageId"]
logger.info(
"Sent email from %s to %s. MessageId: %s",
from_address,
to_addresses,
message_id,
)
return message_id
except ClientError as err:
if err.response["Error"]["Code"] == "MessageRejected":
logger.error(
"Message was rejected. Check that attachments use "
"supported file types and total message size is "
"under 40 MB. Details: %s",
err.response["Error"]["Message"],
)
else:
logger.error(
"Couldn't send email. Here's why: %s: %s",
err.response["Error"]["Code"],
err.response["Error"]["Message"],
)
raise
# snippet-end:[python.example_code.sesv2.SendEmail]
# snippet-start:[python.example_code.sesv2.SendBulkEmail]
def send_bulk_email(
self,
from_address: str,
template_name: str,
default_template_data: str,
bulk_entries: List[Dict[str, Any]],
attachments: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
"""
Sends a templated email to multiple recipients in a single API call.
All recipients receive the same attachment(s) defined in the default
content, while template data can be personalized per recipient.
:param from_address: The verified sender email address.
:param template_name: The name of an existing email template.
:param default_template_data: Default JSON template data string.
:param bulk_entries: A list of BulkEmailEntry dicts, each containing
'Destination' and optionally 'ReplacementEmailContent'.
:param attachments: An optional list of attachment dicts for all
recipients.
:return: A list of BulkEmailEntryResult dicts with status and MessageId.
:raises ClientError: If the message is rejected (MessageRejected).
"""
try:
template_content: Dict[str, Any] = {
"TemplateName": template_name,
"TemplateData": default_template_data,
}
if attachments:
template_content["Attachments"] = attachments
response = self.sesv2_client.send_bulk_email(
FromEmailAddress=from_address,
DefaultContent={"Template": template_content},
BulkEmailEntries=bulk_entries,
)
results = response.get("BulkEmailEntryResults", [])
logger.info(
"Sent bulk email from %s to %d recipients.",
from_address,
len(bulk_entries),
)
return results
except ClientError as err:
if err.response["Error"]["Code"] == "MessageRejected":
logger.error(
"Bulk message was rejected. Check that the template "
"exists, attachment file types are supported, and "
"total message size is within limits. Details: %s",
err.response["Error"]["Message"],
)
else:
logger.error(
"Couldn't send bulk email. Here's why: %s: %s",
err.response["Error"]["Code"],
err.response["Error"]["Message"],
)
raise
# snippet-end:[python.example_code.sesv2.SendBulkEmail]
# snippet-start:[python.example_code.sesv2.DeleteEmailTemplate]
def delete_email_template(self, template_name: str) -> None:
"""
Deletes an email template.
:param template_name: The name of the template to delete.
:raises ClientError: If the template is not found (NotFoundException).
"""
try:
self.sesv2_client.delete_email_template(
TemplateName=template_name
)
logger.info("Deleted email template %s.", template_name)
except ClientError as err:
if err.response["Error"]["Code"] == "NotFoundException":
logger.info(
"Email template %s not found or already deleted.",
template_name,
)
else:
logger.error(
"Couldn't delete email template %s. Here's why: %s: %s",
template_name,
err.response["Error"]["Code"],
err.response["Error"]["Message"],
)
raise
# snippet-end:[python.example_code.sesv2.DeleteEmailTemplate]
# snippet-start:[python.example_code.sesv2.DeleteEmailIdentity]
def delete_email_identity(self, email_address: str) -> None:
"""
Deletes an email identity.
:param email_address: The email address or domain to delete.
:raises ClientError: If the identity is not found (NotFoundException).
"""
try:
self.sesv2_client.delete_email_identity(
EmailIdentity=email_address
)
logger.info("Deleted email identity %s.", email_address)
except ClientError as err:
if err.response["Error"]["Code"] == "NotFoundException":
logger.info(
"Email identity %s not found or already deleted.",
email_address,
)
else:
logger.error(
"Couldn't delete email identity %s. Here's why: %s: %s",
email_address,
err.response["Error"]["Code"],
err.response["Error"]["Message"],
)
raise
# snippet-end:[python.example_code.sesv2.DeleteEmailIdentity]
# snippet-end:[python.example_code.sesv2.SESv2Wrapper.class]