2011-11-24 21:51:49 +00:00
|
|
|
var stream = require('stream');
|
|
|
|
var util = require('util');
|
|
|
|
var fs = require('fs');
|
|
|
|
var os = require('os');
|
|
|
|
var path = require('path');
|
|
|
|
var CRLF = "\r\n";
|
|
|
|
var MIMECHUNK = 76; // MIME standard wants 76 char chunks when sending out.
|
|
|
|
var counter = 0;
|
2011-02-23 21:23:37 +00:00
|
|
|
|
|
|
|
var generate_boundary = function()
|
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
var text = "";
|
|
|
|
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
|
2011-02-23 21:23:37 +00:00
|
|
|
|
2011-09-25 21:51:29 +00:00
|
|
|
for(var i=0; i < 69; i++)
|
|
|
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
2011-02-23 21:23:37 +00:00
|
|
|
|
2011-09-25 21:51:29 +00:00
|
|
|
return text;
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
2011-02-24 23:02:24 +00:00
|
|
|
var Message = function(headers)
|
2011-02-23 21:23:37 +00:00
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
this.attachments = [];
|
|
|
|
this.html = null;
|
2011-11-16 01:52:13 +00:00
|
|
|
this.header = {"message-id":"<" + (new Date()).getTime() + "." + (counter++) + "." + process.pid + "@" + os.hostname() +">"};
|
2011-09-25 21:51:29 +00:00
|
|
|
this.content = "text/plain; charset=utf-8";
|
|
|
|
|
|
|
|
for(var header in headers)
|
|
|
|
{
|
|
|
|
// allow user to override default content-type to override charset or send a single non-text message
|
|
|
|
if(/^content-type$/i.test(header))
|
|
|
|
{
|
|
|
|
this.content = headers[header];
|
|
|
|
}
|
|
|
|
else if(header == 'text')
|
|
|
|
{
|
|
|
|
this.text = headers[header];
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// allow any headers the user wants to set??
|
|
|
|
// if(/cc|bcc|to|from|reply-to|sender|subject|date|message-id/i.test(header))
|
|
|
|
this.header[header.toLowerCase()] = headers[header];
|
|
|
|
}
|
|
|
|
}
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Message.prototype =
|
|
|
|
{
|
|
|
|
|
2011-12-04 01:41:32 +00:00
|
|
|
attach: function(options/*{path, type, name, headers}*/)
|
2011-09-25 21:51:29 +00:00
|
|
|
{
|
2011-12-04 01:41:32 +00:00
|
|
|
/* legacy support */
|
|
|
|
if (arguments.length > 1)
|
|
|
|
options = {path:options, type:arguments[1], name:arguments[2]};
|
|
|
|
|
|
|
|
this.attachments.push(options);
|
2011-09-25 21:51:29 +00:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
attach_alternative: function(html, charset)
|
|
|
|
{
|
|
|
|
this.html = {message:html, charset:charset || "utf-8"};
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
valid: function(callback)
|
|
|
|
{
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
if(!self.header.from)
|
|
|
|
{
|
|
|
|
callback(false, "message does not have a valid sender");
|
|
|
|
}
|
|
|
|
if(!self.header.to)
|
|
|
|
{
|
|
|
|
callback(false, "message does not have a valid recipient");
|
|
|
|
}
|
|
|
|
else if(self.attachments.length === 0)
|
|
|
|
{
|
|
|
|
callback(true);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
var failed = [];
|
|
|
|
|
|
|
|
self.attachments.forEach(function(attachment, index)
|
|
|
|
{
|
|
|
|
path.exists(attachment.path, function(exists)
|
|
|
|
{
|
|
|
|
if(!exists)
|
|
|
|
failed.push(attachment.path + " does not exist");
|
|
|
|
|
|
|
|
if(index + 1 == self.attachments.length)
|
|
|
|
callback(failed.length === 0, failed.join(", "));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
stream: function()
|
|
|
|
{
|
|
|
|
return new MessageStream(this);
|
|
|
|
},
|
|
|
|
|
|
|
|
read: function(callback)
|
|
|
|
{
|
|
|
|
var buffer = "";
|
|
|
|
|
|
|
|
var capture = function(data)
|
|
|
|
{
|
|
|
|
buffer += data;
|
|
|
|
};
|
|
|
|
|
|
|
|
var output = function(err)
|
|
|
|
{
|
|
|
|
callback(err, buffer);
|
|
|
|
};
|
|
|
|
|
|
|
|
var str = this.stream();
|
|
|
|
|
|
|
|
str.on('data', capture);
|
|
|
|
str.on('end', output);
|
|
|
|
str.on('error', output);
|
|
|
|
}
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
var MessageStream = function(message)
|
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
stream.Stream.call(self);
|
|
|
|
|
|
|
|
self.message = message;
|
2011-11-16 01:52:13 +00:00
|
|
|
self.readable = true;
|
|
|
|
self.resume = null;
|
|
|
|
self.paused = false;
|
2011-09-25 21:51:29 +00:00
|
|
|
self.stopped = false;
|
2011-11-16 01:52:13 +00:00
|
|
|
self.stream = null;
|
2011-09-25 21:51:29 +00:00
|
|
|
|
|
|
|
var output_process = function(next)
|
|
|
|
{
|
|
|
|
var check = function()
|
|
|
|
{
|
|
|
|
if(self.stopped)
|
|
|
|
return;
|
|
|
|
|
|
|
|
else if(self.paused)
|
|
|
|
self.resume = next;
|
|
|
|
|
|
|
|
else
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
|
|
|
|
process.nextTick(check);
|
|
|
|
};
|
|
|
|
|
|
|
|
var output_mixed = function()
|
|
|
|
{
|
|
|
|
var data = [];
|
|
|
|
var boundary = generate_boundary();
|
|
|
|
|
|
|
|
self.emit('data', ["Content-Type: multipart/mixed; boundary=\"", boundary, "\"", CRLF, CRLF].join(""));
|
|
|
|
output_process(function() { output_message(-1, boundary); });
|
|
|
|
};
|
|
|
|
|
|
|
|
var output_message = function(index, boundary)
|
|
|
|
{
|
|
|
|
var next = function()
|
|
|
|
{
|
|
|
|
output_process(function() { output_message(index + 1, boundary); });
|
|
|
|
};
|
|
|
|
|
|
|
|
if(index < 0)
|
|
|
|
{
|
|
|
|
self.emit('data', ["--", boundary, CRLF].join(""));
|
|
|
|
|
|
|
|
if(self.message.html)
|
|
|
|
{
|
|
|
|
output_process(function() { output_alternatives(next); });
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
output_text(next);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if(index < self.message.attachments.length)
|
|
|
|
{
|
|
|
|
self.emit('data', ["--", boundary, CRLF].join(""));
|
|
|
|
output_process(function() { output_attachment(self.message.attachments[index], next); });
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
self.emit('data', ["--", boundary, "--", CRLF, CRLF].join(""));
|
|
|
|
self.emit('end');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var output_alternatives = function(next)
|
|
|
|
{
|
|
|
|
var boundary = generate_boundary();
|
|
|
|
|
|
|
|
self.emit('data', ["Content-Type: multipart/alternative; boundary=\"", boundary, "\"", CRLF, CRLF].join(""));
|
|
|
|
self.emit('data', ["--", boundary, CRLF].join(""));
|
|
|
|
|
|
|
|
output_text(function(){});
|
|
|
|
|
|
|
|
var data = ["--", boundary, CRLF];
|
|
|
|
|
|
|
|
data = data.concat(["Content-Type:text/html; charset=", self.message.html.charset, CRLF, "Content-Transfer-Encoding: base64", CRLF]);
|
|
|
|
data = data.concat(["Content-Disposition: inline", CRLF, CRLF]);
|
2011-11-24 17:54:13 +00:00
|
|
|
|
2011-11-24 21:51:49 +00:00
|
|
|
self.emit('data', data.join(""));
|
2011-11-24 18:06:19 +00:00
|
|
|
|
2011-11-24 21:51:49 +00:00
|
|
|
output_chunk(new Buffer(self.message.html.message).toString("base64"));
|
2011-09-25 21:51:29 +00:00
|
|
|
|
2011-11-24 21:51:49 +00:00
|
|
|
self.emit('data', [CRLF, "--", boundary, "--", CRLF, CRLF].join(""));
|
2011-09-25 21:51:29 +00:00
|
|
|
next();
|
|
|
|
};
|
|
|
|
|
|
|
|
var output_attachment = function(attachment, next)
|
|
|
|
{
|
2011-12-04 01:41:32 +00:00
|
|
|
var keys = (attachment.headers ? Object.keys(attachment.headers) : []),
|
|
|
|
data = new Array(10/*default headers*/+(4*keys.length)),
|
|
|
|
hasType = false,
|
|
|
|
hasXferEncoding = false,
|
|
|
|
hasDisposition = false,
|
|
|
|
d = 0;
|
|
|
|
|
|
|
|
for(var k=0,m,len=keys.length; k<len; ++k)
|
|
|
|
{
|
|
|
|
if(m = keys[k].match(/^content-(type|transfer-encoding|disposition)$/i))
|
|
|
|
{
|
|
|
|
switch(m[1].toLowerCase())
|
|
|
|
{
|
|
|
|
case 'type':
|
|
|
|
hasType = true;
|
|
|
|
break;
|
|
|
|
case 'transfer-encoding':
|
|
|
|
hasXferEncoding = true;
|
|
|
|
break;
|
|
|
|
case 'disposition':
|
|
|
|
hasDisposition = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
data[d++] = keys[k];
|
|
|
|
data[d++] = ': ';
|
|
|
|
data[d++] = attachment.headers[keys[k]];
|
|
|
|
data[d++] = CRLF;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!hasType)
|
|
|
|
{
|
|
|
|
data[d++] = 'Content-Type: ';
|
|
|
|
data[d++] = attachment.type;
|
|
|
|
data[d++] = CRLF;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!hasXferEncoding)
|
|
|
|
{
|
|
|
|
data[d++] = 'Content-Transfer-Encoding: base64';
|
|
|
|
data[d++] = CRLF;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!hasDisposition)
|
|
|
|
{
|
|
|
|
data[d++] = 'Content-Disposition: attachment; filename="';
|
|
|
|
data[d++] = attachment.name;
|
|
|
|
data[d++] = '"';
|
|
|
|
data[d++] = CRLF;
|
|
|
|
}
|
|
|
|
|
|
|
|
data[d] = CRLF;
|
2011-09-25 21:51:29 +00:00
|
|
|
|
|
|
|
self.emit('data', data.join(""));
|
2011-12-04 01:41:32 +00:00
|
|
|
|
2011-11-24 21:51:49 +00:00
|
|
|
var chunk = MIMECHUNK*25*3; // 5700
|
|
|
|
var buffer = new Buffer(chunk);
|
|
|
|
var opened = function(err, fd)
|
2011-09-25 21:51:29 +00:00
|
|
|
{
|
|
|
|
if(!err)
|
|
|
|
{
|
|
|
|
var read = function(err, bytes)
|
|
|
|
{
|
|
|
|
if(self.paused)
|
|
|
|
{
|
|
|
|
self.resume = function() { read(err, bytes); };
|
|
|
|
}
|
|
|
|
else if(self.stopped)
|
|
|
|
{
|
|
|
|
fs.close(fd);
|
|
|
|
}
|
|
|
|
else if(!err)
|
|
|
|
{
|
2011-11-25 00:20:37 +00:00
|
|
|
var data = buffer.toString("base64", 0, bytes);
|
|
|
|
var leftover= data.length % MIMECHUNK;
|
|
|
|
output_chunk(data);
|
2011-09-25 21:51:29 +00:00
|
|
|
|
|
|
|
if(bytes == chunk) // gauranteed no leftovers
|
|
|
|
{
|
|
|
|
fs.read(fd, buffer, 0, chunk, null, read);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2011-11-25 00:20:37 +00:00
|
|
|
self.emit('data', leftover ? data.substr(-leftover) + CRLF + CRLF : CRLF); // important!
|
2011-09-25 21:51:29 +00:00
|
|
|
fs.close(fd, next);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
fs.close(fd);
|
|
|
|
self.emit('error', err);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
fs.read(fd, buffer, 0, chunk, null, read);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
self.emit('error', err);
|
|
|
|
};
|
|
|
|
|
|
|
|
fs.open(attachment.path, 'r+', opened);
|
|
|
|
};
|
|
|
|
|
2011-11-24 21:51:49 +00:00
|
|
|
var output_chunk = function(data)
|
|
|
|
{
|
|
|
|
var loops = Math.round(data.length / MIMECHUNK);
|
|
|
|
|
|
|
|
for(var step = 0; step < loops; step++)
|
|
|
|
{
|
|
|
|
self.emit('data', data.substring(step*MIMECHUNK, MIMECHUNK*(step + 1)) + CRLF);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-09-25 21:51:29 +00:00
|
|
|
var output_text = function(next)
|
|
|
|
{
|
|
|
|
var data = ["Content-Type:", self.message.content, CRLF, "Content-Transfer-Encoding: 7bit", CRLF];
|
|
|
|
data = data.concat(["Content-Disposition: inline", CRLF, CRLF]);
|
|
|
|
data = data.concat([self.message.text || "", CRLF, CRLF]);
|
|
|
|
|
|
|
|
self.emit('data', data.join(""));
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
|
|
|
|
var output_data = function()
|
|
|
|
{
|
|
|
|
// are there attachments or alternatives?
|
|
|
|
if(self.message.attachments.length || self.message.html)
|
|
|
|
{
|
|
|
|
self.emit('data', "MIME-Version: 1.0" + CRLF);
|
|
|
|
output_process(output_mixed);
|
|
|
|
}
|
|
|
|
|
|
|
|
// otherwise, you only have a text message
|
|
|
|
else
|
|
|
|
{
|
|
|
|
output_text(function() { self.emit('end'); });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var output_header = function()
|
|
|
|
{
|
|
|
|
var data = [];
|
|
|
|
|
|
|
|
for(var header in self.message.header)
|
|
|
|
{
|
|
|
|
// do not output BCC in the headers...
|
|
|
|
if(!(/bcc/i.test(header)))
|
|
|
|
data = data.concat([header, ": ", self.message.header[header], CRLF]);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.emit('data', data.join(''));
|
|
|
|
output_process(output_data);
|
|
|
|
};
|
|
|
|
|
|
|
|
output_process(output_header);
|
|
|
|
return;
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
MessageStream.prototype.pause = function()
|
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
self.paused = true;
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
MessageStream.prototype.resume = function()
|
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
self.paused = false;
|
|
|
|
|
|
|
|
if(self.resume)
|
|
|
|
{
|
|
|
|
var resume = self.resume;
|
|
|
|
self.resume = null;
|
|
|
|
resume();
|
|
|
|
}
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
MessageStream.prototype.destroy = function()
|
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
self.stopped = true;
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
MessageStream.prototype.destroySoon = function()
|
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
self.stopped = true;
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
util.inherits(MessageStream, stream.Stream);
|
|
|
|
|
|
|
|
exports.Message = Message;
|
2011-03-01 08:27:35 +00:00
|
|
|
exports.create = function(headers)
|
2011-02-23 21:23:37 +00:00
|
|
|
{
|
2011-09-25 21:51:29 +00:00
|
|
|
return new Message(headers);
|
2011-02-23 21:23:37 +00:00
|
|
|
};
|