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