├── email-templates └── templates.yml ├── .gitignore ├── config.js ├── package.json ├── README.md └── index.js /email-templates/templates.yml: -------------------------------------------------------------------------------- 1 | # Templates are in yaml 1.2, format http://yaml.org/ 2 | # Templates should be placed in an S3 bucket whose settings are in config.js 3 | 4 | default: 5 | subject: Now you're doing the Lambada 6 | text: | 7 | Thank you for playing! 8 | See you soon. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temp editor files 2 | *~ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | build 14 | test 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = { 4 | "AWS_API_VERSION" : "2006-03-01", 5 | "appBucket" : "your-bucket", 6 | "submissionsKeyPrefix" : "path/to/submissions", 7 | "processedKeyPrefix" : "path/to/processed/", 8 | "emailTemplatesKey" : "path/to/templates.yml", 9 | "emailMetaKey" : "email", 10 | "defaultEmailTo" : "you@example.com", 11 | "defaultEmailFrom" : "you@example.com", 12 | "defaultEmailSubject" : "Thank You", 13 | "defaultEmailText" : "Thank You", 14 | "SES_STMP_USERNAME" : "YOUR_SMTP_USERNAME", 15 | "SES_SMTP_PASSWORD" : "YOUR_SMTP_PASSWORD", 16 | } 17 | 18 | module.exports = config 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambdaSubmissionFunction", 3 | "version": "1.0.0", 4 | "description": "Amazon Lambda Function for processing S3 bucket events", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:rmauge/aws-lambda-s3-email.git" 12 | }, 13 | "author": "Raymond Mauge ", 14 | "license": "MIT", 15 | "private": true, 16 | "dependencies": { 17 | "async": "^0.9.0", 18 | "js-yaml": "^3.2.7", 19 | "nodemailer": "^1.3.0", 20 | "nodemailer-ses-transport": "^1.2.0", 21 | "nodemailer-smtp-transport": "^0.1.13", 22 | "util": "^0.10.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AWS Lambda Quick start 2 | ========= 3 | 4 | This service processes documents sent to S3. It makes use of Amazon Lambda and S3 bucket events. 5 | Here is the official walkthrough for [configuring S3 and Lambda](http://docs.aws.amazon.com/lambda/latest/dg/walkthrough-s3-events-adminuser.html). 6 | The Setup steps below are only a summary for ongoing development. 7 | This code and setup is further explained in my blog https://peekandpoke.wordpress.com/2015/02/26/dancing-the-lambada-with-aws-lambda-or-sending-emails-on-s3-events/ 8 | 9 | # Development Setup and Testing 10 | 11 | 1. Install pip 12 | * Download: curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py 13 | * Install: sudo python get-pip.py 14 | 2. Use a python virtual environment 15 | * Install: sudo pip install virtualenv 16 | * Create: virtualenv lambda-env 17 | * Activate: source lambda-env/bin/activate 18 | 3. Install node using a virtual environment 19 | * Install nodeenv: pip install nodeenv 20 | * Install node: nodeenv -p --node=0.10.32 nenv 21 | * Update npm (installed with node): npm install npm -g 22 | 4. Install Amazon AWS [cli tools](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-set-up.html) 23 | * Install: pip install awscli 24 | * Configure: aws configure 25 | 5. Update Lambda Function 26 | * Clone project: git@github.com:rmauge/aws-lambda-s3-email.git 27 | * Zip directory: 28 | ``` 29 | cd aws-lambda-s3-email/ && zip -r ../aws-lambda-s3-email.zip . -x build/ test/ *~ *.git* .gitignore && cd .. 30 | ``` 31 | * Submit update: 32 | ``` 33 | aws lambda upload-function 34 | --region us-east-1 35 | --function-name lambdaSubmissionFunction 36 | --function-zip aws-lambda-s3-email.zip 37 | --role arn:aws:iam::99999999:role/lamdba_exec_role 38 | --mode event 39 | --handler index.handler 40 | --runtime nodejs 41 | --debug 42 | --timeout 10 43 | --memory-size 128 44 | ``` 45 | 6. Test function manually 46 | * 47 | ``` 48 | aws lambda invoke-async --function-name lambdaSubmissionFunction --region us-east-1 --invoke-args aws-lambda-s3-email/test/submissionexample.txt --debug 49 | ``` 50 | * When testing the key must exist in the S3 bucket 51 | 52 | # AWS Policies and Roles 53 | The Lambda Function requires certain roles/policies to work. These are: 54 | An IAM user to send email (ses-smtp-user) 55 | A role and policy allowing a bucket event to invoke a Lambda Function (lambda_invoke_role) 56 | A role and policy allowing the Lambda Function to access the S3 bucket (lamdba_exec_role) 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var aws = require('aws-sdk'); 4 | var async = require('async'); 5 | var util = require('util'); 6 | var yaml = require('js-yaml'); 7 | var nodemailer = require('nodemailer'); 8 | var smtpTransport = require('nodemailer-smtp-transport'); 9 | var sesTransport = require('nodemailer-ses-transport'); 10 | 11 | var SubmissionNotifier = (function(config, s3) { 12 | 13 | var defaultMailOptions = { 14 | "from" : config.defaultEmailFrom, 15 | "to" : config.defaultEmailTo, 16 | "subject" : config.defaultEmailSubject, 17 | "text" : config.defaultEmailText, 18 | }; 19 | 20 | var sesOptions = { 21 | accessKeyId: config.SES_STMP_USERNAME, 22 | secretAccessKey: config.SES_SMTP_PASSWORD, 23 | }; 24 | 25 | var validateEvent = function (event) { 26 | return function(next) { 27 | var bucketName = event.Records[0].s3.bucket.name; 28 | var key = event.Records[0].s3.object.key; // Keys contain values, Ex. '@' which may be encoded 29 | 30 | s3.getObject( 31 | { 32 | Bucket:bucketName, Key:decodeURIComponent(key) 33 | }, 34 | function(err, data) { 35 | if (err) { 36 | next('Error: Failed to get object ' + key + ' from bucket ' + bucketName + 37 | '. Make sure it exists and your bucket is in the same region as this function. \n' + err); 38 | } else { 39 | var prefix = key.substr(0, config.submissionsKeyPrefix.length); 40 | if (prefix === config.submissionsKeyPrefix) { // Only process keys with this prefix 41 | var filename = key.substr(key.lastIndexOf('/') + 1); 42 | console.log('Filename: ', filename); 43 | console.log('Email: ', data.Metadata[config.emailMetaKey]); 44 | console.log('Content Type: ', data.ContentType); 45 | next(null, key, filename, data); 46 | } else { 47 | next('The object key: ' + key + ' is not valid for this process'); 48 | } 49 | } 50 | }); 51 | }; 52 | }; 53 | 54 | var moveKey = function(key, filename, submissionKeyData, next) { 55 | s3.copyObject( 56 | { 57 | CopySource: config.appBucket + '/' + key, 58 | Bucket: config.appBucket, 59 | Key: decodeURIComponent(config.processedKeyPrefix + filename), 60 | }, 61 | function(err, templatesKeyData) { 62 | if (err) { 63 | next('Error: Failed to copy object ' + key + ' from bucket ' + config.appBucket + 64 | '. Make sure it exists and your bucket is in the same region as this function. \n' + err); 65 | } else { 66 | s3.deleteObject( 67 | { 68 | Bucket: config.appBucket, 69 | Key: decodeURIComponent(key), 70 | }, 71 | function(err, data) { 72 | if (err) { 73 | next('Error: Failed to delete ' + key + ' from bucket ' + config.appBucket + 74 | '. Make sure it exists and your bucket is in the same region as this function. \n' + err); 75 | } else { 76 | next(null, submissionKeyData); 77 | } 78 | }); 79 | } 80 | }); 81 | }; 82 | 83 | var getEmailTemplates = function (submissionKeyData, next) { 84 | s3.getObject( 85 | { 86 | Bucket:config.appBucket, Key:config.emailTemplatesKey 87 | }, 88 | function(err, templatesKeyData) { 89 | if (err) { 90 | next('Error: Failed to get email templates object ' + emailTemplatesKey + ' from bucket ' + config.appBucket + 91 | '. Make sure it exists and your bucket is in the same region as this function. \n' + err); 92 | } else { 93 | var templates = yaml.safeLoad(templatesKeyData.Body); 94 | next(null, templates, submissionKeyData); 95 | } 96 | }); 97 | }; 98 | 99 | var sendEmail = function(templates, submissionKeyData, next) { 100 | console.log('Sending message..'); 101 | 102 | var email; 103 | if (submissionKeyData.Metadata[config.emailMetaKey]) { 104 | email = submissionKeyData.Metadata[config.emailMetaKey]; 105 | } else { 106 | email = config.defaultEmailFrom; 107 | } 108 | 109 | var template = templates["defalt"]; // use default template for now 110 | 111 | var mailOptions = { 112 | "from" : config.defaultEmailFrom, 113 | "to" : email + ',' + config.defaultEmailFrom, 114 | "subject" : template["subject"], 115 | "text" : template["text"], 116 | }; 117 | 118 | var transporter = nodemailer.createTransport(sesTransport(sesOptions)); 119 | transporter.sendMail(mailOptions, function(err, info){ 120 | if (err) { 121 | next('Error: Sending email failed: ' + err) 122 | } else { 123 | next(null, info.response); 124 | } 125 | }); 126 | }; 127 | 128 | var ensurePrefix = function(prefix, key) { 129 | return key.substr(0, prefix.length) === prefix; 130 | }; 131 | 132 | return { 133 | validateEvent: validateEvent, 134 | moveKey: moveKey, 135 | getEmailTemplates: getEmailTemplates, 136 | sendEmail: sendEmail, 137 | }; 138 | 139 | }); 140 | 141 | exports.handler = function(event, context) { 142 | 143 | var config = require('./config.js'); 144 | var s3 = new aws.S3({apiVersion: config.AWS_API_VERSION}); 145 | var app = new SubmissionNotifier(config, s3); 146 | console.log('Received event:\n', util.inspect(event, {depth:5})); 147 | 148 | async.waterfall( 149 | [ 150 | app.validateEvent(event), 151 | app.moveKey, 152 | app.getEmailTemplates, 153 | app.sendEmail, 154 | ], 155 | function (err, result) { 156 | if (err) { 157 | console.error(err); 158 | } else { 159 | console.log('Processes completed successfully: ' + result); 160 | } 161 | context.done(); 162 | } 163 | ); 164 | }; 165 | --------------------------------------------------------------------------------