2018-05-14 23:02:16 +00:00
|
|
|
const smtp = require('./smtp');
|
|
|
|
const message = require('./message');
|
|
|
|
const addressparser = require('addressparser');
|
|
|
|
|
|
|
|
class Client {
|
2018-05-27 04:25:08 +00:00
|
|
|
constructor(server) {
|
|
|
|
this.smtp = new smtp.SMTP(server);
|
|
|
|
//this.smtp.debug(1);
|
|
|
|
|
|
|
|
this.queue = [];
|
|
|
|
this.timer = null;
|
|
|
|
this.sending = false;
|
|
|
|
this.ready = false;
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
_poll() {
|
|
|
|
clearTimeout(this.timer);
|
2018-05-14 23:02:16 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
if (this.queue.length) {
|
|
|
|
if (this.smtp.state() == smtp.state.NOTCONNECTED) {
|
2018-05-14 23:02:16 +00:00
|
|
|
this._connect(this.queue[0]);
|
|
|
|
} else if (
|
2018-05-27 04:25:08 +00:00
|
|
|
this.smtp.state() == smtp.state.CONNECTED &&
|
|
|
|
!this.sending &&
|
|
|
|
this.ready
|
|
|
|
) {
|
|
|
|
this._sendmail(this.queue.shift());
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
}
|
2018-06-04 16:40:23 +00:00
|
|
|
// 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) {
|
2018-05-27 04:25:08 +00:00
|
|
|
this.timer = setTimeout(() => this.smtp.quit(), 1000);
|
2018-06-04 16:40:23 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_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();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-06-04 16:40:23 +00:00
|
|
|
if (!this.smtp.authorized()) {
|
|
|
|
this.smtp.login(begin);
|
|
|
|
} else {
|
|
|
|
this.smtp.ehlo_or_helo_if_needed(begin);
|
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
} 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();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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))
|
2018-06-04 16:40:23 +00:00
|
|
|
) {
|
2018-05-27 04:25:08 +00:00
|
|
|
msg = message.create(msg);
|
2018-06-04 16:40:23 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
|
|
|
|
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),
|
|
|
|
};
|
|
|
|
|
2018-06-04 16:40:23 +00:00
|
|
|
if (msg.header.cc) {
|
2018-05-27 04:25:08 +00:00
|
|
|
stack.to = stack.to.concat(addressparser(msg.header.cc));
|
2018-06-04 16:40:23 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
|
2018-06-04 16:40:23 +00:00
|
|
|
if (msg.header.bcc) {
|
2018-05-27 04:25:08 +00:00
|
|
|
stack.to = stack.to.concat(addressparser(msg.header.bcc));
|
2018-06-04 16:40:23 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
|
|
|
|
if (
|
|
|
|
msg.header['return-path'] &&
|
|
|
|
addressparser(msg.header['return-path']).length
|
2018-06-04 16:40:23 +00:00
|
|
|
) {
|
2018-05-27 04:25:08 +00:00
|
|
|
stack.returnPath = addressparser(
|
|
|
|
msg.header['return-path']
|
|
|
|
)[0].address;
|
2018-06-04 16:40:23 +00:00
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
|
|
|
|
this.queue.push(stack);
|
|
|
|
this._poll();
|
|
|
|
} else {
|
2018-05-14 23:02:16 +00:00
|
|
|
callback(new Error(why), msg);
|
|
|
|
}
|
2018-05-27 04:25:08 +00:00
|
|
|
});
|
|
|
|
} else {
|
2018-05-14 23:02:16 +00:00
|
|
|
callback(new Error('message is not a valid Message instance'), msg);
|
2018-05-27 04:25:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_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));
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_sendmail(stack) {
|
2018-05-27 04:25:08 +00:00
|
|
|
const from = stack.returnPath || stack.from;
|
|
|
|
this.sending = true;
|
|
|
|
this.smtp.mail(this._sendsmtp(stack, this._sendrcpt), '<' + from + '>');
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_sendrcpt(stack) {
|
2018-05-27 04:25:08 +00:00
|
|
|
const to = stack.to.shift().address;
|
|
|
|
this.smtp.rcpt(
|
|
|
|
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
|
|
|
|
'<' + to + '>'
|
|
|
|
);
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_senddata(stack) {
|
2018-05-27 04:25:08 +00:00
|
|
|
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
_sendmessage(stack) {
|
|
|
|
const stream = stack.message.stream();
|
2018-05-14 23:02:16 +00:00
|
|
|
|
2018-05-27 04:25:08 +00:00
|
|
|
stream.on('data', data => this.smtp.message(data));
|
|
|
|
stream.on('end', () => {
|
|
|
|
this.smtp.data_end(
|
|
|
|
this._sendsmtp(stack, () => this._senddone(null, stack))
|
|
|
|
);
|
|
|
|
});
|
2018-05-14 23:02:16 +00:00
|
|
|
|
|
|
|
// 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
|
2018-05-27 04:25:08 +00:00
|
|
|
stream.on('error', err => {
|
|
|
|
this.smtp.close();
|
|
|
|
this._senddone(err, stack);
|
|
|
|
});
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_senddone(err, stack) {
|
2018-05-27 04:25:08 +00:00
|
|
|
this.sending = false;
|
|
|
|
stack.callback(err, stack.message);
|
|
|
|
this._poll();
|
|
|
|
}
|
2018-05-14 23:02:16 +00:00
|
|
|
}
|
2011-02-23 21:23:37 +00:00
|
|
|
|
|
|
|
exports.Client = Client;
|
2018-05-14 23:02:16 +00:00
|
|
|
exports.connect = server => new Client(server);
|