├── package.json ├── LICENSE ├── README.md └── mailuploader.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailuploader", 3 | "description": "Parse raw e-mail contents and upload as a POST request", 4 | "version": "0.1.0", 5 | "author" : "Andris Reinman", 6 | "maintainers":[ 7 | { 8 | "name":"andris", 9 | "email":"andris@node.ee" 10 | } 11 | ], 12 | "repository" : { 13 | "type" : "git", 14 | "url" : "http://github.com/andris9/mailuploader.git" 15 | }, 16 | "main" : "./mailuploader", 17 | "licenses" : [ 18 | { 19 | "type": "MIT", 20 | "url": "http://github.com/andris9/mailuploader/blob/master/LICENSE" 21 | } 22 | ], 23 | "dependencies": { 24 | "mailparser": "*", 25 | "fetch": "*" 26 | }, 27 | "engines": ["node >=0.4.0"], 28 | "keywords": ["e-mail", "mime", "parser"] 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mailuploader 2 | 3 | Parse a raw e-mail message and post the contents (including attachments) to an URL. 4 | 5 | Attachments are uploaded as files, other fields (subject line, text and html body etc.) as 6 | regular POST fields. 7 | 8 | ## Usage 9 | 10 | Use the following function 11 | 12 | mailUploader(emailFile, targetUrl[, options], callback) 13 | 14 | Where 15 | 16 | * **emailFile** is a file on disk, that contains a mime encoded e-mail message 17 | * **targetUrl** is the URL the POST request is made to 18 | * **options** is an optional options parameter 19 | * **callback** is the function that is run after the data is uploaded to `targetUrl` 20 | 21 | Example 22 | 23 | var mailUploader = require("./test").mailUploader; 24 | 25 | mailUploader("email.eml", "http://example.com/receive.php", function(err){ 26 | console.log(err || "SUCCESS!"); 27 | }); 28 | 29 | ### Receiving URL 30 | 31 | A POST request is made to the `targetUrl` after the e-mail has been parsed. The POST fields used are the following: 32 | 33 | * **from** - the sender of the email in the form of `"email; Sender Name"` 34 | * **to** - receivers for the email in the form of `"email; Sender Name"` - can be in multiple lines when several recipients are defined 35 | * **subject** - the subject line of the email 36 | * **htmlBody** - HTML body of the message 37 | * **textBody** - plaintext body of the message 38 | * **file[x]** - attached file where `x` is an incrementing number (useful for PHP which automatically makes an array out of it) 39 | 40 | **NB!** all the POST fields (except attachments which are binary) are converted automatically to UTF-8, regardless of the original encoding 41 | 42 | ### Inline images in HTML body 43 | 44 | HTML body can include inline images which are pointing to an attachment. In this case the `cid` url is always in the 45 | following form: `cid:SHA1(filename)@node` where `filename` is the filename defined with an attached file. 46 | 47 | ### options 48 | 49 | options parameter can be used wit the following properties 50 | 51 | * **tempDir** (defualts to '/tmp') a directory, where temporary files (attachmetns and such) will be written 52 | * **additionalFields** is an object for setting additional POST parameters. For example `{user:"test"}` adds a value with key `user` and value `"test"` to the POST request 53 | 54 | ### callback 55 | 56 | Callback function gets 3 parameters 57 | 58 | function(err, meta, body){} 59 | 60 | Where 61 | 62 | * **err** is an Error object if an error occured 63 | * **meta** is the headers object of the `targetUrl` 64 | * **body** is the body of the `targetUrl` 65 | 66 | ## License 67 | 68 | **MIT** 69 | -------------------------------------------------------------------------------- /mailuploader.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"), 2 | crypto = require("crypto"), 3 | MailParser = require("mailparser").MailParser, 4 | fetch = require("fetch"); 5 | 6 | module.exports.mailUploader = mailUploader; 7 | 8 | function mailUploader(fileName, targetUrl, options, callback){ 9 | if(!callback && typeof options=="function"){ 10 | callback = options; 11 | options = undefined; 12 | } 13 | new MailUploader(fileName, targetUrl, options, callback).parse(); 14 | } 15 | 16 | /** 17 | * 18 | * 19 | * @constructor 20 | * @param {String} fileName 21 | * @param {String} targetUrl 22 | * @param {Object} [options] 23 | * @param {Function} readyCallback 24 | */ 25 | function MailUploader(fileName, targetUrl, options, readyCallback){ 26 | options = options || {}; 27 | 28 | this.inputFileName = fileName; 29 | this.targetUrl = targetUrl; 30 | this.readyCallback = readyCallback; 31 | 32 | this.tempDir = options.tempDir || "/tmp/"; 33 | 34 | this.additionalFields = options.additionalFields; 35 | 36 | if(this.tempDir.substr(-1)!="/"){ 37 | this.tempDir += "/"; 38 | } 39 | 40 | this.boundary = "-----MAILPARSER" + Date.now(); 41 | this.responseFileName = sha1(this.boundary+Math.random()); 42 | 43 | this.parsingStreams = 1; 44 | this.mailparser = new MailParser({streamAttachments: true}); 45 | this.mailparser.on("attachment", this.handleIncomingAttachment.bind(this)); 46 | this.mailparser.on("end", this.mailparserEnd.bind(this)); 47 | } 48 | 49 | MailUploader.prototype.parse = function(){ 50 | fs.createReadStream(this.inputFileName).pipe(this.mailparser); 51 | } 52 | 53 | MailUploader.prototype.handleIncomingAttachment = function(attachment){ 54 | attachment.usedFileName = sha1(attachment.generatedFileName); 55 | 56 | attachment.saveStream = fs.createWriteStream(this.tempDir + attachment.usedFileName); 57 | attachment.stream.pipe(attachment.saveStream); 58 | 59 | this.parsingStreams++; 60 | 61 | attachment.stream.on("end", (function(){ 62 | this.parsingStreams--; 63 | if(!this.parsingStreams){ 64 | process.nextTick(this.handleFinalMail.bind(this)); 65 | } 66 | }).bind(this)); 67 | } 68 | 69 | MailUploader.prototype.mailparserEnd = function(mail){ 70 | this.parsingStreams--; 71 | this.parsedMail = mail; 72 | 73 | if(!this.parsingStreams){ 74 | process.nextTick(this.handleFinalMail.bind(this)); 75 | } 76 | } 77 | 78 | MailUploader.prototype.handleFinalMail = function(){ 79 | 80 | this.cidList = {}; 81 | this.parsedMail.attachments.forEach((function(attachment){ 82 | if(attachment.contentId){ 83 | this.cidList[attachment.contentId] = attachment.generatedFileName; 84 | } 85 | }).bind(this)); 86 | 87 | if(this.parsedMail.attachments && this.parsedMail.attachments.length){ 88 | if(this.parsedMail.html){ 89 | this.parsedMail.html = this.handleCID(this.parsedMail.html); 90 | } 91 | if(this.parsedMail.alternatives){ 92 | for(var i=0, len = this.parsedMail.alternatives.length; i= this.parsedMail.attachments.length){ 147 | this.responseStream.end("--"+this.boundary+"--"); 148 | this.sendToURL(); 149 | return; 150 | } 151 | var attachment = this.parsedMail.attachments[this.curAttachment], 152 | attachmentStream = fs.createReadStream(this.tempDir + attachment.usedFileName); 153 | 154 | this.responseStream.write("--"+this.boundary + "\r\n"+ 155 | "Content-Disposition: form-data; name=\"file["+this.curAttachment+"]\"; filename=\""+(attachment.generatedFileName.replace(/"/g,"\\\""))+"\"\r\n"+ 156 | "Content-Type: "+attachment.contentType+"\r\n"+ 157 | "\r\n"); 158 | 159 | attachmentStream.on("data", (function(chunk){ 160 | if(this.responseStream.write(chunk) === false){ 161 | attachmentStream.pause(); 162 | } 163 | }).bind(this)); 164 | 165 | this.responseStream.on("drain", (function(){ 166 | attachmentStream.resume(); 167 | }).bind(this)); 168 | 169 | attachmentStream.on("end", (function(){ 170 | fs.unlink(this.tempDir + attachment.usedFileName); 171 | this.responseStream.write("\r\n"); 172 | this.curAttachment++; 173 | process.nextTick(this.writeAttachments.bind(this)); 174 | }).bind(this)); 175 | } 176 | 177 | MailUploader.prototype.addFormField = function(name, value){ 178 | return "--"+this.boundary + "\r\n"+ 179 | "Content-Disposition: form-data; name=\""+(name.replace(/"/g,"\\\""))+"\"\r\n"+ 180 | "\r\n"+ 181 | value+"\r\n"; 182 | } 183 | 184 | MailUploader.prototype.handleCID = function(html){ 185 | return html.replace(/(['"])cid:([^'"]+)(['"])/g, (function(match, quoteStart, cid, quoteEnd){ 186 | if(cid in this.cidList){ 187 | return quoteStart+"cid:"+sha1(this.cidList[cid])+"@node"+quoteEnd; 188 | }else{ 189 | return match; 190 | } 191 | }).bind(this)); 192 | } 193 | 194 | MailUploader.prototype.sendToURL = function(){ 195 | var stream = fs.createReadStream(this.tempDir+this.responseFileName); 196 | stream.pause(); 197 | 198 | fs.stat(this.tempDir+this.responseFileName, (function(err, stat){ 199 | if(err || !stat.isFile()){ 200 | this.readyCallback(new Error("Error saving file to disk")); 201 | return; 202 | } 203 | fetch.fetchUrl(this.targetUrl, { 204 | headers:{ 205 | "content-type":"multipart/form-data; boundary=" + this.boundary 206 | }, 207 | payloadSize: stat.size, 208 | payloadStream: stream 209 | }, (function(err, meta, body){ 210 | fs.unlink(this.tempDir+this.responseFileName); 211 | this.readyCallback(err, meta, body); 212 | }).bind(this)); 213 | }).bind(this)); 214 | 215 | } 216 | 217 | function sha1(str){ 218 | var hash = crypto.createHash("sha1"); 219 | hash.update(str); 220 | return hash.digest("hex"); 221 | } 222 | --------------------------------------------------------------------------------