Click here to Skip to main content
15,881,852 members
Articles / All Topics

Mail Automation with AWS Lambda and SNS

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
9 Oct 2015CPOL3 min read 11.1K   3  
Mail Automation with AWS Lambda and SNS

UPDATE: Yesterday (October 8th, 2015), Amazon announced official support for scheduled events, so I updated my function to use this feature. For the most up-to-date version of this project, please visit the updated version.

I have a great accountant but he has one flaw: I have to ask for the invoice every month! While waiting for him to automate the process, I decided to automate what I can on my end. There are many ways to skin a cat, as the saying goes, the way I picked for this task was developing an AWS Lambda function and trigger it by subscribing to a public SNS topic.

Step 1: Prepare a Function to Send Emails

Developing a simple node.js function that sends e-mails was simple. First, I needed the install two modules:

JavaScript
npm install nodemailer
npm install nodemailer-smtp-transport

And the function is straightforward:

JavaScript
var transporter = nodemailer.createTransport(smtpTransport({
    host: 'email-smtp.eu-west-1.amazonaws.com',
    port: 587,
    auth: {
        user: '{ACCESS KEY}',
        pass: '{SECRET KEY}'
    }
}));

var text = 'Hi, Invoice! Thanks!';

var mailOptions = {
    from: 'from@me.net',
    to: 'to@someone.net',
    bcc: 'me2@me.com',
    subject: 'Invoice',
    text: text 
};

transporter.sendMail(mailOptions, function(error, info){
      if(error){
        console.log(error);
      }else{
        console.log('Message sent');
      }
  });

The challenge was deployment as the script had some dependencies. If you choose Edit Inline and just paste the script, you would get an error like this:

JavaScript
"errorMessage": "Cannot find module 'nodemailer'",

But it's very easy to deploy a full package with dependencies. Just zip everything in the folder (without the folder itself) and upload the zip file. The downside of this method is that you can no longer edit the code inline. So even just for fixing a trivial typo, you need to re-zip and re-upload.

Step 2: Schedule the Process

One simple method to schedule the process is to invoke the method using Powershell and schedule a task to run the script:

JavaScript
Invoke-LMFunction -FunctionName automatedEmails -AccessKey accessKey -SecretKey secretKey -Region eu-west-1

But I don't want a dependency on any machine (local or EC2 instance). Otherwise, I could write a few lines of code in C# to do the same job anyway. The idea of using Lambda is to avoid maintenance and let everything run on infrastructure that's maintained by AWS.

Unreliable Town Clock

Unfortunately AWS doesn't provide an easy method to schedule Lambda function invocations. For the sake of simplicity, I decided to use Unreliable Town Clock (UTC) which is essentially a public SNS topic that sends "chime" messages every 15 minutes.

Image 1

Since all I need is one email, I don't care if it skips a beat or two as long as it chimes at least once throughout the day.

State Management

Of course, to avoid bombarding my accountant with emails, I have to maintain a state so that I would only send one email per month. But Lambda functions must be stateless. Some alternatives are using AWS S3 or DynamoDB. Since all I need is one simple integer value, I decided to store in a text file on S3. So first, I download the log file and check the last sent email month:

JavaScript
function downloadLog(next) {
    s3.getObject({
            Bucket: bucketName,
            Key: fileName
        },
        next);

function checkDate(response, next) {
    var currentDay = parseInt(event.Records[0].Sns.Message.day);
    currentMonth = parseInt(event.Records[0].Sns.Message.month);
    var lastMailMonth = <span class="nb">parseInt(response.Body.toString());
    if (<span class="nb">isNaN(lastMailMonth)) {
        lastMailMonth = currentMonth - 1;
    }
    if ((currentDay == targetDayOfMonth) && (currentMonth > lastMailMonth)) {
        next();
    }
}</span></span>

Putting It Together

So putting it all together, the final code is:

var bucketName = "{BUCKET_NAME}";
var fileName = "mail-automation-last-sent-date.txt";
var targetDayOfMonth = 7;

exports.handler = function(event, context) {

	var async = require('async');
	var AWS = require('aws-sdk');
	var s3 = new AWS.S3();
	var nodemailer = require('nodemailer');
	var smtpTransport = require('nodemailer-smtp-transport');
	var currentMonth;
	
	async.waterfall(
		[ function downloadLog(next) {
			s3.getObject({
					Bucket: bucketName,
					Key: fileName
				},
				next);
		},

		function checkDate(response, next) {
			var message = JSON.parse(event.Records[0].Sns.Message);
			var currentDay = parseInt(message.day);
			currentMonth = parseInt(message.month);
			var lastMailMonth = parseInt(response.Body.toString());
			if (isNaN(lastMailMonth)) {
				lastMailMonth = currentMonth - 1;
			}

			if ((currentDay == targetDayOfMonth) && (currentMonth > lastMailMonth)) {
				next();
			} else {
				context.done(null, 'No action needed');
			}
		},

		function sendMail(response, next) {
			var transporter = nodemailer.createTransport(smtpTransport({
			    host: 'email-smtp.eu-west-1.amazonaws.com',
			    port: 587,
			    auth: {
			        user: '{ACCESS KEY}',
			        pass: '{SECRET KEY}'
			    }
			}));

			var text = 'Hi, Invoice! Thanks!';
			var mailOptions = {
			    from: 'from@me.net',
			    to: 'to@someone.net',
			    bcc: 'me2@me.com',
			    subject: 'Invoice',
			    text: text 
			};
			
			transporter.sendMail(mailOptions, function(error, info){
		          if(error){
		              console.log(error);
		          }else{
		              s3.putObject({
							Bucket: bucketName,
							Key: fileName,
							Body: currentMonth.toString(),
							ContentType: "text/plain"
						},
						function(err, data) {
							console.log('Updated log');
							context.done(null, 'Completed')
						});
		          }
		      });
		} ], function (err) {
			if (err) {
				context.fail(err);
			} else {
				context.succeed('Success');
			}
		});
};

Let's see if it's going to help me get my invoices automatically!

Conclusion

  • A better approach would be to check emails for the invoice and send only if it wasn't received already. Also a copule of reminders after the initial email would be nice. But as my new resolution is to progress in small, incremental steps, I'll call it version 1.0 and leave the remaining tasks for a later version.

  • My main goal was to achieve this task without having to worry about the infrastructure. I still don't but that's only because a nice guy (namely Eric Hammond) decided to setup a public service for the rest of us.

  • During my research, I came across a few references saying that the same task can be done using AWS Simple Workflow (SWF). I haven't used this service before. It looked complicated and felt like there is a steep learning curve to go through. In Version 2, I should look into SWF which would...

    • allow me to handle a complex workflow
    • make dependency to public SNS topic redundant
    • handle state properly

Resources

CodeProject

This article was originally posted at http://volkanpaksoy.com

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
-- There are no messages in this forum --