Odyssey
verifySESEmail.py
1 #!/usr/bin/env python
2 
3 """Manage email verifications for CU Emails.
4 
5 Perform following operations on a list of emails:
6  - verifyEmails: Sends an email to each of the emails in the
7  list. Amazon verifies that the email is
8  syntaxically correct first. (Each email is
9  a call to AWS SES.)
10  - getVerification: Sends Amazon a list of emails and Amazon
11  returns a list of emails and the statuses.
12  (Emails that Amazon doesn't have are not
13  returned.)
14  - removeEmails: Sends a request to Amazon to remove email from
15  the list of identities. (Each email is a call
16  to AWS SES.)
17 
18 Default AWS_REGION is `us-west-2` but the tempalte can be created and verified
19 in other regions by setting up AWS_REGION env variable (check aws
20 docuementation for restrictions in certain regions.)
21 
22 As of 2018-10-02, `HomeCUVerificationEmailTemplate` is the default
23 custom tempalte being and already should exist in the expected aws
24 region before running this script.
25 
26 Fun fact, if you resend email verification request to an email which is already
27 verified, using the custom verification email template, aws ses will change
28 the status from verified to pending and the recipient will have to verify the
29 email again. This is not the case when you send email verifications using
30 aws's default process (i.e. not using custom verification email templates).
31 """
32 
33 import sys
34 import boto3
35 import os
36 import json
37 import argparse
38 from botocore.exceptions import ClientError
39 
40 # Global settings
41 USAGE_HELP = ("verifySESEmail.py -e \"email1 email2 email3 ... emailN\" "
42  "-a \"verifyEmails\"|\"getVerification\"|\"removeEmails\"")
43 
44 # Use us-west-2 as a default region
45 # This should work for local development, alpha stack(us-east-2) and prod
46 AWS_REGION = os.getenv('AWS_REGION', 'us-west-2')
47 
48 # Default custom email verification template name
49 CUSTOM_EMAIL_TEMPLATE = "HomeCUVerificationEmailTemplate"
50 
51 
52 class CustomArgumentParser(argparse.ArgumentParser):
53  """Custom ArgumentParser class
54 
55  Extends:
56  argparse.ArgumentParser
57  """
58 
59  def error(self, message):
60  """Suppressing default error method
61 
62  Return custom error message on ArgumentParser.error.
63 
64  Arguments:
65  message -- error message
66 
67  Raises:
68  SystemExit
69  """
70  custom_error = {
71  'status': '999',
72  'error': USAGE_HELP,
73  'response': {}
74  }
75  print(json.dumps(custom_error))
76  raise SystemExit(2)
77 
78 
79 def get_parser():
80  """Prepare argument parser.
81 
82  Returns:
83  parser -- ArgumentParser object
84  """
85  parser = CustomArgumentParser(
86  description="AWS SES email verification using custom template."
87  )
88 
89  parser.add_argument("-e", "--emails",
90  help="Space delimited list of emails",
91  required=True
92  )
93 
94  parser.add_argument("-a", "--action",
95  choices=["verifyEmails",
96  "getVerification", "removeEmails"],
97  required=True,
98  help=('Action of the script. '
99  '(i) "verifyEmails" -- Sends an email to '
100  'each of the `emails` in the list. Amazon '
101  'verifies that the email is syntaxically correct'
102  ' first. (Each email is a call to AWS SES. '
103  '(ii) "getVerification" -- Sends Amazon a list'
104  ' of emails and Amazon returns a list of emails'
105  ' and the statuses (Emails that Amazon doesn\'t'
106  ' have are not returned). '
107  '(iii) "removeEmails" -- Sends a request to '
108  'Amazon to remove email from the list of '
109  'identities (Each email is a call to AWS SES.))'
110  )
111  )
112 
113  parser.add_argument("-tn", "--template-name",
114  help="Provide a template name to use",
115  default=CUSTOM_EMAIL_TEMPLATE)
116 
117  return parser
118 
119 
120 def main(argv):
121  """Main method to execute operations on email verification.
122 
123  Arguments:
124  argv {list} -- list of script arguments
125 
126  Raises:
127  SystemExit
128  BaseException
129  """
130 
131  main_return = {
132  'status': '000',
133  'error': '',
134  'response': {}
135  }
136 
137  # Get argument parser
138  parser = get_parser()
139  try:
140  args = parser.parse_args(argv)
141  except (argparse.ArgumentError, Exception) as e:
142  main_return = {
143  'status': '999',
144  'error': e,
145  'response': {}
146  }
147  print(json.dumps(main_return))
148  raise SystemExit(2)
149 
150  response = None
151  excption = False
152 
153  try:
154 
155  # Create boto3 client object
156  client = boto3.client('ses', region_name=AWS_REGION)
157 
158  response = {}
159  # Future improvements: As of 2018-10-03, the following loop will halt
160  # if the invalid email is encountered and other remaining emails are
161  # not processed. Since this script is called after validating the
162  # emails on the client side, we should not encounter such use case
163  # scenario. Otherwise, changes in this script and changes from
164  # homecu/odyssey#175{0,1} need to be updated to handle such use case.
165  for email in args.emails.split():
166  # Verify email syntax
167  email = email.strip()
168 
169  if (args.action == "verifyEmails"):
170  try:
171  # Send verification email to email address using the
172  # custom verification email template
173  if (email not in response):
174  response[email] = client.send_custom_verification_email(
175  EmailAddress=email,
176  TemplateName=args.template_name
177  )
178  except ClientError as e:
179  response[email] = e.response["Error"]["Message"]
180  raise BaseException(email + " is not valid")
181 
182  elif (args.action == "removeEmails"):
183  try:
184  # Send request to delete email
185  if (email not in response):
186  response[email] = client.delete_identity(
187  Identity=email)
188  except ClientError as e:
189  response[email] = e.response["Error"]["Message"]
190  raise BaseException(email + " is not valid")
191 
192  elif (args.action == "getVerification"):
193  # remove duplicates from list
194  if (email not in response):
195  response[email] = True
196 
197  if (args.action == "getVerification"):
198  # Need to be "list" for client to accept it
199  response = list(response.keys())
200  try:
201  response = client.get_identity_verification_attributes(
202  Identities=response)
203 
204  except ClientError as e:
205  raise BaseException(e)
206 
207  except BaseException as e:
208  main_return['status'] = '999'
209  main_return['error'] = str(e)
210  excption = True
211  except Exception as e:
212  main_return['status'] = '999'
213  main_return['error'] = e
214  excption = True
215 
216  # SUCCESS
217  main_return['response'] = response
218  print (json.dumps(main_return))
219 
220  if excption:
221  raise SystemExit(2)
222 
223 
224 def run():
225  """Script entrypoint"""
226  main(sys.argv[1:])
227 
228 
229 if __name__ == "__main__":
230  run()