2018-05-14 23:02:16 +00:00
|
|
|
const fs = require('fs');
|
2018-06-25 02:42:29 +00:00
|
|
|
const { hostname } = require('os');
|
|
|
|
const { Stream } = require('stream');
|
2018-05-14 23:02:16 +00:00
|
|
|
const addressparser = require('addressparser');
|
2018-06-25 02:42:29 +00:00
|
|
|
const { mimeWordEncode } = require('emailjs-mime-codec');
|
2018-06-24 01:33:53 +00:00
|
|
|
const { getRFC2822Date } = require('./date');
|
|
|
|
|
2018-05-14 23:02:16 +00:00
|
|
|
const CRLF = '\r\n';
|
2018-06-28 02:56:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* MIME standard wants 76 char chunks when sending out.
|
2018-07-06 17:48:24 +00:00
|
|
|
* @type {76}
|
2018-06-28 02:56:26 +00:00
|
|
|
*/
|
|
|
|
const MIMECHUNK = 76;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* meets both base64 and mime divisibility
|
2018-07-06 17:48:24 +00:00
|
|
|
* @type {456}
|
2018-06-28 02:56:26 +00:00
|
|
|
*/
|
2018-07-06 17:48:24 +00:00
|
|
|
const MIME64CHUNK = /** @type {456} */ (MIMECHUNK * 6);
|
2018-06-28 02:56:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* size of the message stream buffer
|
2018-07-06 17:48:24 +00:00
|
|
|
* @type {12768}
|
2018-06-28 02:56:26 +00:00
|
|
|
*/
|
2018-07-06 17:48:24 +00:00
|
|
|
const BUFFERSIZE = /** @type {12768} */ (MIMECHUNK * 24 * 7);
|
2018-05-14 23:02:16 +00:00
|
|
|
|
2018-07-06 17:48:24 +00:00
|
|
|
/**
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2018-05-14 23:02:16 +00:00
|
|
|
let counter = 0;
|
2011-02-23 21:23:37 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @returns {string} the generated boundary
|
|
|
|
*/
|
2018-05-14 23:02:16 +00:00
|
|
|
function generate_boundary() {
|
2018-05-27 04:25:08 +00:00
|
|
|
let text = '';
|
|
|
|
const possible =
|
|
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
|
2011-02-23 21:23:37 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
for (let i = 0; i < 69; i++) {
|
|
|
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
2011-02-23 21:23:37 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
return text;
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {string} l the person to parse into an address
|
|
|
|
* @returns {string} the parsed address
|
|
|
|
*/
|
2018-05-14 23:02:16 +00:00
|
|
|
function person2address(l) {
|
2018-05-27 04:25:08 +00:00
|
|
|
return addressparser(l)
|
|
|
|
.map(({ name, address }) => {
|
|
|
|
return name
|
|
|
|
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
|
|
|
|
: address;
|
|
|
|
})
|
|
|
|
.join(', ');
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
2011-02-23 21:23:37 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {string} header_name the header name to fix
|
|
|
|
* @returns {string} the fixed header name
|
|
|
|
*/
|
2018-05-14 23:02:16 +00:00
|
|
|
function fix_header_name_case(header_name) {
|
2018-05-27 04:25:08 +00:00
|
|
|
return header_name
|
|
|
|
.toLowerCase()
|
|
|
|
.replace(/^(.)|-(.)/g, match => match.toUpperCase());
|
2012-06-10 07:37:13 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 23:02:16 +00:00
|
|
|
class Message {
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @typedef {Object} MessageHeaders
|
|
|
|
* @property {string?} content-type
|
|
|
|
* @property {string} [subject]
|
|
|
|
* @property {string} [text]
|
|
|
|
* @property {MessageAttachment} [attachment]
|
|
|
|
* @param {MessageHeaders} headers hash of message headers
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
constructor(headers) {
|
|
|
|
this.attachments = [];
|
2018-06-28 02:56:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {MessageAttachment}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
this.alternative = null;
|
|
|
|
this.header = {
|
|
|
|
'message-id': `<${new Date().getTime()}.${counter++}.${
|
|
|
|
process.pid
|
2018-06-25 02:42:29 +00:00
|
|
|
}@${hostname()}>`,
|
2018-06-22 01:18:26 +00:00
|
|
|
date: getRFC2822Date(),
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
this.content = 'text/plain; charset=utf-8';
|
2018-05-27 04:52:50 +00:00
|
|
|
for (const header in headers) {
|
2018-05-27 04:25:08 +00:00
|
|
|
// 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 if (
|
|
|
|
header === 'attachment' &&
|
|
|
|
typeof headers[header] === 'object'
|
|
|
|
) {
|
2018-07-06 18:07:19 +00:00
|
|
|
const attachment = headers[header];
|
|
|
|
if (Array.isArray(attachment)) {
|
|
|
|
for (let i = 0; i < attachment.length; i++) {
|
|
|
|
this.attach(attachment[i]);
|
2018-05-27 04:25:08 +00:00
|
|
|
}
|
|
|
|
} else {
|
2018-07-06 18:07:19 +00:00
|
|
|
this.attach(attachment);
|
2018-05-27 04:25:08 +00:00
|
|
|
}
|
|
|
|
} else if (header === 'subject') {
|
|
|
|
this.header.subject = mimeWordEncode(headers.subject);
|
|
|
|
} else if (/^(cc|bcc|to|from)/i.test(header)) {
|
|
|
|
this.header[header.toLowerCase()] = person2address(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];
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
}
|
2012-05-28 09:59:31 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {MessageAttachment} options attachment options
|
|
|
|
* @returns {Message} the current instance for chaining
|
|
|
|
*/
|
2018-05-14 23:02:16 +00:00
|
|
|
attach(options) {
|
2018-05-27 04:25:08 +00:00
|
|
|
/*
|
2018-06-28 02:56:26 +00:00
|
|
|
legacy support, will remove eventually...
|
|
|
|
arguments -> (path, type, name, headers)
|
|
|
|
*/
|
2018-06-28 02:57:21 +00:00
|
|
|
if (typeof options === 'string' && arguments.length > 1) {
|
|
|
|
options = {
|
|
|
|
path: options,
|
|
|
|
type: arguments[1],
|
|
|
|
name: arguments[2],
|
|
|
|
};
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
// sender can specify an attachment as an alternative
|
|
|
|
if (options.alternative) {
|
|
|
|
this.alternative = options;
|
|
|
|
this.alternative.charset = options.charset || 'utf-8';
|
|
|
|
this.alternative.type = options.type || 'text/html';
|
|
|
|
this.alternative.inline = true;
|
|
|
|
} else {
|
2018-05-14 23:02:16 +00:00
|
|
|
this.attachments.push(options);
|
|
|
|
}
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
return this;
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* legacy support, will remove eventually...
|
|
|
|
* should use Message.attach() instead
|
|
|
|
* @param {string} html html data
|
|
|
|
* @param {string} [charset='utf-8'] the charset to encode as
|
|
|
|
* @returns {Message} the current Message instance
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
attach_alternative(html, charset) {
|
|
|
|
this.alternative = {
|
|
|
|
data: html,
|
|
|
|
charset: charset || 'utf-8',
|
|
|
|
type: 'text/html',
|
|
|
|
inline: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
2018-06-29 00:55:20 +00:00
|
|
|
* @param {function(boolean, string): void} callback This callback is displayed as part of the Requester class.
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
valid(callback) {
|
|
|
|
if (!this.header.from) {
|
|
|
|
callback(false, 'message does not have a valid sender');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(this.header.to || this.header.cc || this.header.bcc)) {
|
|
|
|
callback(false, 'message does not have a valid recipient');
|
|
|
|
} else if (this.attachments.length === 0) {
|
2018-06-29 00:55:20 +00:00
|
|
|
callback(true, undefined);
|
2018-05-27 04:25:08 +00:00
|
|
|
} else {
|
|
|
|
const failed = [];
|
|
|
|
|
|
|
|
this.attachments.forEach(attachment => {
|
|
|
|
if (attachment.path) {
|
2018-06-25 02:28:30 +00:00
|
|
|
if (fs.existsSync(attachment.path) == false) {
|
2018-05-27 04:25:08 +00:00
|
|
|
failed.push(`${attachment.path} does not exist`);
|
2018-06-04 16:40:23 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
} else if (attachment.stream) {
|
2018-06-04 16:40:23 +00:00
|
|
|
if (!attachment.stream.readable) {
|
2018-05-27 04:25:08 +00:00
|
|
|
failed.push('attachment stream is not readable');
|
2018-06-04 16:40:23 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
} else if (!attachment.data) {
|
|
|
|
failed.push('attachment has no data associated with it');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
callback(failed.length === 0, failed.join(', '));
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* returns a stream of the current message
|
|
|
|
* @returns {MessageStream} a stream of the current message
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
stream() {
|
|
|
|
return new MessageStream(this);
|
|
|
|
}
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(Error, string): void} callback the function to call with the error and buffer
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
read(callback) {
|
|
|
|
let buffer = '';
|
|
|
|
const str = this.stream();
|
|
|
|
str.on('data', data => (buffer += data));
|
|
|
|
str.on('end', err => callback(err, buffer));
|
|
|
|
str.on('error', err => callback(err, buffer));
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
2011-09-25 21:51:29 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @typedef {Object} MessageAttachmentHeaders
|
|
|
|
* @property {string} content-type
|
|
|
|
* @property {string} content-transfer-encoding
|
|
|
|
* @property {string} content-disposition
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {Object} MessageAttachment
|
|
|
|
* @property {string} [name]
|
|
|
|
* @property {string} [type]
|
|
|
|
* @property {string} [charset]
|
|
|
|
* @property {string} [method]
|
|
|
|
* @property {string} [path]
|
2018-06-29 02:37:08 +00:00
|
|
|
* @property {NodeJS.ReadWriteStream} [stream]
|
2018-06-28 02:56:26 +00:00
|
|
|
* @property {boolean} [inline]
|
|
|
|
* @property {MessageAttachment} [alternative]
|
|
|
|
* @property {MessageAttachment[]} [related]
|
|
|
|
* @property {*} [encoded]
|
|
|
|
* @property {*} [data]
|
|
|
|
* @property {MessageAttachmentHeaders} [headers]
|
|
|
|
*/
|
|
|
|
|
2018-05-14 23:02:16 +00:00
|
|
|
class MessageStream extends Stream {
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {Message} message the message to stream
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
constructor(message) {
|
2018-05-14 23:02:16 +00:00
|
|
|
super();
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @type {Message}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
this.message = message;
|
2018-06-28 02:56:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
this.readable = true;
|
2018-06-28 02:56:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
this.paused = false;
|
2018-06-28 02:56:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {Buffer}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
|
2018-06-28 02:56:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
this.bufferIndex = 0;
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_mixed = () => {
|
|
|
|
const boundary = generate_boundary();
|
|
|
|
output(
|
|
|
|
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
|
|
|
);
|
|
|
|
|
2018-06-28 02:57:21 +00:00
|
|
|
if (this.message.alternative == null) {
|
2018-05-27 04:25:08 +00:00
|
|
|
output_text(this.message);
|
|
|
|
output_message(boundary, this.message.attachments, 0, close);
|
|
|
|
} else {
|
|
|
|
const cb = () =>
|
|
|
|
output_message(boundary, this.message.attachments, 0, close);
|
|
|
|
output_alternative(this.message, cb);
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {string} boundary the boundary text between outputs
|
|
|
|
* @param {MessageAttachment[]} list the list of potential messages to output
|
|
|
|
* @param {number} index the index of the list item to output
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(): void} callback the function to call if index is greater than upper bound
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_message = (boundary, list, index, callback) => {
|
|
|
|
if (index < list.length) {
|
|
|
|
output(`--${boundary}${CRLF}`);
|
|
|
|
if (list[index].related) {
|
|
|
|
output_related(list[index], () =>
|
|
|
|
output_message(boundary, list, index + 1, callback)
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
output_attachment(list[index], () =>
|
|
|
|
output_message(boundary, list, index + 1, callback)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
|
|
|
callback();
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {MessageAttachment} attachment the metadata to use as headers
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_attachment_headers = attachment => {
|
|
|
|
let data = [];
|
|
|
|
const headers = {
|
|
|
|
'content-type':
|
|
|
|
attachment.type +
|
|
|
|
(attachment.charset ? `; charset=${attachment.charset}` : '') +
|
|
|
|
(attachment.method ? `; method=${attachment.method}` : ''),
|
|
|
|
'content-transfer-encoding': 'base64',
|
|
|
|
'content-disposition': attachment.inline
|
|
|
|
? 'inline'
|
|
|
|
: `attachment; filename="${mimeWordEncode(attachment.name)}"`,
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
// allow sender to override default headers
|
2018-05-27 04:52:50 +00:00
|
|
|
for (const header in attachment.headers || {}) {
|
2018-05-27 04:25:08 +00:00
|
|
|
headers[header.toLowerCase()] = attachment.headers[header];
|
|
|
|
}
|
|
|
|
|
2018-05-27 04:52:50 +00:00
|
|
|
for (const header in headers) {
|
2018-05-27 04:25:08 +00:00
|
|
|
data = data.concat([
|
|
|
|
fix_header_name_case(header),
|
|
|
|
': ',
|
|
|
|
headers[header],
|
|
|
|
CRLF,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
output(data.concat([CRLF]).join(''));
|
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {MessageAttachment} attachment the metadata to use as headers
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(): void} callback the function to call after output is finished
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_attachment = (attachment, callback) => {
|
|
|
|
const build = attachment.path
|
|
|
|
? output_file
|
|
|
|
: attachment.stream
|
|
|
|
? output_stream
|
|
|
|
: output_data;
|
|
|
|
output_attachment_headers(attachment);
|
|
|
|
build(attachment, callback);
|
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {MessageAttachment} attachment the metadata to use as headers
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(): void} callback the function to call after output is finished
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_data = (attachment, callback) => {
|
|
|
|
output_base64(
|
|
|
|
attachment.encoded
|
|
|
|
? attachment.data
|
|
|
|
: Buffer.from(attachment.data).toString('base64'),
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {MessageAttachment} attachment the metadata to use as headers
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(NodeJS.ErrnoException): void} next the function to call when the file is closed
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_file = (attachment, next) => {
|
|
|
|
const chunk = MIME64CHUNK * 16;
|
|
|
|
const buffer = Buffer.alloc(chunk);
|
2018-06-25 02:33:29 +00:00
|
|
|
const closed = fd => fs.closeSync(fd);
|
2018-05-27 04:25:08 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {Error} err the error to emit
|
2018-06-28 02:56:26 +00:00
|
|
|
* @param {number} fd the file descriptor
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const opened = (err, fd) => {
|
|
|
|
if (!err) {
|
|
|
|
const read = (err, bytes) => {
|
|
|
|
if (!err && this.readable) {
|
|
|
|
let encoding =
|
|
|
|
attachment && attachment.headers
|
|
|
|
? attachment.headers['content-transfer-encoding'] || 'base64'
|
|
|
|
: 'base64';
|
|
|
|
if (encoding === 'ascii' || encoding === '7bit') {
|
|
|
|
encoding = 'ascii';
|
|
|
|
} else if (encoding === 'binary' || encoding === '8bit') {
|
|
|
|
encoding = 'binary';
|
|
|
|
} else {
|
|
|
|
encoding = 'base64';
|
|
|
|
}
|
|
|
|
// guaranteed to be encoded without padding unless it is our last read
|
|
|
|
output_base64(buffer.toString(encoding, 0, bytes), () => {
|
|
|
|
if (bytes == chunk) {
|
|
|
|
// we read a full chunk, there might be more
|
|
|
|
fs.read(fd, buffer, 0, chunk, null, read);
|
|
|
|
} // that was the last chunk, we are done reading the file
|
|
|
|
else {
|
|
|
|
this.removeListener('error', closed);
|
|
|
|
fs.close(fd, next);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.emit(
|
|
|
|
'error',
|
|
|
|
err || { message: 'message stream was interrupted somehow!' }
|
|
|
|
);
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
2011-12-09 10:28:38 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
fs.read(fd, buffer, 0, chunk, null, read);
|
|
|
|
this.once('error', closed);
|
|
|
|
} else {
|
2018-05-14 23:02:16 +00:00
|
|
|
this.emit('error', err);
|
|
|
|
}
|
|
|
|
};
|
2011-11-24 21:51:49 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
fs.open(attachment.path, 'r', opened);
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {MessageAttachment} attachment the metadata to use as headers
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(): void} callback the function to call after output is finished
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_stream = (attachment, callback) => {
|
|
|
|
if (attachment.stream.readable) {
|
2018-06-28 02:57:21 +00:00
|
|
|
let previous = Buffer.alloc(0);
|
2018-05-14 23:02:16 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
attachment.stream.resume();
|
2018-05-14 23:02:16 +00:00
|
|
|
|
|
|
|
attachment.stream.on('end', () => {
|
2018-06-28 02:57:21 +00:00
|
|
|
output_base64(previous.toString('base64'), callback);
|
2018-05-27 04:25:08 +00:00
|
|
|
this.removeListener('pause', attachment.stream.pause);
|
|
|
|
this.removeListener('resume', attachment.stream.resume);
|
|
|
|
this.removeListener('error', attachment.stream.resume);
|
2018-05-14 23:02:16 +00:00
|
|
|
});
|
|
|
|
|
2018-06-28 02:57:21 +00:00
|
|
|
attachment.stream.on('data', buff => {
|
2018-05-27 04:25:08 +00:00
|
|
|
// do we have bytes from a previous stream data event?
|
2018-06-28 02:57:21 +00:00
|
|
|
let buffer = Buffer.isBuffer(buff) ? buff : Buffer.from(buff);
|
|
|
|
|
|
|
|
if (previous.byteLength > 0) {
|
2018-07-06 18:10:40 +00:00
|
|
|
buffer = Buffer.concat([previous, buffer]);
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
const padded = buffer.length % MIME64CHUNK;
|
2018-07-06 18:10:40 +00:00
|
|
|
previous = Buffer.alloc(padded);
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
// encode as much of the buffer to base64 without empty bytes
|
2018-06-28 02:57:21 +00:00
|
|
|
if (padded > 0) {
|
2018-05-27 04:25:08 +00:00
|
|
|
// copy dangling bytes into previous buffer
|
|
|
|
buffer.copy(previous, 0, buffer.length - padded);
|
|
|
|
}
|
|
|
|
output_base64(buffer.toString('base64', 0, buffer.length - padded));
|
2018-05-14 23:02:16 +00:00
|
|
|
});
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
this.on('pause', attachment.stream.pause);
|
|
|
|
this.on('resume', attachment.stream.resume);
|
|
|
|
this.on('error', attachment.stream.resume);
|
|
|
|
} else {
|
2018-05-14 23:02:16 +00:00
|
|
|
this.emit('error', { message: 'stream not readable' });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {string} data the data to output as base64
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(): void} [callback] the function to call after output is finished
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_base64 = (data, callback) => {
|
|
|
|
const loops = Math.ceil(data.length / MIMECHUNK);
|
|
|
|
let loop = 0;
|
|
|
|
while (loop < loops) {
|
|
|
|
output(data.substring(MIMECHUNK * loop, MIMECHUNK * (loop + 1)) + CRLF);
|
|
|
|
loop++;
|
|
|
|
}
|
2018-06-04 16:40:23 +00:00
|
|
|
if (callback) {
|
|
|
|
callback();
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
2011-12-09 10:28:38 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {Message} message the message to output
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_text = message => {
|
|
|
|
let data = [];
|
2011-12-09 10:28:38 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
data = data.concat([
|
|
|
|
'Content-Type:',
|
|
|
|
message.content,
|
|
|
|
CRLF,
|
|
|
|
'Content-Transfer-Encoding: 7bit',
|
|
|
|
CRLF,
|
|
|
|
]);
|
|
|
|
data = data.concat(['Content-Disposition: inline', CRLF, CRLF]);
|
2018-05-14 23:02:16 +00:00
|
|
|
data = data.concat([message.text || '', CRLF, CRLF]);
|
2011-09-25 21:51:29 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
output(data.join(''));
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
2011-12-09 10:28:38 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {Message} message the message to output
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(): void} callback the function to call after output is finished
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_alternative = (message, callback) => {
|
|
|
|
const boundary = generate_boundary();
|
|
|
|
output(
|
|
|
|
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
|
|
|
);
|
|
|
|
output_text(message);
|
2018-05-14 23:02:16 +00:00
|
|
|
output(`--${boundary}${CRLF}`);
|
2011-12-09 10:28:38 +00:00
|
|
|
|
2018-06-29 03:25:10 +00:00
|
|
|
/**
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const finish = () => {
|
|
|
|
output([CRLF, '--', boundary, '--', CRLF, CRLF].join(''));
|
|
|
|
callback();
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
2012-03-27 16:48:01 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
if (message.alternative.related) {
|
|
|
|
output_related(message.alternative, finish);
|
|
|
|
} else {
|
|
|
|
output_attachment(message.alternative, finish);
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {MessageAttachment} message the message to output
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(): void} callback the function to call after output is finished
|
2018-06-28 02:56:26 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_related = (message, callback) => {
|
2018-05-14 23:02:16 +00:00
|
|
|
const boundary = generate_boundary();
|
2018-05-27 04:25:08 +00:00
|
|
|
output(
|
|
|
|
`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
|
|
|
);
|
|
|
|
output_attachment(message, () => {
|
|
|
|
output_message(boundary, message.related, 0, () => {
|
|
|
|
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
});
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_header_data = () => {
|
|
|
|
if (this.message.attachments.length || this.message.alternative) {
|
|
|
|
output(`MIME-Version: 1.0${CRLF}`);
|
|
|
|
output_mixed();
|
|
|
|
} // you only have a text message!
|
|
|
|
else {
|
|
|
|
output_text(this.message);
|
|
|
|
close();
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
2011-09-25 21:51:29 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output_header = () => {
|
2018-05-14 23:02:16 +00:00
|
|
|
let data = [];
|
2011-09-25 21:51:29 +00:00
|
|
|
|
2018-05-27 04:52:50 +00:00
|
|
|
for (const header in this.message.header) {
|
2018-05-27 04:25:08 +00:00
|
|
|
// do not output BCC in the headers (regex) nor custom Object.prototype functions...
|
|
|
|
if (
|
|
|
|
!/bcc/i.test(header) &&
|
|
|
|
this.message.header.hasOwnProperty(header)
|
|
|
|
) {
|
|
|
|
data = data.concat([
|
|
|
|
fix_header_name_case(header),
|
|
|
|
': ',
|
|
|
|
this.message.header[header],
|
|
|
|
CRLF,
|
|
|
|
]);
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
}
|
2011-09-25 21:51:29 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
output(data.join(''));
|
|
|
|
output_header_data();
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
2011-12-09 10:28:38 +00:00
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {string} data the data to output
|
2018-06-29 03:44:54 +00:00
|
|
|
* @param {function(...args): void} [callback] the function
|
2018-06-28 02:56:26 +00:00
|
|
|
* @param {*[]} [args] array of arguments to pass to the callback
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const output = (data, callback, args) => {
|
2018-05-14 23:02:16 +00:00
|
|
|
const bytes = Buffer.byteLength(data);
|
2011-12-13 23:10:13 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
// can we buffer the data?
|
|
|
|
if (bytes + this.bufferIndex < this.buffer.length) {
|
|
|
|
this.buffer.write(data, this.bufferIndex);
|
|
|
|
this.bufferIndex += bytes;
|
2018-06-04 16:40:23 +00:00
|
|
|
if (callback) {
|
|
|
|
callback.apply(null, args);
|
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
}
|
|
|
|
// we can't buffer the data, so ship it out!
|
|
|
|
else if (bytes > this.buffer.length) {
|
|
|
|
if (this.bufferIndex) {
|
|
|
|
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
|
|
|
|
this.bufferIndex = 0;
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
const loops = Math.ceil(data.length / this.buffer.length);
|
|
|
|
let loop = 0;
|
|
|
|
while (loop < loops) {
|
|
|
|
this.emit(
|
|
|
|
'data',
|
|
|
|
data.substring(
|
|
|
|
this.buffer.length * loop,
|
|
|
|
this.buffer.length * (loop + 1)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
loop++;
|
|
|
|
}
|
|
|
|
} // we need to clean out the buffer, it is getting full
|
|
|
|
else {
|
|
|
|
if (!this.paused) {
|
|
|
|
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
|
|
|
|
this.buffer.write(data, 0);
|
|
|
|
this.bufferIndex = bytes;
|
|
|
|
// we could get paused after emitting data...
|
|
|
|
if (this.paused) {
|
|
|
|
this.once('resume', () => callback.apply(null, args));
|
|
|
|
} else if (callback) {
|
|
|
|
callback.apply(null, args);
|
|
|
|
}
|
|
|
|
} // we can't empty out the buffer, so let's wait till we resume before adding to it
|
|
|
|
else {
|
|
|
|
this.once('resume', () => output(data, callback, args));
|
|
|
|
}
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* @param {*} [err] the error to emit
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
const close = err => {
|
|
|
|
if (err) {
|
|
|
|
this.emit('error', err);
|
|
|
|
} else {
|
|
|
|
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
|
|
|
|
this.emit('end');
|
|
|
|
}
|
|
|
|
this.buffer = null;
|
|
|
|
this.bufferIndex = 0;
|
|
|
|
this.readable = false;
|
|
|
|
this.removeAllListeners('resume');
|
|
|
|
this.removeAllListeners('pause');
|
|
|
|
this.removeAllListeners('error');
|
|
|
|
this.removeAllListeners('data');
|
|
|
|
this.removeAllListeners('end');
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
this.once('destroy', close);
|
|
|
|
process.nextTick(output_header);
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* pause the stream
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
pause() {
|
|
|
|
this.paused = true;
|
|
|
|
this.emit('pause');
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* resume the stream
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
resume() {
|
|
|
|
this.paused = false;
|
|
|
|
this.emit('resume');
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* destroy the stream
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
destroy() {
|
|
|
|
this.emit(
|
|
|
|
'destroy',
|
|
|
|
this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null
|
|
|
|
);
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-06-28 02:56:26 +00:00
|
|
|
/**
|
|
|
|
* destroy the stream at first opportunity
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-05-27 04:25:08 +00:00
|
|
|
destroySoon() {
|
|
|
|
this.emit('destroy');
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
2011-02-23 21:23:37 +00:00
|
|
|
|
|
|
|
exports.Message = Message;
|
2011-12-09 10:28:38 +00:00
|
|
|
exports.BUFFERSIZE = BUFFERSIZE;
|
2018-05-14 23:02:16 +00:00
|
|
|
exports.create = headers => new Message(headers);
|