Odyssey
sendSESEmail.py
1 #!/usr/bin/env python
2 
3 """Send emails through AWS SES.
4 
5 Uses the AWS SES call send-mail.
6 Has the following parameters:
7 
8 Body contents: (at least one of the following has to be specified.)
9 - text -- the plain text version of the email.
10 - html -- the HTML-formatted version of the email.
11 
12 - subject -- the subject of the email. This is required.
13 - efrom -- the from email. This is required.
14 The efrom requires verification by AWS SES (either by email address or by domain.)
15 - replyto -- the reply to email. (submitform.pl & submitsecure.pl)
16 
17 Email destinations: (at least one of the following has to be specified.)
18 Destinations are space-separated like a@comp.dom b@comp.dom ...
19 - to -- a list of to recipients. (Who the email is actually addressed to.)
20 - bcc -- a list of bcc recipients. (List isn't visible in email.)
21 - cc -- the cc for the email. (Visible list in email.)
22 
23 Amazon requires the from email to be verified or the domain verified.
24 """
25 
26 import sys
27 import boto3
28 import os
29 import json
30 import argparse
31 from botocore.exceptions import ClientError
32 import textwrap
33 import logging
34 
35 format=("[%(levelname)-5s] %(asctime)-15s ({}|%(funcName)s|%(lineno)d) "
36  ":: %(message)s").format(__file__)
37 LOGGER = logging.getLogger(__name__)
38 LOGGER.setLevel(logging.DEBUG)
39 logging_handler = logging.StreamHandler()
40 logging_handler.setFormatter(logging.Formatter(format))
41 LOGGER.addHandler(logging_handler)
42 
43 # Global settings
44 USAGE_HELP = ("Usage: sendSESEmail.py [-h] [--to TO [TO ...]]"
45  "[--bcc BCC [BCC ...]] [--cc CC [CC ...]] --efrom EFROM"
46  "[--replyto REPLYTO [REPLYTO ...]] --subject SUBJECT"
47  "[--plaintext PLAINTEXT] [--htmltext HTMLTEXT]"
48  "[--quiet] [--context CONTEXT]")
49 
50 # Use us-west-2 as a default region
51 # This should work for local development, alpha stack(us-east-2) and prod
52 AWS_REGION = os.getenv('AWS_REGION', 'us-west-2')
53 
54 
55 class CustomArgumentParser(argparse.ArgumentParser):
56  """Custom ArgumentParser class
57 
58  Extends:
59  argparse.ArgumentParser
60  """
61 
62  def error(self, message):
63  """Suppressing default error method
64 
65  Return custom error message on ArgumentParser.error.
66 
67  Arguments:
68  message -- error message
69 
70  Raises:
71  BaseException
72  """
73  raise BaseException(message)
74 
75 
76 def getParser():
77  """Prepare argument parser.
78 
79  Returns:
80  parser -- ArgumentParser object
81  """
82  parser = CustomArgumentParser (
83  description = "Send emails through AWS SES.",
84  formatter_class = argparse.RawDescriptionHelpFormatter,
85  epilog = textwrap.dedent('''\
86  additional information:
87  One of --to, --bcc, or --cc has to be defined.
88  One of --plaintext or --htmltext has to be defined.
89  --efrom and --subject are required.
90  '''))
91 
92  parser.add_argument ("--to", nargs = "+",
93  help = "Space-separated list of to email addresses",
94  required = False)
95 
96  parser.add_argument ("--bcc", nargs = "+",
97  help = "Space-separated list of bcc email addresses",
98  required = False)
99 
100  parser.add_argument ("--cc", nargs = "+",
101  help = "Space-separated list of cc email addresses",
102  required = False)
103 
104  parser.add_argument ("--efrom",
105  help = "From email address",
106  required = True)
107 
108  parser.add_argument ("--replyto", nargs = "+",
109  help = "Reply to email address",
110  required = False)
111 
112  parser.add_argument ("--subject",
113  help = "Subject of the email",
114  required = True)
115 
116  parser.add_argument ("--plaintext",
117  help = "Email body in plaintext",
118  required = False)
119 
120  parser.add_argument ("--htmltext",
121  help = "Html body in HTML",
122  required = False)
123 
124  parser.add_argument("--quiet", action = "store_true",
125  help = "Suppress success message",
126  required = False)
127 
128  parser.add_argument("--context",
129  help = ("Json string for logging application"
130  "context for the email messages being sent."),
131  required = False)
132 
133 
134  return parser
135 
136 
137 def main(argv):
138  """Main method to execute operations on email verification.
139 
140  Arguments:
141  argv {list} -- list of script arguments
142 
143  Raises:
144  SystemExit
145  BaseException
146  """
147  return_code = 0
148  response = {}
149  mainReturn = {
150  'status': '000',
151  'error': [],
152  'response': response,
153  'context': {}
154  }
155 
156  # Get argument parser
157  parser = getParser()
158 
159  try:
160  args = parser.parse_args(argv)
161  mainReturn["context"] = args.context
162 
163  # Validation
164  if (args.to == None and args.bcc == None and args.cc == None):
165  raise BaseException ("Destination has to be specified. One of"
166  " the --to, --bcc or --cc options need"
167  " to be provided.")
168 
169  if (args.plaintext == None and args.htmltext == None):
170  raise BaseException ("Email body has to be specified")
171 
172  except (BaseException) as e:
173  return_code = 102
174  evalue = str(e)
175  if (evalue != "0"): # If it is empty, it is a help message
176  mainReturn["status"] = return_code
177  mainReturn["error"].append(USAGE_HELP)
178  mainReturn["error"].append(evalue)
179  LOGGER.error(json.dumps(mainReturn))
180 
181  except (Exception) as e:
182  return_code = 103
183  mainReturn["status"] = return_code
184  mainReturn["error"] = e
185  LOGGER.error(json.dumps(mainReturn))
186 
187  # exit if validation error
188  if return_code > 0:
189  return return_code
190 
191  try:
192  # Create boto3 client object
193  client = boto3.client('ses', region_name=AWS_REGION)
194 
195  # Construct destination
196  destination = {}
197  if (args.to != None):
198  destination["ToAddresses"] = args.to
199  if (args.bcc != None):
200  destination["BccAddresses"] = args.bcc
201  if (args.cc != None):
202  destination["CcAddresses"] = args.cc
203 
204  # Construct message
205  charset = "UTF-8"
206  message = {}
207  message["Subject"] = {}
208  message["Subject"]["Data"] = args.subject
209  message["Subject"]["Charset"] = charset
210  if (args.plaintext != None):
211  message["Body"] = {}
212  message["Body"]["Text"] = {}
213  message["Body"]["Text"]["Data"] = args.plaintext
214  message["Body"]["Text"]["Charset"] = charset
215 
216  if (args.htmltext != None):
217  if "Body" not in message:
218  message["Body"] = {}
219 
220  message["Body"]["Html"] = {}
221  message["Body"]["Html"]["Data"] = args.htmltext
222  message["Body"]["Html"]["Charset"] = charset
223 
224  if (args.replyto != None):
225  # Call AWS SES
226  response = client.send_email (
227  Source = args.efrom,
228  Destination = destination,
229  Message = message,
230  ReplyToAddresses = args.replyto,
231  ConfigurationSetName='ses-detail-logging',
232  )
233  else:
234  # Call AWS SES
235  response = client.send_email (
236  Source = args.efrom,
237  Destination = destination,
238  Message = message,
239  ConfigurationSetName='ses-detail-logging',
240  )
241 
242  except ClientError as e:
243  return_code = 902
244  mainReturn["status"] = return_code
245  mainReturn["error"] = str(e)
246 
247  except Exception as e:
248  return_code = 903
249  mainReturn["status"] = return_code
250  mainReturn['error'] = str(e)
251 
252  mainReturn['response'] = response
253 
254  if return_code > 0:
255  LOGGER.error(json.dumps(mainReturn))
256  else:
257  LOGGER.info(json.dumps(mainReturn))
258 
259  return return_code
260 
261 
262 def run():
263  """Script entrypoint"""
264  sys.exit(main(sys.argv[1:]))
265 
266 
267 if __name__ == "__main__":
268  run()
def main(argv)
def getParser()
Definition: sendSESEmail.py:76