integrate eslint & prettier

This commit is contained in:
Zack Schuster 2018-05-26 21:25:08 -07:00
parent a81f056d1f
commit dc12f85b16
12 changed files with 1561 additions and 1405 deletions

35
.eslintrc.json Normal file
View File

@ -0,0 +1,35 @@
{
"env": {
"es6": true,
"mocha": true,
"node": true
},
"plugins": [
"mocha"
],
"extends": [
"eslint:recommended",
"plugin:prettier/recommended"
],
"rules": {
"linebreak-style": [
"error",
"unix"
],
"no-case-declarations": "off",
"no-console": "off",
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"ignoreRestSiblings": true
}
],
"mocha/handle-done-callback": "error",
"mocha/no-exclusive-tests": "error",
"mocha/no-global-tests": "error",
"mocha/no-mocha-arrows": "error",
"mocha/no-skipped-tests": "error"
}
}

View File

@ -1,4 +1,4 @@
exports.server = require('./smtp/client');
exports.message = require('./smtp/message');
exports.SMTP = require('./smtp/smtp');
exports.error= require('./smtp/error');
exports.error = require('./smtp/error');

View File

@ -24,8 +24,13 @@
},
"devDependencies": {
"chai": "1.1.0",
"eslint": "^4.19.1",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-prettier": "^2.6.0",
"mailparser": "2.2.0",
"mocha": "5.0.0",
"prettier": "^1.12.1",
"smtp-server": "^3.4.1"
},
"engine": [
@ -35,5 +40,10 @@
"scripts": {
"test": "mocha -R spec"
},
"license": "MIT"
"license": "MIT",
"prettier": {
"singleQuote": true,
"trailingComma": "es5",
"useTabs": true
}
}

View File

@ -1,186 +1,185 @@
const smtp = require('./smtp');
const smtpError = require('./error');
const message = require('./message');
const addressparser = require('addressparser');
class Client {
constructor(server) {
this.smtp = new smtp.SMTP(server);
//this.smtp.debug(1);
constructor(server) {
this.smtp = new smtp.SMTP(server);
//this.smtp.debug(1);
this.queue = [];
this.timer = null;
this.sending = false;
this.ready = false;
this.queue = [];
this.timer = null;
this.sending = false;
this.ready = false;
}
_poll() {
clearTimeout(this.timer);
_poll() {
clearTimeout(this.timer);
if (this.queue.length) {
if (this.smtp.state() == smtp.state.NOTCONNECTED) {
if (this.queue.length) {
if (this.smtp.state() == smtp.state.NOTCONNECTED) {
this._connect(this.queue[0]);
} else if (
this.smtp.state() == smtp.state.CONNECTED &&
!this.sending &&
this.ready
) {
this._sendmail(this.queue.shift());
this.smtp.state() == smtp.state.CONNECTED &&
!this.sending &&
this.ready
) {
this._sendmail(this.queue.shift());
}
}
// wait around 1 seconds in case something does come in, otherwise close out SMTP connection if still open
else if (this.smtp.state() == smtp.state.CONNECTED)
this.timer = setTimeout(() => this.smtp.quit(), 1000);
}
}
// wait around 1 seconds in case something does come in, otherwise close out SMTP connection if still open
else if (this.smtp.state() == smtp.state.CONNECTED)
this.timer = setTimeout(() => this.smtp.quit(), 1000);
}
_connect(stack) {
const connect = err => {
if (!err) {
const begin = err => {
if (!err) {
this.ready = true;
this._poll();
} else {
stack.callback(err, stack.message);
_connect(stack) {
const connect = err => {
if (!err) {
const begin = err => {
if (!err) {
this.ready = true;
this._poll();
} else {
stack.callback(err, stack.message);
// clear out the queue so all callbacks can be called with the same error message
this.queue.shift();
this._poll();
}
};
// clear out the queue so all callbacks can be called with the same error message
this.queue.shift();
this._poll();
}
};
if (!this.smtp.authorized()) this.smtp.login(begin);
else this.smtp.ehlo_or_helo_if_needed(begin);
} else {
stack.callback(err, stack.message);
if (!this.smtp.authorized()) this.smtp.login(begin);
else this.smtp.ehlo_or_helo_if_needed(begin);
} else {
stack.callback(err, stack.message);
// clear out the queue so all callbacks can be called with the same error message
this.queue.shift();
this._poll();
}
};
// clear out the queue so all callbacks can be called with the same error message
this.queue.shift();
this._poll();
}
};
this.ready = false;
this.smtp.connect(connect);
}
this.ready = false;
this.smtp.connect(connect);
}
send(msg, callback) {
if (
!(msg instanceof message.Message) &&
msg.from &&
(msg.to || msg.cc || msg.bcc) &&
(msg.text !== undefined || this._containsInlinedHtml(msg.attachment))
)
msg = message.create(msg);
send(msg, callback) {
if (
!(msg instanceof message.Message) &&
msg.from &&
(msg.to || msg.cc || msg.bcc) &&
(msg.text !== undefined || this._containsInlinedHtml(msg.attachment))
)
msg = message.create(msg);
if (msg instanceof message.Message) {
msg.valid((valid, why) => {
if (valid) {
const stack = {
message: msg,
to: addressparser(msg.header.to),
from: addressparser(msg.header.from)[0].address,
callback: (callback || function() {}).bind(this),
};
if (msg instanceof message.Message) {
msg.valid((valid, why) => {
if (valid) {
const stack = {
message: msg,
to: addressparser(msg.header.to),
from: addressparser(msg.header.from)[0].address,
callback: (callback || function() {}).bind(this),
};
if (msg.header.cc)
stack.to = stack.to.concat(addressparser(msg.header.cc));
if (msg.header.cc)
stack.to = stack.to.concat(addressparser(msg.header.cc));
if (msg.header.bcc)
stack.to = stack.to.concat(addressparser(msg.header.bcc));
if (msg.header.bcc)
stack.to = stack.to.concat(addressparser(msg.header.bcc));
if (
msg.header['return-path'] &&
addressparser(msg.header['return-path']).length
)
stack.returnPath = addressparser(
msg.header['return-path']
)[0].address;
if (
msg.header['return-path'] &&
addressparser(msg.header['return-path']).length
)
stack.returnPath = addressparser(
msg.header['return-path']
)[0].address;
this.queue.push(stack);
this._poll();
} else {
this.queue.push(stack);
this._poll();
} else {
callback(new Error(why), msg);
}
});
} else {
});
} else {
callback(new Error('message is not a valid Message instance'), msg);
}
}
_containsInlinedHtml(attachment) {
if (Array.isArray(attachment)) {
return attachment.some(() => {
return att => {
return this._isAttachmentInlinedHtml(att);
};
});
} else {
return this._isAttachmentInlinedHtml(attachment);
}
}
_isAttachmentInlinedHtml(attachment) {
return (
attachment &&
(attachment.data || attachment.path) &&
attachment.alternative === true
);
}
_sendsmtp(stack, next) {
return err => {
if (!err && next) {
next.apply(this, [stack]);
} else {
// if we snag on SMTP commands, call done, passing the error
// but first reset SMTP state so queue can continue polling
this.smtp.rset(() => this._senddone(err, stack));
}
};
}
_containsInlinedHtml(attachment) {
if (Array.isArray(attachment)) {
return attachment.some(() => {
return att => {
return this._isAttachmentInlinedHtml(att);
};
});
} else {
return this._isAttachmentInlinedHtml(attachment);
}
}
_isAttachmentInlinedHtml(attachment) {
return (
attachment &&
(attachment.data || attachment.path) &&
attachment.alternative === true
);
}
_sendsmtp(stack, next) {
return err => {
if (!err && next) {
next.apply(this, [stack]);
} else {
// if we snag on SMTP commands, call done, passing the error
// but first reset SMTP state so queue can continue polling
this.smtp.rset(() => this._senddone(err, stack));
}
};
}
_sendmail(stack) {
const from = stack.returnPath || stack.from;
this.sending = true;
this.smtp.mail(this._sendsmtp(stack, this._sendrcpt), '<' + from + '>');
const from = stack.returnPath || stack.from;
this.sending = true;
this.smtp.mail(this._sendsmtp(stack, this._sendrcpt), '<' + from + '>');
}
_sendrcpt(stack) {
const to = stack.to.shift().address;
this.smtp.rcpt(
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
'<' + to + '>'
);
const to = stack.to.shift().address;
this.smtp.rcpt(
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
'<' + to + '>'
);
}
_senddata(stack) {
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
}
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
}
_sendmessage(stack) {
const stream = stack.message.stream();
_sendmessage(stack) {
const stream = stack.message.stream();
stream.on('data', (data) => this.smtp.message(data));
stream.on('end', () => {
this.smtp.data_end(
this._sendsmtp(stack, () => this._senddone(null, stack))
);
});
stream.on('data', data => this.smtp.message(data));
stream.on('end', () => {
this.smtp.data_end(
this._sendsmtp(stack, () => this._senddone(null, stack))
);
});
// there is no way to cancel a message while in the DATA portion,
// so we have to close the socket to prevent a bad email from going out
stream.on('error', (err) => {
this.smtp.close();
this._senddone(err, stack);
});
stream.on('error', err => {
this.smtp.close();
this._senddone(err, stack);
});
}
_senddone(err, stack) {
this.sending = false;
stack.callback(err, stack.message);
this._poll();
}
this.sending = false;
stack.callback(err, stack.message);
this._poll();
}
}
exports.Client = Client;

View File

@ -1,16 +1,16 @@
module.exports = function(message, code, error, smtp) {
const err = new Error(
error && error.message ? `${message} (${error.message})` : message
);
const err = new Error(
error && error.message ? `${message} (${error.message})` : message
);
err.code = code;
err.smtp = smtp;
err.smtp = smtp;
if (error) {
err.previous = error;
}
return err;
return err;
};
module.exports.COULDNOTCONNECT = 1;

View File

@ -7,7 +7,6 @@ const mimeWordEncode = require('emailjs-mime-codec').mimeWordEncode;
const addressparser = require('addressparser');
const CRLF = '\r\n';
const MIMECHUNK = 76; // MIME standard wants 76 char chunks when sending out.
const BASE64CHUNK = 24; // BASE64 bits needed before padding is used
const MIME64CHUNK = MIMECHUNK * 6; // meets both base64 and mime divisibility
const BUFFERSIZE = MIMECHUNK * 24 * 7; // size of the message stream buffer
@ -15,497 +14,528 @@ let counter = 0;
// support for nodejs without Buffer.concat native function
if (!Buffer.concat) {
require('bufferjs/concat');
require('bufferjs/concat');
}
function generate_boundary() {
let text = '';
const possible = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?`;
let text = '';
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
for (let i = 0; i < 69; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
for (let i = 0; i < 69; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
return text;
}
function person2address(l) {
return addressparser(l)
.map(({ name, address }) => {
return name
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
: address;
})
.join(', ');
return addressparser(l)
.map(({ name, address }) => {
return name
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
: address;
})
.join(', ');
}
function fix_header_name_case(header_name) {
return header_name
.toLowerCase()
.replace(/^(.)|-(.)/g, match => match.toUpperCase());
return header_name
.toLowerCase()
.replace(/^(.)|-(.)/g, match => match.toUpperCase());
}
class Message {
constructor(headers) {
this.attachments = [];
this.alternative = null;
this.header = {
'message-id': `<${new Date().getTime()}.${counter++}.${process.pid}@${os.hostname()}>`,
date: moment()
.locale('en')
.format('ddd, DD MMM YYYY HH:mm:ss ZZ')
constructor(headers) {
this.attachments = [];
this.alternative = null;
this.header = {
'message-id': `<${new Date().getTime()}.${counter++}.${
process.pid
}@${os.hostname()}>`,
date: moment()
.locale('en')
.format('ddd, DD MMM YYYY HH:mm:ss ZZ'),
};
this.content = 'text/plain; charset=utf-8';
for (let 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 if (header === 'attachment' && typeof headers[header] === 'object') {
if (Array.isArray(headers[header])) {
for (let i = 0, l = headers[header].length; i < l; i++) {
this.attach(headers[header][i]);
}
} else {
this.attach(headers[header]);
}
} 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];
}
this.content = 'text/plain; charset=utf-8';
for (let 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 if (
header === 'attachment' &&
typeof headers[header] === 'object'
) {
if (Array.isArray(headers[header])) {
for (let i = 0, l = headers[header].length; i < l; i++) {
this.attach(headers[header][i]);
}
} else {
this.attach(headers[header]);
}
} 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];
}
}
}
attach(options) {
/*
/*
legacy support, will remove eventually...
arguments -> (path, type, name, headers)
*/
if (arguments.length > 1) {
options = { path: options, type: arguments[1], name: arguments[2] };
if (arguments.length > 1) {
options = { path: options, type: arguments[1], name: arguments[2] };
}
// 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 {
// 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 {
this.attachments.push(options);
}
return this;
}
return this;
}
/*
/*
legacy support, will remove eventually...
should use Message.attach() instead
*/
attach_alternative(html, charset) {
this.alternative = {
data: html,
charset: charset || 'utf-8',
type: 'text/html',
inline: true
};
attach_alternative(html, charset) {
this.alternative = {
data: html,
charset: charset || 'utf-8',
type: 'text/html',
inline: true,
};
return this;
}
return this;
}
valid(callback) {
if (!this.header.from) {
callback(false, 'message does not have a valid sender');
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) {
callback(true);
} else {
const failed = [];
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) {
callback(true);
} else {
const failed = [];
this.attachments.forEach((attachment, index) => {
if (attachment.path) {
// migrating path->fs for existsSync)
if (!(fs.existsSync || path.existsSync)(attachment.path))
failed.push(`${attachment.path} does not exist`);
} else if (attachment.stream) {
if (!attachment.stream.readable)
failed.push('attachment stream is not readable');
} else if (!attachment.data) {
failed.push('attachment has no data associated with it');
}
});
this.attachments.forEach(attachment => {
if (attachment.path) {
// migrating path->fs for existsSync)
if (!(fs.existsSync || path.existsSync)(attachment.path))
failed.push(`${attachment.path} does not exist`);
} else if (attachment.stream) {
if (!attachment.stream.readable)
failed.push('attachment stream is not readable');
} else if (!attachment.data) {
failed.push('attachment has no data associated with it');
}
});
callback(failed.length === 0, failed.join(', '));
}
}
callback(failed.length === 0, failed.join(', '));
}
}
stream() {
return new MessageStream(this);
}
stream() {
return new MessageStream(this);
}
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));
}
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));
}
}
class MessageStream extends Stream {
constructor(message) {
constructor(message) {
super();
this.message = message;
this.readable = true;
this.paused = false;
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
this.bufferIndex = 0;
this.message = message;
this.readable = true;
this.paused = false;
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
this.bufferIndex = 0;
const output_mixed = () => {
const boundary = generate_boundary();
output(
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
);
const output_mixed = () => {
const boundary = generate_boundary();
output(
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
);
if (!this.message.alternative) {
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);
}
};
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();
}
};
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)}"`
};
for (let header in attachment.headers || {}) {
// allow sender to override default headers
headers[header.toLowerCase()] = attachment.headers[header];
}
for (let header in headers) {
data = data.concat([
fix_header_name_case(header),
': ',
headers[header],
CRLF
]);
}
output(data.concat([CRLF]).join(''));
};
const output_attachment = (attachment, callback) => {
const build = attachment.path
? output_file
: attachment.stream
? output_stream
: output_data;
output_attachment_headers(attachment);
build(attachment, callback);
if (!this.message.alternative) {
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);
}
};
const output_data = (attachment, callback) => {
output_base64(
attachment.encoded
? attachment.data
: Buffer.from(attachment.data).toString('base64'),
callback
);
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();
}
};
const output_file = (attachment, next) => {
const chunk = MIME64CHUNK * 16;
const buffer = Buffer.alloc(chunk);
const closed = (fd) => {
if (fs.closeSync) {
fs.closeSync(fd);
}
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)}"`,
};
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!' });
}
for (let header in attachment.headers || {}) {
// allow sender to override default headers
headers[header.toLowerCase()] = attachment.headers[header];
}
for (let header in headers) {
data = data.concat([
fix_header_name_case(header),
': ',
headers[header],
CRLF,
]);
}
output(data.concat([CRLF]).join(''));
};
const output_attachment = (attachment, callback) => {
const build = attachment.path
? output_file
: attachment.stream
? output_stream
: output_data;
output_attachment_headers(attachment);
build(attachment, callback);
};
const output_data = (attachment, callback) => {
output_base64(
attachment.encoded
? attachment.data
: Buffer.from(attachment.data).toString('base64'),
callback
);
};
const output_file = (attachment, next) => {
const chunk = MIME64CHUNK * 16;
const buffer = Buffer.alloc(chunk);
const closed = fd => {
if (fs.closeSync) {
fs.closeSync(fd);
}
};
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!' }
);
}
};
fs.read(fd, buffer, 0, chunk, null, read);
this.once('error', closed);
} else {
fs.read(fd, buffer, 0, chunk, null, read);
this.once('error', closed);
} else {
this.emit('error', err);
}
};
fs.open(attachment.path, 'r', opened);
fs.open(attachment.path, 'r', opened);
};
const output_stream = (attachment, callback) => {
if (attachment.stream.readable) {
const output_stream = (attachment, callback) => {
if (attachment.stream.readable) {
let previous = null;
attachment.stream.resume();
attachment.stream.resume();
attachment.stream.on('end', () => {
output_base64(
(previous || Buffer.from(0)).toString('base64'),
callback
);
this.removeListener('pause', attachment.stream.pause);
this.removeListener('resume', attachment.stream.resume);
this.removeListener('error', attachment.stream.resume);
output_base64(
(previous || Buffer.from(0)).toString('base64'),
callback
);
this.removeListener('pause', attachment.stream.pause);
this.removeListener('resume', attachment.stream.resume);
this.removeListener('error', attachment.stream.resume);
});
attachment.stream.on('data', (buffer) => {
// do we have bytes from a previous stream data event?
if (previous) {
const buffer2 = Buffer.concat([previous, buffer]);
previous = null; // free up the buffer
buffer = null; // free up the buffer
buffer = buffer2;
attachment.stream.on('data', buffer => {
// do we have bytes from a previous stream data event?
if (previous) {
const buffer2 = Buffer.concat([previous, buffer]);
previous = null; // free up the buffer
buffer = null; // free up the buffer
buffer = buffer2;
}
const padded = buffer.length % MIME64CHUNK;
// encode as much of the buffer to base64 without empty bytes
if (padded) {
previous = Buffer.alloc(padded);
// copy dangling bytes into previous buffer
buffer.copy(previous, 0, buffer.length - padded);
}
output_base64(buffer.toString('base64', 0, buffer.length - padded));
const padded = buffer.length % MIME64CHUNK;
// encode as much of the buffer to base64 without empty bytes
if (padded) {
previous = Buffer.alloc(padded);
// copy dangling bytes into previous buffer
buffer.copy(previous, 0, buffer.length - padded);
}
output_base64(buffer.toString('base64', 0, buffer.length - padded));
});
this.on('pause', attachment.stream.pause);
this.on('resume', attachment.stream.resume);
this.on('error', attachment.stream.resume);
} else {
this.on('pause', attachment.stream.pause);
this.on('resume', attachment.stream.resume);
this.on('error', attachment.stream.resume);
} else {
this.emit('error', { message: 'stream not readable' });
}
};
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++;
}
if (callback) callback();
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++;
}
if (callback) callback();
};
const output_text = (message) => {
let data = [];
data = data.concat(['Content-Type:', message.content, CRLF, 'Content-Transfer-Encoding: 7bit', CRLF]);
data = data.concat(['Content-Disposition: inline', CRLF, CRLF]);
data = data.concat([message.text || '', CRLF, CRLF]);
output(data.join(''));
};
const output_alternative = (message, callback) => {
const boundary = generate_boundary();
output(`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`);
output_text(message);
output(`--${boundary}${CRLF}`);
const finish = () => {
output([CRLF, '--', boundary, '--', CRLF, CRLF].join(''));
callback();
};
if (message.alternative.related) {
output_related(message.alternative, finish);
} else {
output_attachment(message.alternative, finish);
}
};
const output_related = (message, callback) => {
const boundary = generate_boundary();
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();
});
});
};
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();
}
};
const output_header = () => {
const output_text = message => {
let data = [];
for (let header in this.message.header) {
// 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]);
data = data.concat([
'Content-Type:',
message.content,
CRLF,
'Content-Transfer-Encoding: 7bit',
CRLF,
]);
data = data.concat(['Content-Disposition: inline', CRLF, CRLF]);
data = data.concat([message.text || '', CRLF, CRLF]);
output(data.join(''));
};
const output_alternative = (message, callback) => {
const boundary = generate_boundary();
output(
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
);
output_text(message);
output(`--${boundary}${CRLF}`);
const finish = () => {
output([CRLF, '--', boundary, '--', CRLF, CRLF].join(''));
callback();
};
if (message.alternative.related) {
output_related(message.alternative, finish);
} else {
output_attachment(message.alternative, finish);
}
};
const output_related = (message, callback) => {
const boundary = generate_boundary();
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();
});
});
};
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();
}
};
const output_header = () => {
let data = [];
for (let header in this.message.header) {
// 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,
]);
}
}
output(data.join(''));
output_header_data();
output(data.join(''));
output_header_data();
};
const output = (data, callback, args) => {
const output = (data, callback, args) => {
const bytes = Buffer.byteLength(data);
// can we buffer the data?
if ((bytes + this.bufferIndex) < this.buffer.length) {
this.buffer.write(data, this.bufferIndex);
this.bufferIndex += bytes;
if (callback) callback.apply(null, args);
}
// 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;
// can we buffer the data?
if (bytes + this.bufferIndex < this.buffer.length) {
this.buffer.write(data, this.bufferIndex);
this.bufferIndex += bytes;
if (callback) callback.apply(null, args);
}
// 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;
}
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));
}
}
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));
}
}
};
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');
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');
};
this.once('destroy', close);
process.nextTick(output_header);
this.once('destroy', close);
process.nextTick(output_header);
}
pause() {
this.paused = true;
this.emit('pause');
pause() {
this.paused = true;
this.emit('pause');
}
resume() {
this.paused = false;
this.emit('resume');
resume() {
this.paused = false;
this.emit('resume');
}
destroy() {
this.emit('destroy', this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null);
destroy() {
this.emit(
'destroy',
this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null
);
}
destroySoon() {
this.emit('destroy');
}
destroySoon() {
this.emit('destroy');
}
}
exports.Message = Message;

View File

@ -8,53 +8,75 @@ class SMTPResponse {
if (buffer.length) {
// parse buffer for response codes
const line = buffer.replace('\r', '');
if (!line.trim().split(/\n/).pop().match(/^(\d{3})\s/)) {
if (
!line
.trim()
.split(/\n/)
.pop()
.match(/^(\d{3})\s/)
) {
return;
}
const match = line ? line.match(/(\d+)\s?(.*)/) : null;
const data = match !== null
? { code: match[1], message: match[2], data: line }
: { code: -1, data: line };
const data =
match !== null
? { code: match[1], message: match[2], data: line }
: { code: -1, data: line };
stream.emit('response', null, data);
buffer = '';
}
};
const error = (err) => {
stream.emit('response', SMTPError('connection encountered an error', SMTPError.ERROR, err));
}
const error = err => {
stream.emit(
'response',
SMTPError('connection encountered an error', SMTPError.ERROR, err)
);
};
const timedout = (err) => {
const timedout = err => {
stream.end();
stream.emit('response', SMTPError('timedout while connecting to smtp server', SMTPError.TIMEDOUT, err));
}
stream.emit(
'response',
SMTPError(
'timedout while connecting to smtp server',
SMTPError.TIMEDOUT,
err
)
);
};
const watch = (data) => {
const watch = data => {
if (data !== null) {
buffer += data.toString();
notify();
}
};
const close = (err) => {
stream.emit('response', SMTPError('connection has closed', SMTPError.CONNECTIONCLOSED, err));
const close = err => {
stream.emit(
'response',
SMTPError('connection has closed', SMTPError.CONNECTIONCLOSED, err)
);
};
const end = (err) => {
stream.emit('response', SMTPError('connection has ended', SMTPError.CONNECTIONENDED, err));
const end = err => {
stream.emit(
'response',
SMTPError('connection has ended', SMTPError.CONNECTIONENDED, err)
);
};
this.stop = (err) => {
this.stop = err => {
stream.removeAllListeners('response');
stream.removeListener('data', watch);
stream.removeListener('end', end);
stream.removeListener('close', close);
stream.removeListener('error', error);
if (err && typeof onerror === 'function')
onerror(err);
if (err && typeof onerror === 'function') onerror(err);
};
stream.on('data', watch);
@ -65,4 +87,5 @@ class SMTPResponse {
}
}
exports.monitor = (stream, timeout, onerror) => new SMTPResponse(stream, timeout, onerror);
exports.monitor = (stream, timeout, onerror) =>
new SMTPResponse(stream, timeout, onerror);

View File

@ -15,44 +15,36 @@ const SMTP_SSL_PORT = 465;
const SMTP_TLS_PORT = 587;
const CRLF = '\r\n';
const AUTH_METHODS = {
PLAIN: 'PLAIN',
CRAM_MD5: 'CRAM-MD5',
LOGIN: 'LOGIN',
XOAUTH2: 'XOAUTH2'
PLAIN: 'PLAIN',
CRAM_MD5: 'CRAM-MD5',
LOGIN: 'LOGIN',
XOAUTH2: 'XOAUTH2',
};
const TIMEOUT = 5000;
let DEBUG = 0;
const log = function() {
if (DEBUG) {
Array.prototype.slice.call(arguments).forEach(function(d) {
console.log(d);
});
}
};
const quotedata = function(data) {
// Quote data for email.
// Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
// Internet CRLF end-of-line.
return data.replace(/(?:\r\n|\n|\r(?!\n))/g, CRLF).replace(/^\./gm, '..');
if (DEBUG) {
Array.prototype.slice.call(arguments).forEach(function(d) {
console.log(d);
});
}
};
const caller = function(callback) {
if (typeof(callback) == 'function') {
const args = Array.prototype.slice.call(arguments);
args.shift();
if (typeof callback == 'function') {
const args = Array.prototype.slice.call(arguments);
args.shift();
callback.apply(null, args);
}
callback.apply(null, args);
}
};
const SMTPState = {
NOTCONNECTED: 0,
CONNECTING: 1,
CONNECTED: 2
NOTCONNECTED: 0,
CONNECTING: 1,
CONNECTED: 2,
};
class SMTP extends EventEmitter {
@ -69,19 +61,22 @@ class SMTP extends EventEmitter {
ssl,
tls,
authentication,
} = Object.assign({
timeout: TIMEOUT,
domain: os.hostname(),
host: 'localhost',
ssl: false,
tls: false,
authentication: [
AUTH_METHODS.CRAM_MD5,
AUTH_METHODS.LOGIN,
AUTH_METHODS.PLAIN,
AUTH_METHODS.XOAUTH2,
],
}, options);
} = Object.assign(
{
timeout: TIMEOUT,
domain: os.hostname(),
host: 'localhost',
ssl: false,
tls: false,
authentication: [
AUTH_METHODS.CRAM_MD5,
AUTH_METHODS.LOGIN,
AUTH_METHODS.PLAIN,
AUTH_METHODS.XOAUTH2,
],
},
options
);
this._state = SMTPState.NOTCONNECTED;
this._secure = false;
@ -98,7 +93,7 @@ class SMTP extends EventEmitter {
this.tls = tls;
this.port = port || (ssl ? SMTP_SSL_PORT : tls ? SMTP_TLS_PORT : SMTP_PORT);
this.loggedin = (user && password) ? false : true;
this.loggedin = user && password ? false : true;
// keep these strings hidden when quicky debugging/logging
this.user = () => options.user;
@ -106,132 +101,154 @@ class SMTP extends EventEmitter {
}
debug(level) {
DEBUG = level;
}
DEBUG = level;
}
state() {
return this._state;
}
state() {
return this._state;
}
authorized() {
return this.loggedin;
}
authorized() {
return this.loggedin;
}
connect(callback, port, host, options) {
options = options || {};
connect(callback, port, host, options) {
options = options || {};
this.host = host || this.host;
this.port = port || this.port;
this.ssl = options.ssl || this.ssl;
this.host = host || this.host;
this.port = port || this.port;
this.ssl = options.ssl || this.ssl;
if (this._state != SMTPState.NOTCONNECTED) {
this.quit(() => this.connect(callback, port, host, options));
return;
}
if (this._state != SMTPState.NOTCONNECTED) {
this.quit(() => this.connect(callback, port, host, options));
return;
}
const connected = (err) => {
if (!err) {
log(`connected: ${this.host}:${this.port}`);
const connected = err => {
if (!err) {
log(`connected: ${this.host}:${this.port}`);
if (this.ssl && !this.tls) {
// if key/ca/cert was passed in, check if connection is authorized
if (typeof(this.ssl) != 'boolean' && !this.sock.authorized) {
if (this.ssl && !this.tls) {
// if key/ca/cert was passed in, check if connection is authorized
if (typeof this.ssl != 'boolean' && !this.sock.authorized) {
this.close(true);
const msg = 'could not establish an ssl connection';
caller(callback, SMTPError(msg, SMTPError.CONNECTIONAUTH, err));
} else {
caller(callback, SMTPError(msg, SMTPError.CONNECTIONAUTH, err));
} else {
this._secure = true;
}
}
} else {
this.close(true);
caller(callback, SMTPError('could not connect', SMTPError.COULDNOTCONNECT, err));
}
};
}
} else {
this.close(true);
caller(
callback,
SMTPError('could not connect', SMTPError.COULDNOTCONNECT, err)
);
}
};
const response = (err, msg) => {
if (err) {
if (this._state === SMTPState.NOTCONNECTED && !this.sock) {
return;
}
this.close(true);
caller(callback, err);
} else if (msg.code == '220') {
log(msg.data);
const response = (err, msg) => {
if (err) {
if (this._state === SMTPState.NOTCONNECTED && !this.sock) {
return;
}
this.close(true);
caller(callback, err);
} else if (msg.code == '220') {
log(msg.data);
// might happen first, so no need to wait on connected()
this._state = SMTPState.CONNECTED;
caller(callback, null, msg.data);
} else {
log(`response (data): ${msg.data}`);
this.quit(() => {
const err = SMTPError('bad response on connection', SMTPError.BADRESPONSE, err, msg.data);
caller(callback, err);
});
}
};
// might happen first, so no need to wait on connected()
this._state = SMTPState.CONNECTED;
caller(callback, null, msg.data);
} else {
log(`response (data): ${msg.data}`);
this.quit(() => {
const err = SMTPError(
'bad response on connection',
SMTPError.BADRESPONSE,
err,
msg.data
);
caller(callback, err);
});
}
};
this._state = SMTPState.CONNECTING;
log(`connecting: ${this.host}:${this.port}`);
this._state = SMTPState.CONNECTING;
log(`connecting: ${this.host}:${this.port}`);
if (this.ssl) {
this.sock = tls.connect(this.port, this.host, this.ssl, connected);
} else {
this.sock = new net.Socket();
this.sock.connect(this.port, this.host, connected);
}
if (this.ssl) {
this.sock = tls.connect(this.port, this.host, this.ssl, connected);
} else {
this.sock = new net.Socket();
this.sock.connect(this.port, this.host, connected);
}
this.monitor = SMTPResponse.monitor(this.sock, this.timeout, () => this.close(true));
this.sock.once('response', response);
this.sock.once('error', response); // the socket could reset or throw, so let's handle it and let the user know
}
this.monitor = SMTPResponse.monitor(this.sock, this.timeout, () =>
this.close(true)
);
this.sock.once('response', response);
this.sock.once('error', response); // the socket could reset or throw, so let's handle it and let the user know
}
send(str, callback) {
if (this.sock && this._state == SMTPState.CONNECTED) {
log(str);
send(str, callback) {
if (this.sock && this._state == SMTPState.CONNECTED) {
log(str);
this.sock.once('response', (err, msg) => {
if (err) {
caller(callback, err);
} else {
log(msg.data);
caller(callback, null, msg);
}
});
this.sock.write(str);
} else {
this.close(true);
caller(callback, SMTPError('no connection has been established', SMTPError.NOCONNECTION));
}
}
this.sock.once('response', (err, msg) => {
if (err) {
caller(callback, err);
} else {
log(msg.data);
caller(callback, null, msg);
}
});
this.sock.write(str);
} else {
this.close(true);
caller(
callback,
SMTPError('no connection has been established', SMTPError.NOCONNECTION)
);
}
}
command(cmd, callback, codes, failed) {
codes = Array.isArray(codes) ? codes : typeof(codes) == 'number' ? [codes] : [250];
command(cmd, callback, codes, failed) {
codes = Array.isArray(codes)
? codes
: typeof codes == 'number'
? [codes]
: [250];
const response = (err, msg) => {
if (err) {
caller(callback, err);
} else {
if (codes.indexOf(Number(msg.code)) != -1) {
caller(callback, err, msg.data, msg.message);
} else {
const response = (err, msg) => {
if (err) {
caller(callback, err);
} else {
if (codes.indexOf(Number(msg.code)) != -1) {
caller(callback, err, msg.data, msg.message);
} else {
const suffix = msg.message ? `: ${msg.message}` : '';
const errorMessage = `bad response on command '${cmd.split(' ')[0]}'${suffix}`;
caller(callback, SMTPError(errorMessage, SMTPError.BADRESPONSE, null, msg.data));
}
}
};
const errorMessage = `bad response on command '${
cmd.split(' ')[0]
}'${suffix}`;
caller(
callback,
SMTPError(errorMessage, SMTPError.BADRESPONSE, null, msg.data)
);
}
}
};
this.send(cmd + CRLF, response);
}
this.send(cmd + CRLF, response);
}
helo(callback, domain) {
/*
helo(callback, domain) {
/*
* SMTP 'helo' command.
* Hostname to send for self command defaults to the FQDN of the local
* host.
*/
this.command(`helo ${domain || this.domain}`, (err, data) => {
this.command(`helo ${domain || this.domain}`, (err, data) => {
if (err) {
caller(callback, err);
} else {
@ -239,92 +256,102 @@ class SMTP extends EventEmitter {
caller(callback, err, data);
}
});
}
}
starttls(callback) {
const response = (err, msg) => {
if (err) {
err.message += ' while establishing a starttls session';
caller(callback, err);
} else {
// support new API
if (tls.TLSSocket) {
const secured_socket = new tls.TLSSocket(this.sock, {
secureContext: tls.createSecureContext ? tls.createSecureContext(this.tls) : crypto.createCredentials(this.tls),
isServer: false // older versions of node (0.12), do not default to false properly...
});
starttls(callback) {
const response = (err, msg) => {
if (err) {
err.message += ' while establishing a starttls session';
caller(callback, err);
} else {
// support new API
if (tls.TLSSocket) {
const secured_socket = new tls.TLSSocket(this.sock, {
secureContext: tls.createSecureContext
? tls.createSecureContext(this.tls)
: crypto.createCredentials(this.tls),
isServer: false, // older versions of node (0.12), do not default to false properly...
});
secured_socket.on('error', (err) => {
this.close(true);
caller(callback, err);
});
secured_socket.on('error', err => {
this.close(true);
caller(callback, err);
});
this._secure = true;
this.sock = secured_socket;
this._secure = true;
this.sock = secured_socket;
SMTPResponse.monitor(this.sock, this.timeout, () => this.close(true));
caller(callback, msg.data);
} else {
const secured_socket = null;
const secured = () => {
this._secure = true;
this.sock = secured_socket;
SMTPResponse.monitor(this.sock, this.timeout, () => this.close(true));
caller(callback, msg.data);
} else {
let secured_socket = null;
const secured = () => {
this._secure = true;
this.sock = secured_socket;
SMTPResponse.monitor(this.sock, this.timeout, () => this.close(true));
caller(callback, msg.data);
};
SMTPResponse.monitor(this.sock, this.timeout, () =>
this.close(true)
);
caller(callback, msg.data);
};
const starttls = require('starttls');
secured_socket = starttls({
socket: this.sock,
host: this.host,
port: this.port,
pair: tls.createSecurePair(
tls.createSecureContext ? tls.createSecureContext(this.tls) : crypto.createCredentials(this.tls),
false)
}, secured).cleartext;
const starttls = require('starttls');
secured_socket = starttls(
{
socket: this.sock,
host: this.host,
port: this.port,
pair: tls.createSecurePair(
tls.createSecureContext
? tls.createSecureContext(this.tls)
: crypto.createCredentials(this.tls),
false
),
},
secured
).cleartext;
secured_socket.on('error', (err) => {
this.close(true);
caller(callback, err);
});
}
}
};
secured_socket.on('error', err => {
this.close(true);
caller(callback, err);
});
}
}
};
this.command('starttls', response, [220]);
}
this.command('starttls', response, [220]);
}
parse_smtp_features(data) {
// According to RFC1869 some (badly written)
// MTA's will disconnect on an ehlo. Toss an exception if
// that happens -ddm
parse_smtp_features(data) {
// According to RFC1869 some (badly written)
// MTA's will disconnect on an ehlo. Toss an exception if
// that happens -ddm
data.split('\n').forEach((ext) => {
const parse = ext.match(/^(?:\d+[\-=]?)\s*?([^\s]+)(?:\s+(.*)\s*?)?$/);
data.split('\n').forEach(ext => {
const parse = ext.match(/^(?:\d+[-=]?)\s*?([^\s]+)(?:\s+(.*)\s*?)?$/);
// To be able to communicate with as many SMTP servers as possible,
// we have to take the old-style auth advertisement into account,
// because:
// 1) Else our SMTP feature parser gets confused.
// 2) There are some servers that only advertise the auth methods we
// support using the old style.
// To be able to communicate with as many SMTP servers as possible,
// we have to take the old-style auth advertisement into account,
// because:
// 1) Else our SMTP feature parser gets confused.
// 2) There are some servers that only advertise the auth methods we
// support using the old style.
if (parse) {
// RFC 1869 requires a space between ehlo keyword and parameters.
// It's actually stricter, in that only spaces are allowed between
// parameters, but were not going to check for that here. Note
// that the space isn't present if there are no parameters.
this.features[parse[1].toLowerCase()] = parse[2] || true;
}
});
if (parse) {
// RFC 1869 requires a space between ehlo keyword and parameters.
// It's actually stricter, in that only spaces are allowed between
// parameters, but were not going to check for that here. Note
// that the space isn't present if there are no parameters.
this.features[parse[1].toLowerCase()] = parse[2] || true;
}
});
return;
}
return;
}
ehlo(callback, domain) {
this.features = {};
this.command(`ehlo ${domain || this.domain}`, (err, data) => {
ehlo(callback, domain) {
this.features = {};
this.command(`ehlo ${domain || this.domain}`, (err, data) => {
if (err) {
caller(callback, err);
} else {
@ -337,74 +364,74 @@ class SMTP extends EventEmitter {
}
}
});
}
}
has_extn(opt) {
return this.features[opt.toLowerCase()] === undefined;
}
has_extn(opt) {
return this.features[opt.toLowerCase()] === undefined;
}
help(callback, args) {
// SMTP 'help' command, returns text from the server
this.command(args ? `help ${args}` : 'help', callback, [211, 214]);
}
help(callback, args) {
// SMTP 'help' command, returns text from the server
this.command(args ? `help ${args}` : 'help', callback, [211, 214]);
}
rset(callback) {
this.command('rset', callback);
}
rset(callback) {
this.command('rset', callback);
}
noop(callback) {
this.send('noop', callback);
}
noop(callback) {
this.send('noop', callback);
}
mail(callback, from) {
this.command(`mail FROM:${from}`, callback);
}
mail(callback, from) {
this.command(`mail FROM:${from}`, callback);
}
rcpt(callback, to) {
this.command(`RCPT TO:${to}`, callback, [250, 251]);
}
rcpt(callback, to) {
this.command(`RCPT TO:${to}`, callback, [250, 251]);
}
data(callback) {
this.command('data', callback, [354]);
}
data(callback) {
this.command('data', callback, [354]);
}
data_end(callback) {
this.command(`${CRLF}.`, callback);
}
data_end(callback) {
this.command(`${CRLF}.`, callback);
}
message(data) {
log(data);
this.sock.write(data);
}
message(data) {
log(data);
this.sock.write(data);
}
verify(address, callback) {
// SMTP 'verify' command -- checks for address validity.
this.command(`vrfy ${address}`, callback, [250, 251, 252]);
}
verify(address, callback) {
// SMTP 'verify' command -- checks for address validity.
this.command(`vrfy ${address}`, callback, [250, 251, 252]);
}
expn(address, callback) {
// SMTP 'expn' command -- expands a mailing list.
this.command(`expn ${address}`, callback);
}
expn(address, callback) {
// SMTP 'expn' command -- expands a mailing list.
this.command(`expn ${address}`, callback);
}
ehlo_or_helo_if_needed(callback, domain) {
// Call this.ehlo() and/or this.helo() if needed.
// If there has been no previous EHLO or HELO command self session, self
// method tries ESMTP EHLO first.
if (!this.features) {
const response = (err, data) => caller(callback, err, data);
this.ehlo((err, data) => {
if (err) {
ehlo_or_helo_if_needed(callback, domain) {
// Call this.ehlo() and/or this.helo() if needed.
// If there has been no previous EHLO or HELO command self session, self
// method tries ESMTP EHLO first.
if (!this.features) {
const response = (err, data) => caller(callback, err, data);
this.ehlo((err, data) => {
if (err) {
this.helo(response, domain);
} else {
caller(callback, err, data);
}
}, domain);
}
}
}, domain);
}
}
login(callback, user, password, options) {
const login = {
login(callback, user, password, options) {
const login = {
user: user ? () => user : this.user,
password: password ? () => password : this.password,
method: options && options.method ? options.method.toUpperCase() : '',
@ -433,22 +460,24 @@ class SMTP extends EventEmitter {
let method = null;
const encode_cram_md5 = (challenge) => {
const encode_cram_md5 = challenge => {
const hmac = crypto.createHmac('md5', login.password());
hmac.update(Buffer.from(challenge, 'base64').toString('ascii'));
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString('base64');
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString(
'base64'
);
};
const encode_plain = () =>
Buffer
.from(`\u0000${login.user()}\u0000${login.password()}`)
.toString('base64');
Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString(
'base64'
);
// see: https://developers.google.com/gmail/xoauth2_protocol
const encode_xoauth2 = () =>
Buffer
.from(`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`)
.toString('base64');
Buffer.from(
`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`
).toString('base64');
// List of authentication methods we support: from preferred to
// less preferred methods.
@ -457,7 +486,7 @@ class SMTP extends EventEmitter {
let auth = '';
if (this.features && this.features.auth) {
if (typeof(this.features.auth) === 'string') {
if (typeof this.features.auth === 'string') {
auth = this.features.auth;
}
}
@ -474,7 +503,10 @@ class SMTP extends EventEmitter {
const failed = (err, data) => {
this.loggedin = false;
this.close(); // if auth is bad, close the connection, it won't get better by itself
caller(callback, SMTPError('authorization.failed', SMTPError.AUTHFAILED, err, data));
caller(
callback,
SMTPError('authorization.failed', SMTPError.AUTHFAILED, err, data)
);
};
const response = (err, data) => {
@ -493,7 +525,11 @@ class SMTP extends EventEmitter {
if (method == AUTH_METHODS.CRAM_MD5) {
this.command(encode_cram_md5(msg), response, [235, 503]);
} else if (method == AUTH_METHODS.LOGIN) {
this.command(Buffer.from(login.password()).toString('base64'), response, [235, 503]);
this.command(
Buffer.from(login.password()).toString('base64'),
response,
[235, 503]
);
}
}
};
@ -503,7 +539,11 @@ class SMTP extends EventEmitter {
failed(err, data);
} else {
if (method == AUTH_METHODS.LOGIN) {
this.command(Buffer.from(login.user()).toString('base64'), attempt, [334]);
this.command(
Buffer.from(login.user()).toString('base64'),
attempt,
[334]
);
}
}
};
@ -516,10 +556,24 @@ class SMTP extends EventEmitter {
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attempt_user, [334]);
break;
case AUTH_METHODS.PLAIN:
this.command(`AUTH ${AUTH_METHODS.PLAIN} ${encode_plain(login.user(), login.password())}`, response, [235, 503]);
this.command(
`AUTH ${AUTH_METHODS.PLAIN} ${encode_plain(
login.user(),
login.password()
)}`,
response,
[235, 503]
);
break;
case AUTH_METHODS.XOAUTH2:
this.command(`AUTH ${AUTH_METHODS.XOAUTH2} ${encode_xoauth2(login.user(), login.password())}`, response, [235, 503]);
this.command(
`AUTH ${AUTH_METHODS.XOAUTH2} ${encode_xoauth2(
login.user(),
login.password()
)}`,
response,
[235, 503]
);
break;
default:
const msg = 'no form of authorization supported';
@ -529,38 +583,42 @@ class SMTP extends EventEmitter {
}
};
this.ehlo_or_helo_if_needed(initiate, domain);
}
this.ehlo_or_helo_if_needed(initiate, domain);
}
close(force) {
if (this.sock) {
if (force) {
log('smtp connection destroyed!');
this.sock.destroy();
} else {
log('smtp connection closed.');
this.sock.end();
}
}
close(force) {
if (this.sock) {
if (force) {
log('smtp connection destroyed!');
this.sock.destroy();
} else {
log('smtp connection closed.');
this.sock.end();
}
}
if (this.monitor) {
this.monitor.stop();
this.monitor = null;
}
if (this.monitor) {
this.monitor.stop();
this.monitor = null;
}
this._state = SMTPState.NOTCONNECTED;
this._secure = false;
this.sock = null;
this.features = null;
this.loggedin = !(this.user() && this.password());
}
this._state = SMTPState.NOTCONNECTED;
this._secure = false;
this.sock = null;
this.features = null;
this.loggedin = !(this.user() && this.password());
}
quit(callback) {
this.command('quit', (err, data) => {
caller(callback, err, data);
this.close();
}, [221, 250]);
}
quit(callback) {
this.command(
'quit',
(err, data) => {
caller(callback, err, data);
this.close();
},
[221, 250]
);
}
}
exports.SMTP = SMTP;

View File

@ -1,64 +1,64 @@
describe('authorize plain', function() {
const { simpleParser: parser } = require('mailparser');
const { SMTPServer: smtpServer } = require('smtp-server');
const { expect } = require('chai');
const email = require('../email');
const { simpleParser: parser } = require('mailparser');
const { SMTPServer: smtpServer } = require('smtp-server');
const { expect } = require('chai');
const email = require('../email');
const port = 2526;
let server = null;
let smtp = null;
let server = null;
let smtp = null;
const send = function(message, verify, done) {
smtp.onData = function(stream, session, callback) {
parser(stream)
.then(verify)
.then(done)
.catch(done);
stream.on('end', callback);
};
server.send(message, function(err) {
if (err) throw err;
});
};
before(function(done) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
smtp.listen(port, function() {
smtp.onAuth = function(auth, session, callback) {
if (auth.username == 'pooh' && auth.password == 'honey') {
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
}
};
server = email.server.connect({
port: port,
user: 'pooh',
password: 'honey',
ssl: true
});
done();
});
});
after(function(done) {
smtp.close(done);
});
it('login', function(done) {
const message = {
subject: 'this is a test TEXT message from emailjs',
from: 'piglet@gmail.com',
to: 'pooh@gmail.com',
text: 'It is hard to be brave when you\'re only a Very Small Animal.'
const send = function(message, verify, done) {
smtp.onData = function(stream, session, callback) {
parser(stream)
.then(verify)
.then(done)
.catch(done);
stream.on('end', callback);
};
const created = email.message.create(message);
server.send(message, function(err) {
if (err) throw err;
});
};
before(function(done) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
smtp.listen(port, function() {
smtp.onAuth = function(auth, session, callback) {
if (auth.username == 'pooh' && auth.password == 'honey') {
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
}
};
server = email.server.connect({
port: port,
user: 'pooh',
password: 'honey',
ssl: true,
});
done();
});
});
after(function(done) {
smtp.close(done);
});
it('login', function(done) {
const message = {
subject: 'this is a test TEXT message from emailjs',
from: 'piglet@gmail.com',
to: 'pooh@gmail.com',
text: "It is hard to be brave when you're only a Very Small Animal.",
};
const created = email.message.create(message);
const callback = function(mail) {
expect(mail.text).to.equal(message.text + '\n\n\n');
@ -67,6 +67,6 @@ describe('authorize plain', function() {
expect(mail.to.text).to.equal(message.to);
};
send(created, callback, done);
});
send(created, callback, done);
});
});

View File

@ -1,71 +1,71 @@
describe('authorize ssl', function() {
const { simpleParser: parser } = require('mailparser');
const { SMTPServer: smtpServer } = require('smtp-server');
const { expect } = require('chai');
const email = require('../email');
const { simpleParser: parser } = require('mailparser');
const { SMTPServer: smtpServer } = require('smtp-server');
const { expect } = require('chai');
const email = require('../email');
const port = 2526;
let server = null;
let smtp = null;
let server = null;
let smtp = null;
const send = function(message, verify, done) {
smtp.onData = function(stream, session, callback) {
parser(stream)
.then(verify)
.then(done)
.catch(done);
stream.on('end', callback);
};
const send = function(message, verify, done) {
smtp.onData = function(stream, session, callback) {
parser(stream)
.then(verify)
.then(done)
.catch(done);
stream.on('end', callback);
};
server.send(message, function(err) {
if (err) throw err;
});
};
server.send(message, function(err) {
if (err) throw err;
});
};
before(function(done) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
before(function(done) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
smtp.listen(port, function() {
smtp.onAuth = function(auth, session, callback) {
if (auth.username == 'pooh' && auth.password == 'honey') {
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
}
};
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
smtp.listen(port, function() {
smtp.onAuth = function(auth, session, callback) {
if (auth.username == 'pooh' && auth.password == 'honey') {
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
}
};
server = email.server.connect({
port: port,
user: 'pooh',
password: 'honey',
ssl: true
});
done();
});
});
server = email.server.connect({
port: port,
user: 'pooh',
password: 'honey',
ssl: true,
});
done();
});
});
after(function(done) {
smtp.close(done);
});
after(function(done) {
smtp.close(done);
});
it('login', function(done) {
const message = {
subject: 'this is a test TEXT message from emailjs',
from: 'pooh@gmail.com',
to: 'rabbit@gmail.com',
text: 'hello friend, i hope this message finds you well.'
};
it('login', function(done) {
const message = {
subject: 'this is a test TEXT message from emailjs',
from: 'pooh@gmail.com',
to: 'rabbit@gmail.com',
text: 'hello friend, i hope this message finds you well.',
};
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
});

View File

@ -1,455 +1,455 @@
describe('messages', function() {
const { simpleParser: parser } = require('mailparser');
const { SMTPServer: smtpServer } = require('smtp-server');
const { expect } = require('chai');
const fs = require('fs');
const path = require('path');
const email = require('../email');
const port = 2526;
const { simpleParser: parser } = require('mailparser');
const { SMTPServer: smtpServer } = require('smtp-server');
const { expect } = require('chai');
const fs = require('fs');
const path = require('path');
const email = require('../email');
const port = 2526;
let server = null;
let smtp = null;
let server = null;
let smtp = null;
const send = function(message, verify, done) {
smtp.onData = function(stream, session, callback) {
//stream.pipe(process.stdout);
parser(stream)
.then(verify)
.then(done)
.catch(done);
stream.on('end', callback);
};
const send = function(message, verify, done) {
smtp.onData = function(stream, session, callback) {
//stream.pipe(process.stdout);
parser(stream)
.then(verify)
.then(done)
.catch(done);
stream.on('end', callback);
};
server.send(message, function(err) {
if (err) throw err;
});
};
server.send(message, function(err) {
if (err) throw err;
});
};
before(function(done) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
before(function(done) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
smtp.listen(port, function() {
smtp.onAuth = function(auth, session, callback) {
if (auth.username == 'pooh' && auth.password == 'honey') {
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
}
};
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
smtp.listen(port, function() {
smtp.onAuth = function(auth, session, callback) {
if (auth.username == 'pooh' && auth.password == 'honey') {
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
}
};
server = email.server.connect({
port: port,
user: 'pooh',
password: 'honey',
ssl: true
});
done();
});
});
server = email.server.connect({
port: port,
user: 'pooh',
password: 'honey',
ssl: true,
});
done();
});
});
after(function(done) {
smtp.close(done);
});
after(function(done) {
smtp.close(done);
});
it('simple text message', function(done) {
var message = {
subject: 'this is a test TEXT message from emailjs',
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: 'hello friend, i hope this message finds you well.',
'message-id': 'this is a special id'
};
it('simple text message', function(done) {
var message = {
subject: 'this is a test TEXT message from emailjs',
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: 'hello friend, i hope this message finds you well.',
'message-id': 'this is a special id',
};
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
expect(mail.messageId).to.equal('<' + message['message-id'] + '>');
},
done
);
});
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
expect(mail.messageId).to.equal('<' + message['message-id'] + '>');
},
done
);
});
it('null text', function(done) {
send(
{
subject: 'this is a test TEXT message from emailjs',
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: null,
'message-id': 'this is a special id'
},
function(mail) {
expect(mail.text).to.equal('\n\n\n');
},
done
);
});
it('null text', function(done) {
send(
{
subject: 'this is a test TEXT message from emailjs',
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: null,
'message-id': 'this is a special id',
},
function(mail) {
expect(mail.text).to.equal('\n\n\n');
},
done
);
});
it('empty text', function(done) {
send(
{
subject: 'this is a test TEXT message from emailjs',
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: '',
'message-id': 'this is a special id'
},
function(mail) {
expect(mail.text).to.equal('\n\n\n');
},
done
);
});
it('empty text', function(done) {
send(
{
subject: 'this is a test TEXT message from emailjs',
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: '',
'message-id': 'this is a special id',
},
function(mail) {
expect(mail.text).to.equal('\n\n\n');
},
done
);
});
it('simple unicode text message', function(done) {
var message = {
subject: 'this ✓ is a test ✓ TEXT message from emailjs',
from: 'zelda✓ <zelda@gmail.com>',
to: 'gannon✓ <gannon@gmail.com>',
text: 'hello ✓ friend, i hope this message finds you well.'
};
it('simple unicode text message', function(done) {
var message = {
subject: 'this ✓ is a test ✓ TEXT message from emailjs',
from: 'zelda✓ <zelda@gmail.com>',
to: 'gannon✓ <gannon@gmail.com>',
text: 'hello ✓ friend, i hope this message finds you well.',
};
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
it('very large text message', function(done) {
this.timeout(20000);
// thanks to jart+loberstech for this one!
var message = {
subject: 'this is a test TEXT message from emailjs',
from: 'ninjas@gmail.com',
to: 'pirates@gmail.com',
text: fs.readFileSync(
path.join(__dirname, 'attachments/smtp.txt'),
'utf-8'
)
};
it('very large text message', function(done) {
this.timeout(20000);
// thanks to jart+loberstech for this one!
var message = {
subject: 'this is a test TEXT message from emailjs',
from: 'ninjas@gmail.com',
to: 'pirates@gmail.com',
text: fs.readFileSync(
path.join(__dirname, 'attachments/smtp.txt'),
'utf-8'
),
};
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text.replace(/\r/g, '') + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
send(
email.message.create(message),
function(mail) {
expect(mail.text).to.equal(message.text.replace(/\r/g, '') + '\n\n\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
it('very large text data', function(done) {
this.timeout(10000);
var text =
'<html><body><pre>' +
fs.readFileSync(path.join(__dirname, 'attachments/smtp.txt'), 'utf-8') +
'</pre></body></html>';
var message = {
subject: 'this is a test TEXT+DATA message from emailjs',
from: 'lobsters@gmail.com',
to: 'lizards@gmail.com',
text:
'hello friend if you are seeing this, you can not view html emails. it is attached inline.',
attachment: { data: text, alternative: true }
};
it('very large text data', function(done) {
this.timeout(10000);
var text =
'<html><body><pre>' +
fs.readFileSync(path.join(__dirname, 'attachments/smtp.txt'), 'utf-8') +
'</pre></body></html>';
var message = {
subject: 'this is a test TEXT+DATA message from emailjs',
from: 'lobsters@gmail.com',
to: 'lizards@gmail.com',
text:
'hello friend if you are seeing this, you can not view html emails. it is attached inline.',
attachment: { data: text, alternative: true },
};
send(
message,
function(mail) {
expect(mail.html).to.equal(text.replace(/\r/g, ''));
expect(mail.text).to.equal(message.text + '\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
send(
message,
function(mail) {
expect(mail.html).to.equal(text.replace(/\r/g, ''));
expect(mail.text).to.equal(message.text + '\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
it('html data', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp.html'),
'utf-8'
);
var message = {
subject: 'this is a test TEXT+HTML+DATA message from emailjs',
from: 'obama@gmail.com',
to: 'mitt@gmail.com',
attachment: { data: html, alternative: true }
};
it('html data', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp.html'),
'utf-8'
);
var message = {
subject: 'this is a test TEXT+HTML+DATA message from emailjs',
from: 'obama@gmail.com',
to: 'mitt@gmail.com',
attachment: { data: html, alternative: true },
};
send(
message,
function(mail) {
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
send(
message,
function(mail) {
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(message.subject);
expect(mail.from.text).to.equal(message.from);
expect(mail.to.text).to.equal(message.to);
},
done
);
});
it('html file', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp.html'),
'utf-8'
);
var headers = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: {
path: path.join(__dirname, 'attachments/smtp.html'),
alternative: true
}
};
it('html file', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp.html'),
'utf-8'
);
var headers = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: {
path: path.join(__dirname, 'attachments/smtp.html'),
alternative: true,
},
};
send(
headers,
function(mail) {
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
send(
headers,
function(mail) {
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
it('html with image embed', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp2.html'),
'utf-8'
);
var image = fs.readFileSync(path.join(__dirname, 'attachments/smtp.gif'));
var headers = {
subject: 'this is a test TEXT+HTML+IMAGE message from emailjs',
from: 'ninja@gmail.com',
to: 'pirate@gmail.com',
attachment: {
path: path.join(__dirname, 'attachments/smtp2.html'),
alternative: true,
related: [
{
path: path.join(__dirname, 'attachments/smtp.gif'),
type: 'image/gif',
name: 'smtp-diagram.gif',
headers: { 'Content-ID': '<smtp-diagram@local>' }
}
]
}
};
it('html with image embed', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp2.html'),
'utf-8'
);
var image = fs.readFileSync(path.join(__dirname, 'attachments/smtp.gif'));
var headers = {
subject: 'this is a test TEXT+HTML+IMAGE message from emailjs',
from: 'ninja@gmail.com',
to: 'pirate@gmail.com',
attachment: {
path: path.join(__dirname, 'attachments/smtp2.html'),
alternative: true,
related: [
{
path: path.join(__dirname, 'attachments/smtp.gif'),
type: 'image/gif',
name: 'smtp-diagram.gif',
headers: { 'Content-ID': '<smtp-diagram@local>' },
},
],
},
};
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
image.toString('base64')
);
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
image.toString('base64')
);
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
it('html data and attachment', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp.html'),
'utf-8'
);
var headers = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: [
{
path: path.join(__dirname, 'attachments/smtp.html'),
alternative: true
},
{ path: path.join(__dirname, 'attachments/smtp.gif') }
]
};
it('html data and attachment', function(done) {
var html = fs.readFileSync(
path.join(__dirname, 'attachments/smtp.html'),
'utf-8'
);
var headers = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: [
{
path: path.join(__dirname, 'attachments/smtp.html'),
alternative: true,
},
{ path: path.join(__dirname, 'attachments/smtp.gif') },
],
};
send(
headers,
function(mail) {
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
send(
headers,
function(mail) {
expect(mail.html).to.equal(html.replace(/\r/g, ''));
expect(mail.text).to.equal('\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
it('attachment', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var headers = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
attachment: {
path: path.join(__dirname, 'attachments/smtp.pdf'),
type: 'application/pdf',
name: 'smtp-info.pdf'
}
};
it('attachment', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var headers = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
attachment: {
path: path.join(__dirname, 'attachments/smtp.pdf'),
type: 'application/pdf',
name: 'smtp-info.pdf',
},
};
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
it('attachment sent with unicode filename', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var headers = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
attachment: {
path: path.join(__dirname, 'attachments/smtp.pdf'),
type: 'application/pdf',
name: 'smtp-✓-info.pdf'
}
};
it('attachment sent with unicode filename', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var headers = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
attachment: {
path: path.join(__dirname, 'attachments/smtp.pdf'),
type: 'application/pdf',
name: 'smtp-✓-info.pdf',
},
};
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.attachments[0].filename).to.equal('smtp-✓-info.pdf');
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.attachments[0].filename).to.equal('smtp-✓-info.pdf');
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
it('attachments', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var tar = fs.readFileSync(
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
);
var headers = {
subject: 'this is a test TEXT+2+ATTACHMENTS message from emailjs',
from: 'sergey@gmail.com',
to: 'jobs@gmail.com',
text: 'hello friend, i hope this message and attachments finds you well.',
attachment: [
{
path: path.join(__dirname, 'attachments/smtp.pdf'),
type: 'application/pdf',
name: 'smtp-info.pdf'
},
{
path: path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz'),
type: 'application/tar-gz',
name: 'postfix.source.2.8.7.tar.gz'
}
]
};
it('attachments', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var tar = fs.readFileSync(
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
);
var headers = {
subject: 'this is a test TEXT+2+ATTACHMENTS message from emailjs',
from: 'sergey@gmail.com',
to: 'jobs@gmail.com',
text: 'hello friend, i hope this message and attachments finds you well.',
attachment: [
{
path: path.join(__dirname, 'attachments/smtp.pdf'),
type: 'application/pdf',
name: 'smtp-info.pdf',
},
{
path: path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz'),
type: 'application/tar-gz',
name: 'postfix.source.2.8.7.tar.gz',
},
],
};
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.attachments[1].content.toString('base64')).to.equal(
tar.toString('base64')
);
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.attachments[1].content.toString('base64')).to.equal(
tar.toString('base64')
);
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
it('streams', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var tar = fs.readFileSync(
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
);
var stream = fs.createReadStream(
path.join(__dirname, 'attachments/smtp.pdf')
);
var stream2 = fs.createReadStream(
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
);
var headers = {
subject:
'this is a test TEXT+2+STREAMED+ATTACHMENTS message from emailjs',
from: 'stanford@gmail.com',
to: 'mit@gmail.com',
text:
'hello friend, i hope this message and streamed attachments finds you well.',
attachment: [
{ stream: stream, type: 'application/pdf', name: 'smtp-info.pdf' },
{
stream: stream2,
type: 'application/x-gzip',
name: 'postfix.source.2.8.7.tar.gz'
}
]
};
it('streams', function(done) {
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
var tar = fs.readFileSync(
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
);
var stream = fs.createReadStream(
path.join(__dirname, 'attachments/smtp.pdf')
);
var stream2 = fs.createReadStream(
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
);
var headers = {
subject:
'this is a test TEXT+2+STREAMED+ATTACHMENTS message from emailjs',
from: 'stanford@gmail.com',
to: 'mit@gmail.com',
text:
'hello friend, i hope this message and streamed attachments finds you well.',
attachment: [
{ stream: stream, type: 'application/pdf', name: 'smtp-info.pdf' },
{
stream: stream2,
type: 'application/x-gzip',
name: 'postfix.source.2.8.7.tar.gz',
},
],
};
stream.pause();
stream2.pause();
stream.pause();
stream2.pause();
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.attachments[1].content.toString('base64')).to.equal(
tar.toString('base64')
);
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
send(
headers,
function(mail) {
expect(mail.attachments[0].content.toString('base64')).to.equal(
pdf.toString('base64')
);
expect(mail.attachments[1].content.toString('base64')).to.equal(
tar.toString('base64')
);
expect(mail.text).to.equal(headers.text + '\n');
expect(mail.subject).to.equal(headers.subject);
expect(mail.from.text).to.equal(headers.from);
expect(mail.to.text).to.equal(headers.to);
},
done
);
});
});

View File

@ -15,16 +15,17 @@ describe('Connect to wrong email server', function() {
it('Should not call callback multiple times with wrong server configuration', function(done) {
this.timeout(5000);
const server = email.server.connect({ host: 'bar.baz' });
server.send({
from: 'foo@bar.baz',
to: 'foo@bar.baz',
subject: 'hello world',
text: 'hello world',
}, function(err) {
assert.notEqual(err, null);
done();
});
server.send(
{
from: 'foo@bar.baz',
to: 'foo@bar.baz',
subject: 'hello world',
text: 'hello world',
},
function(err) {
assert.notEqual(err, null);
done();
}
);
});
});