Note: I'm currently not using this contact form on this site, although it worked well. I just haven't adjusted it to work with nextJS yet.
I needed a contacts form for my website and didn’t want to run a virtual server to host the processing. Saurabh Shrivastava’s article on the AWS blog site gave me all the information I required. However, a few things have changed in the AWS console and I’ve added a couple of features:-
- constants such as receiver email address are stored in lambda parameters so they don’t have to be set in the lambda code; and
- google captcha checking at the back-end to avoid someone getting round the captcha by writing their own form;
Background
The structure of this solution is an HTML form, with some javascript that JSON-encodes the submitted data and sends it to an AWS API Gateway, which passes the data on to a javascript (nodeJS) AWS lambda function. The Lambda function verifies the Captcha data with Google's API and if that's OK, it sends an email to the contact form recipient using AWS SES.Components
There are three main AWS components here:-- AWS Lambda - The business logic about what to do with the form data;
- API Gateway - An HTTP interface to Lambda; and
- AWS SES - The mail system used to send the mail notification
- An HTML form; and
- A javascript function to bundle the form data, captch data into JSON and send
Code
I'll start with the Lambda, HTML form and front end javascript first.The Lambda
This is where the back end work gets done. It get's the JSON encoded form data from the user's browser, via the API Gateway.
So my lambda code is:-
'use strict';
const AWS = require('aws-sdk');
/* Use SES in same region as lambda is running */
const ses = new AWS.SES({region: process.env.AWS_REGION});
/* this is boilerplate setup when you create the function */
exports.handler = async (lamdaEvent) => {
console.log('Received event:', lamdaEvent);
// email to and from
const RECEIVER = process.env.RECEIVER;
const SENDER = process.env.SENDER;
// You get this from google when you settup captcha
const googleSecret = process.env.GOOGLE_SECRET;
/* The JSON from the client comes in as JSON encoded text inside lamdaEvent.body*/
const eventbody = JSON.parse(lamdaEvent.body);
/* Function to call google to check that the captcha response is valid. Better
* than simply relying on code running in the browser */
const googleCaptchaProcFun = (resolve, reject) => {
const queryString = require('querystring');
const requestData = queryString.stringify( {
secret: googleSecret,
response: eventbody.capt
} );
const https = require('https');
const options = {
hostname: 'www.google.com',
port: 443,
path: '/recaptcha/api/siteverify',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': requestData.length
}
};
const request = https.request( options, (response ) => {
response.on('data', (data) => {
console.log( 'Verified Google Captcha' );
let responseObject = JSON.parse( data );
if (responseObject.success){
resolve( 'verified ' + data);
}else{
reject( 'not verified ' + data )
}
});
} );
request.on('data', (data) => {
console.log( 'Sending verify', data );
});
request.write( requestData );
request.end();
};
const formContactName = eventbody.firstname + " " + eventbody.lastname;
const mailParams = {
Destination: {
ToAddresses: [
RECEIVER
]
},
Message: {
Body: {
Text: {
Data: 'name: ' + formContactName +
'phone: ' + eventbody.tel +
'email: ' + eventbody.email +
'Website: ' + eventbody.website +
'LinkedIn: ' + eventbody.linkedin +
'desc: ' + eventbody.note,
Charset: 'UTF-8'
}
},
Subject: {
Data: 'Website Referral Form: ' + formContactName,
Charset: 'UTF-8'
}
},
Source: SENDER
};
const sendMailFun = (resolve, reject) => {
console.log('Sending mail with name: ', formContactName);
ses.sendEmail(mailParams, (err, data) => {
if( err ){
console.log('Sending error:', err);
reject( err );
}else{
console.log('Email sent');
resolve( data );
}
} );
};
/* As it's asynchronous, the lambda must return a promise */
const response =
new Promise(googleCaptchaProcFun)
.then((result) => {
return new Promise(sendMailFun);
}).then((result) => {
console.log('Promises reloved');
return {result: "ok" };
}).catch((err) => {
console.log('Error', err);
return {result: "Error"};
});
return response;
};
The HTML Form
<head>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<script src="js/contact.js"></script>
</head>
…
<form id="form" method="post">
<fieldset>
<legend>Your details</legend>
<div class="form-group" >
<label for="firstname" >First Name</label>
<input type="text" name="firstname" id="firstname"
placeholder="First Name" class="form-control" />
</div>
<div class="form-group">
<label for="lastname" >Last Name</label>
<input type="text" name="lastname" id="lastname"
placeholder="Last Name" class="form-control" />
</div>
<div class="form-group">
<label for="tel" >Telephone</label>
<input type="tel" name="tel" id="tel"
placeholder="+442079460000" class="form-control" />
</div>
<div class="form-group">
<label for="email" >Email</label>
<input type="email" name="email" id="email"
placeholder="name@example" class="form-control" />
</div>
<div class="form-group">
<label for="website" >Website</label>
<input type="url" name="website" id="website"
placeholder="https://example/" class="form-control" />
</div>
<div class="form-group">
<label for="linkedin" >Linkedin</label>
<input type="url" name="linkedin" id="linkedin"
placeholder="https://uk.linkedin.com/" class="form-control" />
</div>
</fieldset>
<div class="form-group">
<label for="note" >How can I help you?</label>
<textarea rows="3" name="note" id="note"
placeholder="I'd like to make contact..." class="form-control" ></textarea>
</div>
<fieldset>
<legend>You're not a robot, are you?</legend>
<div class="g-recaptcha" data-sitekey="6LeoP9Q….N9N"></div>
</fieldset>
<button type="submit" class="btn btn-primary" >Submit</button>
<output id="info" ></output>
</form>
The Front End Script
window.addEventListener('load', (event) => {
const URL = "https://…./contactFormMail";
let form = document.getElementById('form');
let infoArea = document.getElementById('info');
form.addEventListener('submit', (event) => {
event.preventDefault();
let firstname = document.getElementById('firstname').value;
let lastname = document.getElementById('lastname').value;
let tel = document.getElementById('tel').value;
let email = document.getElementById('email').value;
let website = document.getElementById('website').value;
let linkedin = document.getElementById('linkedin').value;
let note = document.getElementById('note').value;
let data = {
'firstname': firstname,
'lastname': lastname,
'tel': tel,
'email': email,
'website': website,
'linkedin': linkedin,
'note': note,
'capt': grecaptcha.getResponse()
};
let http = new XMLHttpRequest();
output = (message) => {
console.log( message );
infoArea.value = message;
};
http.onreadystatechange = (status) => {
if(http.readyState === XMLHttpRequest.DONE){
let status = http.status;
if (status === 0 || (status >= 200 && status < 400)) {
if ("ok" === JSON.parse(http.responseText).result){
output("Thanks. Your message has been sent");
form.disabled = true;
}else{
output("Sorry. There was a problem.");
}
} else {
output("Sorry. There was a problem.");
}
}
};
http.open("POST", URL, true);
http.setRequestHeader("Content-type", "application/json; charset=utf-8");
http.send(JSON.stringify(data));
});
});
The AWS Components
Create the Lambda
In the ASW Lambda console page, create a Lambda function. I've assumed that you have created a role for the Lambda to assume, that has permission to send emails through SES.
Edit in the code and parameters.
Create the API Gateway
Choose add integration:- Enter a name and click “Add Integration” and select Lambda and then your lambda function.
Create a route for POST requests to /contactFormMail go to the Lambda integration:-
Go with the default stage and click next:-
Configure Cross Origin Sharing. This tells the gateway to respond to CORS OPTIONS requests to say that certain webpages can connect to this Gateway. This doesn't verify that the connection is coming from your own script; it just allows the browser to make this check and most browsers will do this properly. The filtering against robots is the Captcha however.
Conclusion
It's possible to create a contact form for your website by linking API Gateway, to a Lambda function and the to SES to send you an email.
The above is basically how to do it the long way.
References
Saurabh Shrivastava’s article on theAWS blog got me to my first working solution.
Randy Findley has done something similar but build it all conveniently into a Cloudformation Stack; see:article.
Derek Woods covers the detail of setting up a domain in SES; seearticle.