Odyssey
Classes | Functions | Variables
sendSESEmailBulk Namespace Reference

Classes

class  CustomArgumentParser
 

Functions

def getParser ()
 
def get_ith_batch (_iter, batch_i, batch_size)
 
def batch_recipients (to, cc, bcc, ses_batch_size=AWS_SES_EMAIL_LIMIT)
 
def send_email_batch (email_group, context, args)
 
def main (argv)
 
def run ()
 

Variables

tuple format
 
 LOGGER = logging.getLogger(__name__)
 
 logging_handler = logging.StreamHandler()
 
tuple USAGE_HELP
 
string AWS_REGION = 'us-west-2'
 
int AWS_SES_EMAIL_LIMIT = 50
 

Detailed Description

Send bulk emails through AWS SES.
This script is an extension (and eventually a replacement) to sendSESEmail.py,
and a more generalized version of it's functionalities. Eventually, we want
to validate the funtionalities of this script in use cases where
sendSESEmail.py is being used, and make the email sending implementation in
this script default.

This attempts to simplify the grouping of email recipients
(to, cc and bcc) by respecting the limit of number of recipients that can be
used in a single aws ses send_email api call.

Uses the AWS SES call send-mail.
Has the following important parameters:

Body contents: (at least one of the following has to be specified.)
- text -- the plain text version of the email.
- html -- the HTML-formatted version of the email.

- subject -- the subject of the email.  This is required.
- efrom -- the from email.  This is required.
The efrom requires verification by AWS SES (either by email address or by domain.)
- replyto -- the reply to email.  (submitform.pl & submitsecure.pl)

IMPORTANT: Please inspect the batch_recipients function to see how email
groups are constructed.

Email destinations: (at least one of the following has to be specified.)
Destinations are space-separated like a@comp.dom b@comp.dom ...
- to -- a list of to recipients.  (Who the email is actually addressed to.)
- bcc -- a list of bcc recipients. (List isn't visible in email.)
- cc -- the cc for the email.  (Visible list in email.)

Amazon requires the from email to be verified or the domain verified.

Function Documentation

◆ batch_recipients()

def sendSESEmailBulk.batch_recipients (   to,
  cc,
  bcc,
  ses_batch_size = AWS_SES_EMAIL_LIMIT 
)
This function constructs a collective (to, cc, bcc) batch of email
recipients to be used for aws ses send_email destination argument.

NOTE: there are varions use cases that need detailed conversation:
- What if the total of cc + to recipients is greater than the
  AWS_SES_EMAIL_LIMIT? How do we construct the appropriate
  destination object? How do we divide cc's? How many cc's do
  do we want to allow?
- Similar concerns with cc + bcc.

Args:
    to: list of to recipients
    cc: list of cc recipients
    bcc: list of bcc recipients

Returns:
    Generator that iterator of email group recipients

Definition at line 158 of file sendSESEmailBulk.py.

158 def batch_recipients(to, cc, bcc, ses_batch_size=AWS_SES_EMAIL_LIMIT):
159  """
160  This function constructs a collective (to, cc, bcc) batch of email
161  recipients to be used for aws ses send_email destination argument.
162 
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
167  do we want to allow?
168  - Similar concerns with cc + bcc.
169 
170  Args:
171  to: list of to recipients
172  cc: list of cc recipients
173  bcc: list of bcc recipients
174 
175  Returns:
176  Generator that iterator of email group recipients
177 
178  """
179  bcc_count = len(bcc)
180  to_count = len(to)
181  cc_count = len(cc)
182 
183  # list of all recipients under batch size
184  if to_count + cc_count + bcc_count <= ses_batch_size:
185  yield (to, cc, bcc)
186 
187  else:
188  # total of to and cc list can be processed
189  if to_count + cc_count <= ses_batch_size:
190  yield(to, cc, [])
191 
192  # TODO: any combination of to, cc and bcc: this needs to be
193  # further discussed to cover other possible use cases
194  else:
195  # admin broadcast email uses only `to:` recipients
196  if to_count > 0:
197  for ith_to in range(int(to_count/ses_batch_size)+1):
198  yield (get_ith_batch(to, ith_to, ses_batch_size), [], [])
199 
200  # admin broadcast email uses only 'cc:' recipients
201  # this means that this would only send emails as cc
202  # for all the recipients in this group
203  if cc_count > 0:
204  for ith_cc in range(int(cc_count/ses_batch_size)+1):
205  yield ([], get_ith_batch(cc, ith_cc, ses_batch_size), [])
206 
207  # broadcast email uses only 'bcc:' recipients
208  if bcc_count > 0:
209  for ith_bcc in range(int(bcc_count/ses_batch_size)+1):
210  yield ([], [], get_ith_batch(bcc, ith_bcc, ses_batch_size))
211 
212 

◆ getParser()

def sendSESEmailBulk.getParser ( )
Prepare argument parser.

Returns:
    parser -- ArgumentParser object

Definition at line 92 of file sendSESEmailBulk.py.

92 def getParser():
93  """Prepare argument parser.
94 
95  Returns:
96  parser -- ArgumentParser object
97  """
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.
106  '''))
107 
108  parser.add_argument ("--to", nargs = "+",
109  help = "Space-separated list of to email addresses",
110  required = False)
111 
112  parser.add_argument ("--bcc", nargs = "+",
113  help = "Space-separated list of bcc email addresses",
114  required = False)
115 
116  parser.add_argument ("--cc", nargs = "+",
117  help = "Space-separated list of cc email addresses",
118  required = False)
119 
120  parser.add_argument ("--efrom",
121  help = "From email address",
122  required = True)
123 
124  parser.add_argument ("--replyto", nargs = "+",
125  help = "Reply to email address",
126  required = False)
127 
128  parser.add_argument ("--subject",
129  help = "Subject of the email",
130  required = True)
131 
132  parser.add_argument ("--plaintext",
133  help = "Email body in plaintext",
134  required = False)
135 
136  parser.add_argument ("--htmltext",
137  help = "Html body in HTML",
138  required = False)
139 
140  parser.add_argument("--quiet", action = "store_true",
141  help = "Suppress success message",
142  required = False)
143 
144  parser.add_argument("--context",
145  help = ("Json string for logging application"
146  "context for the email messages being sent."),
147  required = False)
148 
149  return parser
150 
151 

◆ main()

def sendSESEmailBulk.main (   argv)
Main method to execute operations on email verification.

Arguments:
    argv {list} -- list of script arguments

Returns:
    validation_response <dictionary>: if any arguments validation error,
                                      return a json error: code is non zero
    OR
    respons_list <list>: list of json response for each batch
                         of email recipients

Raises:
    SystemExit
    BaseException

Definition at line 309 of file sendSESEmailBulk.py.

309 def main(argv):
310  """Main method to execute operations on email verification.
311 
312  Arguments:
313  argv {list} -- list of script arguments
314 
315  Returns:
316  validation_response <dictionary>: if any arguments validation error,
317  return a json error: code is non zero
318  OR
319  respons_list <list>: list of json response for each batch
320  of email recipients
321 
322  Raises:
323  SystemExit
324  BaseException
325  """
326 
327  return_code = 0
328  sender_context = {}
329  validation_response = {
330  'status': '000',
331  'error': [],
332  'response': {},
333  'context': sender_context
334  }
335 
336  recipient_to = []
337  recipient_cc = []
338  recipient_bcc = []
339 
340  # Get argument parser
341  parser = getParser()
342 
343  try:
344  args = parser.parse_args(argv)
345  sender_context = args.context
346 
347  # Validation
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"
351  " to be provided.")
352  else:
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
356 
357  if (args.plaintext == None and args.htmltext == None):
358  raise BaseException ("Email body has to be specified")
359 
360  except (BaseException) as e:
361  return_code = 102
362  evalue = str(e)
363  if (evalue != "0"): # If it is empty, it is a help message
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))
369 
370  except (Exception) as e:
371  return_code = 103
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))
376 
377  # exit if validation error
378  if return_code > 0:
379  return return_code
380 
381  # prepare email groups and send emails
382  recipient_generator = batch_recipients(recipient_to,
383  recipient_cc,
384  recipient_bcc)
385 
386  for email_group in recipient_generator:
387  if sum(map(len, email_group)) > 0:
388  batch_success = send_email_batch(email_group, sender_context, args)
389  # mark that aws ses failed to process some batches or all messages
390  # only used for script exit code
391  if not batch_success:
392  return_code = 1
393 
394  return return_code
395 
396 

◆ run()

def sendSESEmailBulk.run ( )
Script entrypoint

Definition at line 397 of file sendSESEmailBulk.py.

397 def run():
398  """Script entrypoint"""
399  sys.exit(main(sys.argv[1:]))
400 
401 

◆ send_email_batch()

def sendSESEmailBulk.send_email_batch (   email_group,
  context,
  args 
)
This sends emails to a batch of to, cc and bcc recipients.

Args:
    email_group: group of to, cc and bcc email recipients; total
             should be less than or equal to AWS_SES_EMAIL_LIMIT
Returns:
    batch_return: a dictionary of status, error and response force
                  each email group

Definition at line 213 of file sendSESEmailBulk.py.

213 def send_email_batch(email_group, context, args):
214  """
215  This sends emails to a batch of to, cc and bcc recipients.
216 
217  Args:
218  email_group: group of to, cc and bcc email recipients; total
219  should be less than or equal to AWS_SES_EMAIL_LIMIT
220  Returns:
221  batch_return: a dictionary of status, error and response force
222  each email group
223 
224  """
225  success = True
226  response = {}
227 
228  batch_return = {
229  'status': '000',
230  'error': [],
231  'response': {},
232  'context': context
233  }
234 
235  try:
236 
237  # Create boto3 client object
238  client = boto3.client('ses', region_name=AWS_REGION)
239 
240  # Construct destination
241  destination = {}
242 
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]
246 
247  # Construct message
248  charset = "UTF-8"
249  message = {}
250  message["Subject"] = {}
251  message["Subject"]["Data"] = args.subject
252  message["Subject"]["Charset"] = charset
253 
254  if (args.plaintext != None):
255  message["Body"] = {}
256  message["Body"]["Text"] = {}
257  message["Body"]["Text"] ["Data"] = args.plaintext
258  message["Body"]["Text"] ["Charset"] = charset
259 
260  if (args.htmltext != None):
261  if "Body" not in message:
262  message ["Body"] = {}
263 
264  message["Body"]["Html"] = {}
265  message["Body"]["Html"]["Data"] = args.htmltext
266  message["Body"]["Html"]["Charset"] = charset
267 
268  if (args.replyto != None):
269  # Call AWS SES
270  response = client.send_email (
271  Source = args.efrom,
272  Destination = destination,
273  Message = message,
274  ReplyToAddresses = args.replyto,
275  ConfigurationSetName='ses-detail-logging',
276  )
277  else:
278  # Call AWS SES
279  response = client.send_email (
280  Source = args.efrom,
281  Destination = destination,
282  Message = message,
283  ConfigurationSetName='ses-detail-logging',
284  )
285 
286  except ClientError as e:
287  success = False
288  batch_return["status"] = 902
289  batch_return["error"] = str(e)
290 
291  except Exception as e:
292  success = False
293  batch_return['status'] = 903
294  batch_return['error'] = e
295 
296 
297  batch_return['response'] = response
298 
299  # log response with requestId, messageId and calling context for each batch
300  # AWS S3 store AWS Kinesis Data Firehose events regarding SES send-email
301  if success:
302  LOGGER.info(batch_return)
303  else:
304  LOGGER.error(batch_return)
305 
306  return success
307 
308 

Variable Documentation

◆ format

tuple sendSESEmailBulk.format
Initial value:
1 = ("[%(levelname)-5s] %(asctime)-15s ({}|%(funcName)s|%(lineno)d) "
2  ":: %(message)s").format(__file__)

Definition at line 47 of file sendSESEmailBulk.py.

◆ USAGE_HELP

tuple sendSESEmailBulk.USAGE_HELP
Initial value:
1 = ("Usage: sendSESEmailBulk.py [-h] [--to TO [TO ...]] "
2  "[--bcc BCC [BCC ...]] [--cc CC [CC ...]] --efrom EFROM"
3  "[--replyto REPLYTO [REPLYTO ...]] --subject SUBJECT"
4  "[--plaintext PLAINTEXT] [--htmltext HTMLTEXT]"
5  "[--quiet] [--context CONTEXT]")

Definition at line 56 of file sendSESEmailBulk.py.