1 /* 2 * This file is based on the original SES module for Nodemailer by dfellis 3 * https://github.com/andris9/Nodemailer/blob/11fb3ef560b87e1c25e8bc15c2179df5647ea6f5/lib/engines/SES.js 4 */ 5 6 // NB! Amazon SES does not allow unicode filenames on attachments! 7 8 var http = require('http'), 9 https = require('https'), 10 crypto = require('crypto'), 11 urllib = require("url"); 12 13 // Expose to the world 14 module.exports = SESTransport; 15 16 /** 17 * <p>Generates a Transport object for Amazon SES</p> 18 * 19 * <p>Possible options can be the following:</p> 20 * 21 * <ul> 22 * <li><b>AWSAccessKeyID</b> - AWS access key (required)</li> 23 * <li><b>AWSSecretKey</b> - AWS secret (required)</li> 24 * <li><b>ServiceUrl</b> - optional API endpoint URL (defaults to <code>"https://email.us-east-1.amazonaws.com"</code>) 25 * </ul> 26 * 27 * @constructor 28 * @param {Object} options Options object for the SES transport 29 */ 30 function SESTransport(options){ 31 this.options = options || {}; 32 33 //Set defaults if necessary 34 this.options.ServiceUrl = this.options.ServiceUrl || "https://email.us-east-1.amazonaws.com"; 35 } 36 37 /** 38 * <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p> 39 * 40 * @param {Object} emailMessage MailComposer object 41 * @param {Function} callback Callback function to run when the sending is completed 42 */ 43 SESTransport.prototype.sendMail = function(emailMessage, callback) { 44 45 //Check if required config settings set 46 if(!this.options.AWSAccessKeyID || !this.options.AWSSecretKey) { 47 return callback(new Error("Missing AWS Credentials")); 48 } 49 50 this.generateMessage(emailMessage, (function(err, email){ 51 if(err){ 52 return callback(err); 53 } 54 this.handleMessage(email, callback); 55 }).bind(this)); 56 }; 57 58 /** 59 * <p>Compiles and sends the request to SES with e-mail data</p> 60 * 61 * @param {String} email Compiled raw e-mail as a string 62 * @param {Function} callback Callback function to run once the message has been sent 63 */ 64 SESTransport.prototype.handleMessage = function(email, callback) { 65 var request, 66 67 date = new Date(), 68 69 urlparts = urllib.parse(this.options.ServiceUrl), 70 71 params = this.buildKeyValPairs({ 72 'Action': 'SendRawEmail', 73 'RawMessage.Data': (new Buffer(email, "utf-8")).toString('base64'), 74 'Version': '2010-12-01', 75 'Timestamp': this.ISODateString(date) 76 }), 77 78 reqObj = { 79 host: urlparts.hostname, 80 path: urlparts.path || "/", 81 method: "POST", 82 headers: { 83 'Content-Type': 'application/x-www-form-urlencoded', 84 'Content-Length': params.length, 85 'Date': date.toUTCString(), 86 'X-Amzn-Authorization': 87 ['AWS3-HTTPS AWSAccessKeyID='+this.options.AWSAccessKeyID, 88 "Signature="+this.buildSignature(date.toUTCString(), this.options.AWSSecretKey), 89 "Algorithm=HmacSHA256"].join(",") 90 } 91 }; 92 93 //Execute the request on the correct protocol 94 if(urlparts.protocol.substr() == "https:") { 95 request = https.request(reqObj, this.responseHandler.bind(this, callback)); 96 } else { 97 request = http.request(reqObj, this.responseHandler.bind(this, callback)); 98 } 99 request.end(params); 100 }; 101 102 /** 103 * <p>Handles the response for the HTTP request to SES</p> 104 * 105 * @param {Function} callback Callback function to run on end (binded) 106 * @param {Object} response HTTP Response object 107 */ 108 SESTransport.prototype.responseHandler = function(callback, response) { 109 var body = ""; 110 response.setEncoding('utf8'); 111 112 //Re-assembles response data 113 response.on('data', function(d) { 114 body += d.toString(); 115 }); 116 117 //Performs error handling and executes callback, if it exists 118 response.on('end', function(err) { 119 if(err instanceof Error) { 120 return callback && callback(err, null); 121 } 122 if(response.statusCode != 200) { 123 return callback && 124 callback(new Error('Email failed: ' + response.statusCode + '\n' + body), null); 125 } 126 return callback && callback(null, { 127 message: body 128 }); 129 }); 130 }; 131 132 /** 133 * <p>Compiles the messagecomposer object to a string.</p> 134 * 135 * <p>It really sucks but I don't know a good way to stream a POST request with 136 * unknown legth, so the message needs to be fully composed as a string.</p> 137 * 138 * @param {Object} emailMessage MailComposer object 139 * @param {Function} callback Callback function to run once the message has been compiled 140 */ 141 142 SESTransport.prototype.generateMessage = function(emailMessage, callback) { 143 var email = ""; 144 145 emailMessage.on("data", function(chunk){ 146 email += (chunk || "").toString("utf-8"); 147 }); 148 149 emailMessage.on("end", function(chunk){ 150 email += (chunk || "").toString("utf-8"); 151 callback(null, email); 152 }); 153 154 emailMessage.streamMessage(); 155 }; 156 157 /** 158 * <p>Converts an object into a Array with "key=value" values</p> 159 * 160 * @param {Object} config Object with keys and values 161 * @return {Array} Array of key-value pairs 162 */ 163 SESTransport.prototype.buildKeyValPairs = function(config){ 164 var keys = Object.keys(config).sort(), 165 keyValPairs = [], 166 key, i, len; 167 168 for(i=0, len = keys.length; i < len; i++) { 169 key = keys[i]; 170 if(key != "ServiceUrl") { 171 keyValPairs.push((encodeURIComponent(key) + "=" + encodeURIComponent(config[key]))); 172 } 173 } 174 175 return keyValPairs.join("&"); 176 }; 177 178 /** 179 * <p>Uses SHA-256 HMAC with AWS key on date string to generate a signature</p> 180 * 181 * @param {String} date ISO UTC date string 182 * @param {String} AWSSecretKey ASW secret key 183 */ 184 SESTransport.prototype.buildSignature = function(date, AWSSecretKey) { 185 var sha256 = crypto.createHmac('sha256', AWSSecretKey); 186 sha256.update(date); 187 return sha256.digest('base64'); 188 }; 189 190 /** 191 * <p>Generates an UTC string in the format of YYY-MM-DDTHH:MM:SSZ</p> 192 * 193 * @param {Date} d Date object 194 * @return {String} Date string 195 */ 196 SESTransport.prototype.ISODateString = function(d){ 197 return d.getUTCFullYear() + '-' + 198 this.strPad(d.getUTCMonth()+1) + '-' + 199 this.strPad(d.getUTCDate()) + 'T' + 200 this.strPad(d.getUTCHours()) + ':' + 201 this.strPad(d.getUTCMinutes()) + ':' + 202 this.strPad(d.getUTCSeconds()) + 'Z'; 203 }; 204 205 /** 206 * <p>Simple padding function. If the number is below 10, add a zero</p> 207 * 208 * @param {Number} n Number to pad with 0 209 * @return {String} 0 padded number 210 */ 211 SESTransport.prototype.strPad = function(n){ 212 return n<10 ? '0'+n : n; 213 }; 214 215