PyLa-Mail
Send email with Python, AWS Lambda
Use case:
We’ve setup a static website with a simple contact form. We could use something like Formspree.io to handle the posted email form to forward the message to us, however, we would rather handle our own email relay than send it through a 3rd party.
We want CORS and a custom domain used to prevent cross site script posting messages and use throttling to prevent getting flooded with messages.
Assumptions:
- You have access to setup whatever you need in an AWS account.
- You have a registered domain registered through AWS and managed in Route 53.
- You will extend the Python script to add any additional security and validation you deem necessary.
- You already have an email address confirmed with SES that you can send emails to.
Overview:
- Setup an AWS Lambda function to handle the basic message processing with AWS SES.
- Connect AWS API Gateway to handle form post and pass the data on the AWS Lambda.
- Setup a form to post the data to AWS API Gateway.
Reference reading
1. Setup the Lambda function
-
Go to AWS Lambda and create a Python 3.6 function. After creating the function, you’ll be presented with a code editor.
-
Use the following code in the code editor for lambda_function.py:
import json
import boto3
import os
import base64
import urllib.parse
def lambda_handler(event, context):
body = event['body']
# decode the submitted body of data
d_body = base64.b64decode(body).decode('UTF-8')
parts = d_body.split('&')
data = {}
# parse the difference parts of the submitted data
for part in parts:
data[part.split('=')[0]] = part.split('=')[1]
# call the function to send the email using boto3
if send_mail(data['email'], data['name'], data['message']):
redirect_url = os.environ['PATH_PASS']
else:
redirect_url = os.environ['PATH_FAIL']
# return and redirect as needed
return {
"statusCode": 303,
"headers": {
"content-type": "text/html; charset=utf-8",
"location": redirect_url
}
}
def send_mail(email, name, message):
client = boto3.client('ses')
CHARSET = "UTF-8"
try:
#Provide the contents of the email.
response = client.send_email(
Destination={
'ToAddresses': [
os.environ['SES_EMAIL_TO'],
],
},
Message={
'Body': {
'Text': {
'Charset': CHARSET,
'Data': "Email from " + \
urllib.parse.unquote(email) + "\n\n" + \
urllib.parse.unquote_plus(message),
}
},
'Subject': {
'Charset': CHARSET,
'Data': 'Message from ' + name,
},
},
Source=os.environ['SES_EMAIL_TO']
)
except Exception as e:
print('failed to send email')
# print('Error:' + e.response['Error']['Message'])
print(e)
print('--end-error')
return False
else:
print("Email sent; Message ID: " + response['MessageId'])
return True
-
Add environment variables for the function. These are key value pairs (examples):
-
A place to redirect on failure (error)
PATH_FAIL
= https://www.example.com/contact-oh-no/ -
A place to redirect the user on success
PATH_PASS
= https://www.example.com/contact-thanks/ -
An email address to the send the email to (see Assumptions above)
SES_EMAIL_TO
= mike@example.com
-
-
There is no reason why this simple script should not run quickly so set the timeout to 3 seconds.
-
For “Concurrency” throttle the function as needed. For testing purposes, you should just set “Reserve concurrency” to 2.
-
Save the Lambda function.
2. Connect API Gateway to trigger Lambda
Extra reading
- Setting up custom domain names for HTTP APIs
- How do I define a custom domain name for my API Gateway API?
Pre-requisite
Make sure that you have a TLS cert for securing the API Gateway call. You can use a wildcard cert or a sub-domain (recommended). Use AWS Certificate Manager to create one if you dont already have one.
-
In the Lambda function designer, click the “Add trigger” button which will create all the API Gateway settings you need.
- Select “API Gateway” as the trigger to use.
- For security, we are going to choose “Open” this time then click the “Add” button to continue.
-
Now that we are forwarded back to the designer, we will see “pyla-mail-API” was added under “API Gateway”
-
We want to make sure that the API only handles form posts, so in the API Gateway screen, under Develop, go to Routes and select the /pyla-mail route and change it from
ANY
toPOST
. -
In API Gateway click “Custom domain names”. Create a custom domain that matches your TLS cert. Use a Regional cert; TLS 1.2. with the proper ACM certificate selected and click the “Create” button.
-
Now that the custom domain name is created it needs an API Mapping so that your custom domain name will be used for this API. Select the domain name you just created to see the “Domain name details”. At the bottom under “API mappings” click the “Configure API mappings” button, then “Add new mapping”. For API choose “pyla-mail-API”;
$default
for the Stage andlambda
for the Path, then save it. since we set$default
for the Stage, and notdefault
, we wont actually have to use “default” to indicate the Stage name in the URL. -
You will see that it provided an “API Gateway domain name”. You will need to go to Route53 and setup a CNAME record from the custom domain name to point to that.
-
Before we test with our custom domain, lets make sure that the function can be triggered from API Gateway using the URL that AWS provides. Go to the “pyla-mail-API” in API Gateway and for the
$default
stage, copy the “Invoke URL” to use in a CURL call. For example:
curl -i -d 'name=Name&email=sam@example.com&message=a message' \
https://abcdef1234.execute-api.us-west-2.amazonaws.com/pyla-mail
-
If all goes well, the response should be a 303 redirect to the URL set in in the
PATH_PASS
environment variable of the Lambda function. If not, check AWS CloudWatch for the error and fix as needed. -
Now we test a CURL call using our custom domain name. So for example, if we set the custom domain name up as: api.example.com we will use the following CURL call:
curl -i -d 'name=Name&email=sam@example.com&message=a message' \
https://api.example.com/lambda/pyla-mail
We should get the same result.
3. Final considerations
As needed:
- Add additional security and CORS configurations.
- In API Gateway, look at Protect > Throttling to setup throttling for your routes. You dont want to get spammed from your own form handler and incur unwanted charges.
- Consider adding a CAPTCHA.