3 """Send bulk emails through AWS SES. 4 This script is an extension (and eventually a replacement) to sendSESEmail.py, 5 and a more generalized version of it's functionalities. Eventually, we want 6 to validate the funtionalities of this script in use cases where 7 sendSESEmail.py is being used, and make the email sending implementation in 10 This attempts to simplify the grouping of email recipients 11 (to, cc and bcc) by respecting the limit of number of recipients that can be 12 used in a single aws ses send_email api call. 14 Uses the AWS SES call send-mail. 15 Has the following important parameters: 17 Body contents: (at least one of the following has to be specified.) 18 - text -- the plain text version of the email. 19 - html -- the HTML-formatted version of the email. 21 - subject -- the subject of the email. This is required. 22 - efrom -- the from email. This is required. 23 The efrom requires verification by AWS SES (either by email address or by domain.) 24 - replyto -- the reply to email. (submitform.pl & submitsecure.pl) 26 IMPORTANT: Please inspect the batch_recipients function to see how email 27 groups are constructed. 29 Email destinations: (at least one of the following has to be specified.) 30 Destinations are space-separated like a@comp.dom b@comp.dom ... 31 - to -- a list of to recipients. (Who the email is actually addressed to.) 32 - bcc -- a list of bcc recipients. (List isn't visible in email.) 33 - cc -- the cc for the email. (Visible list in email.) 35 Amazon requires the from email to be verified or the domain verified. 43 from botocore.exceptions
import ClientError
47 format=(
"[%(levelname)-5s] %(asctime)-15s ({}|%(funcName)s|%(lineno)d) " 48 ":: %(message)s").format(__file__)
49 LOGGER = logging.getLogger(__name__)
50 LOGGER.setLevel(logging.DEBUG)
51 logging_handler = logging.StreamHandler()
52 logging_handler.setFormatter(logging.Formatter(format))
53 LOGGER.addHandler(logging_handler)
56 USAGE_HELP = (
"Usage: sendSESEmailBulk.py [-h] [--to TO [TO ...]] " 57 "[--bcc BCC [BCC ...]] [--cc CC [CC ...]] --efrom EFROM" 58 "[--replyto REPLYTO [REPLYTO ...]] --subject SUBJECT" 59 "[--plaintext PLAINTEXT] [--htmltext HTMLTEXT]" 60 "[--quiet] [--context CONTEXT]")
63 AWS_REGION =
'us-west-2' 68 AWS_SES_EMAIL_LIMIT = 50
72 """Custom ArgumentParser class 75 argparse.ArgumentParser 79 """Suppressing default error method 81 Return custom error message on ArgumentParser.error. 84 message -- error message 89 raise BaseException(message)
93 """Prepare argument parser. 96 parser -- ArgumentParser object 98 parser = CustomArgumentParser (
99 description =
"Send emails through AWS SES.",
100 formatter_class = argparse.RawDescriptionHelpFormatter,
101 epilog = textwrap.dedent(
'''\ 102 additional information: 103 One of --to, --bcc, or --cc has to be defined. 104 One of --plaintext or --htmltext has to be defined. 105 --efrom and --subject are required. 108 parser.add_argument (
"--to", nargs =
"+",
109 help =
"Space-separated list of to email addresses",
112 parser.add_argument (
"--bcc", nargs =
"+",
113 help =
"Space-separated list of bcc email addresses",
116 parser.add_argument (
"--cc", nargs =
"+",
117 help =
"Space-separated list of cc email addresses",
120 parser.add_argument (
"--efrom",
121 help =
"From email address",
124 parser.add_argument (
"--replyto", nargs =
"+",
125 help =
"Reply to email address",
128 parser.add_argument (
"--subject",
129 help =
"Subject of the email",
132 parser.add_argument (
"--plaintext",
133 help =
"Email body in plaintext",
136 parser.add_argument (
"--htmltext",
137 help =
"Html body in HTML",
140 parser.add_argument(
"--quiet", action =
"store_true",
141 help =
"Suppress success message",
144 parser.add_argument(
"--context",
145 help = (
"Json string for logging application" 146 "context for the email messages being sent."),
152 def get_ith_batch(_iter, batch_i, batch_size):
153 start = batch_i * batch_size
154 end = min(batch_i * batch_size + batch_size, len(_iter))
155 return _iter[start:end]
160 This function constructs a collective (to, cc, bcc) batch of email 161 recipients to be used for aws ses send_email destination argument. 163 NOTE: there are varions use cases that need detailed conversation: 164 - What if the total of cc + to recipients is greater than the 165 AWS_SES_EMAIL_LIMIT? How do we construct the appropriate 166 destination object? How do we divide cc's? How many cc's do 168 - Similar concerns with cc + bcc. 171 to: list of to recipients 172 cc: list of cc recipients 173 bcc: list of bcc recipients 176 Generator that iterator of email group recipients 184 if to_count + cc_count + bcc_count <= ses_batch_size:
189 if to_count + cc_count <= ses_batch_size:
197 for ith_to
in range(int(to_count/ses_batch_size)+1):
198 yield (get_ith_batch(to, ith_to, ses_batch_size), [], [])
204 for ith_cc
in range(int(cc_count/ses_batch_size)+1):
205 yield ([], get_ith_batch(cc, ith_cc, ses_batch_size), [])
209 for ith_bcc
in range(int(bcc_count/ses_batch_size)+1):
210 yield ([], [], get_ith_batch(bcc, ith_bcc, ses_batch_size))
215 This sends emails to a batch of to, cc and bcc recipients. 218 email_group: group of to, cc and bcc email recipients; total 219 should be less than or equal to AWS_SES_EMAIL_LIMIT 221 batch_return: a dictionary of status, error and response force 238 client = boto3.client(
'ses', region_name=AWS_REGION)
243 if email_group[0]: destination[
"ToAddresses"] = email_group[0]
244 if email_group[1]: destination[
"CcAddresses"] = email_group[1]
245 if email_group[2]: destination[
"BccAddresses"] = email_group[2]
250 message[
"Subject"] = {}
251 message[
"Subject"][
"Data"] = args.subject
252 message[
"Subject"][
"Charset"] = charset
254 if (args.plaintext !=
None):
256 message[
"Body"][
"Text"] = {}
257 message[
"Body"][
"Text"] [
"Data"] = args.plaintext
258 message[
"Body"][
"Text"] [
"Charset"] = charset
260 if (args.htmltext !=
None):
261 if "Body" not in message:
262 message [
"Body"] = {}
264 message[
"Body"][
"Html"] = {}
265 message[
"Body"][
"Html"][
"Data"] = args.htmltext
266 message[
"Body"][
"Html"][
"Charset"] = charset
268 if (args.replyto !=
None):
270 response = client.send_email (
272 Destination = destination,
274 ReplyToAddresses = args.replyto,
275 ConfigurationSetName=
'ses-detail-logging',
279 response = client.send_email (
281 Destination = destination,
283 ConfigurationSetName=
'ses-detail-logging',
286 except ClientError
as e:
288 batch_return[
"status"] = 902
289 batch_return[
"error"] = str(e)
291 except Exception
as e:
293 batch_return[
'status'] = 903
294 batch_return[
'error'] = e
297 batch_return[
'response'] = response
302 LOGGER.info(batch_return)
304 LOGGER.error(batch_return)
310 """Main method to execute operations on email verification. 313 argv {list} -- list of script arguments 316 validation_response <dictionary>: if any arguments validation error, 317 return a json error: code is non zero 319 respons_list <list>: list of json response for each batch 329 validation_response = {
333 'context': sender_context
344 args = parser.parse_args(argv)
345 sender_context = args.context
348 if (args.to ==
None and args.bcc ==
None and args.cc ==
None):
349 raise BaseException (
"Destination has to be specified. One of" 350 " the --to, --bcc or --cc options need" 353 if args.to !=
None: recipient_to = args.to
354 if args.cc !=
None: recipient_cc = args.cc
355 if args.bcc !=
None: recipient_bcc = args.bcc
357 if (args.plaintext ==
None and args.htmltext ==
None):
358 raise BaseException (
"Email body has to be specified")
360 except (BaseException)
as e:
364 validation_response[
"status"] = return_code
365 validation_response[
"error"].append(USAGE_HELP)
366 validation_response[
"error"].append(str(e))
367 validation_response[
"context"] = sender_context
368 LOGGER.error(json.dumps(validation_response))
370 except (Exception)
as e:
372 validation_response[
"status"] = return_code
373 validation_response[
"error"] = str(e)
374 validation_response[
"context"] = sender_context
375 LOGGER.error(json.dumps(validation_response))
386 for email_group
in recipient_generator:
387 if sum(map(len, email_group)) > 0:
391 if not batch_success:
398 """Script entrypoint""" 399 sys.exit(
main(sys.argv[1:]))
402 if __name__ ==
"__main__":
def batch_recipients(to, cc, bcc, ses_batch_size=AWS_SES_EMAIL_LIMIT)
def send_email_batch(email_group, context, args)