1
0
mirror of https://github.com/eleith/emailjs.git synced 2024-07-03 11:38:50 +00:00
emailjs/smtp/client.ts

339 lines
7.2 KiB
TypeScript
Raw Normal View History

import addressparser from 'addressparser';
import { Message, create, MessageAttachment } from './message.js';
import { SMTP, SMTPState } from './smtp.js';
export interface MessageStack {
callback: (error: Error, message: Message) => void;
message: Message;
attachment: MessageAttachment;
text: string;
returnPath: string;
from: string;
to: string | string[];
cc: string[];
bcc: string[];
}
class Client {
public smtp: SMTP;
public queue: any[];
public timer: any;
public sending: boolean;
public ready: boolean;
2018-06-29 00:55:20 +00:00
/**
* @typedef {Object} MessageStack
*
2018-07-12 17:02:42 +00:00
* @typedef {Object} SMTPSocketOptions
* @property {string} key
* @property {string} ca
* @property {string} cert
*
* @typedef {Object} SMTPOptions
* @property {number} [timeout]
* @property {string} [user]
* @property {string} [password]
* @property {string} [domain]
* @property {string} [host]
* @property {number} [port]
* @property {boolean|SMTPSocketOptions} [ssl]
* @property {boolean|SMTPSocketOptions} [tls]
* @property {string[]} [authentication]
* @property {function(...any): void} [logger]
*
2018-06-29 00:55:20 +00:00
* @constructor
* @param {SMTPOptions} server smtp options
*/
2018-05-27 04:25:08 +00:00
constructor(server) {
2018-06-25 02:42:29 +00:00
this.smtp = new SMTP(server);
2018-05-27 04:25:08 +00:00
//this.smtp.debug(1);
2018-06-29 00:55:20 +00:00
/**
* @type {MessageStack[]}
*/
2018-05-27 04:25:08 +00:00
this.queue = [];
2018-06-29 00:55:20 +00:00
/**
* @type {NodeJS.Timer}
*/
2018-05-27 04:25:08 +00:00
this.timer = null;
2018-06-29 00:55:20 +00:00
/**
* @type {boolean}
*/
2018-05-27 04:25:08 +00:00
this.sending = false;
2018-06-29 00:55:20 +00:00
/**
* @type {boolean}
*/
2018-05-27 04:25:08 +00:00
this.ready = false;
}
2018-07-06 20:31:45 +00:00
/**
* @param {Message|MessageStack} msg msg
* @param {function(Error, MessageStack): void} callback callback
* @returns {void}
*/
send(msg, callback) {
/**
* @type {Message}
*/
const message =
msg instanceof Message
? msg
: this._canMakeMessage(msg)
? create(msg)
: null;
if (message == null) {
callback(
new Error('message is not a valid Message instance'),
/** @type {MessageStack} */ (msg)
);
return;
}
message.valid((valid, why) => {
if (valid) {
const stack = {
message,
to: addressparser(message.header.to),
from: addressparser(message.header.from)[0].address,
callback: (callback || function() {}).bind(this),
};
if (message.header.cc) {
stack.to = stack.to.concat(addressparser(message.header.cc));
}
if (message.header.bcc) {
stack.to = stack.to.concat(addressparser(message.header.bcc));
}
if (
message.header['return-path'] &&
addressparser(message.header['return-path']).length
) {
stack.returnPath = addressparser(
message.header['return-path']
)[0].address;
}
this.queue.push(stack);
this._poll();
} else {
callback(new Error(why), /** @type {MessageStack} */ (msg));
}
});
}
2018-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @returns {void}
*/
2018-05-27 04:25:08 +00:00
_poll() {
clearTimeout(this.timer);
2018-05-27 04:25:08 +00:00
if (this.queue.length) {
if (this.smtp.state() == SMTPState.NOTCONNECTED) {
this._connect(this.queue[0]);
} else if (
this.smtp.state() == SMTPState.CONNECTED &&
2018-05-27 04:25:08 +00:00
!this.sending &&
this.ready
) {
this._sendmail(this.queue.shift());
}
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() == SMTPState.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
}
2018-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
2018-05-27 04:25:08 +00:00
_connect(stack) {
2018-06-29 00:55:20 +00:00
/**
* @param {Error} err callback error
* @returns {void}
*/
2018-05-27 04:25:08 +00:00
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);
}
/**
2018-07-06 18:28:51 +00:00
* @private
* @param {MessageStack} msg message stack
* @returns {boolean} can make message
*/
_canMakeMessage(msg) {
return (
msg.from &&
(msg.to || msg.cc || msg.bcc) &&
(msg.text !== undefined || this._containsInlinedHtml(msg.attachment))
);
}
2018-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {*} attachment attachment
* @returns {boolean} does contain
*/
2018-05-27 04:25:08 +00:00
_containsInlinedHtml(attachment) {
if (Array.isArray(attachment)) {
2018-06-29 00:55:20 +00:00
return attachment.some(att => {
return this._isAttachmentInlinedHtml(att);
2018-05-27 04:25:08 +00:00
});
} else {
return this._isAttachmentInlinedHtml(attachment);
}
}
2018-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {*} attachment attachment
* @returns {boolean} is inlined
*/
2018-05-27 04:25:08 +00:00
_isAttachmentInlinedHtml(attachment) {
return (
attachment &&
(attachment.data || attachment.path) &&
attachment.alternative === true
);
}
2018-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
2018-06-29 03:44:54 +00:00
* @param {function(MessageStack): void} next next
* @returns {function(Error): void} callback
2018-06-29 00:55:20 +00:00
*/
2018-05-27 04:25:08 +00:00
_sendsmtp(stack, next) {
2018-06-29 00:55:20 +00:00
/**
* @param {Error} [err] error
* @returns {void}
*/
2018-05-27 04:25:08 +00:00
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-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
_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-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
_sendrcpt(stack) {
2018-07-06 17:42:59 +00:00
if (stack.to == null || typeof stack.to === 'string') {
throw new TypeError('stack.to must be array');
}
const to = stack.to.shift().address;
2018-05-27 04:25:08 +00:00
this.smtp.rcpt(
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
2018-07-06 17:42:59 +00:00
`<${to}>`
2018-05-27 04:25:08 +00:00
);
}
2018-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
_senddata(stack) {
2018-05-27 04:25:08 +00:00
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
}
2018-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
2018-05-27 04:25:08 +00:00
_sendmessage(stack) {
const stream = stack.message.stream();
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))
);
});
// 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-06-29 00:55:20 +00:00
/**
2018-07-06 18:28:51 +00:00
* @private
2018-06-29 00:55:20 +00:00
* @param {Error} err err
* @param {MessageStack} stack stack
* @returns {void}
*/
_senddone(err, stack) {
2018-05-27 04:25:08 +00:00
this.sending = false;
stack.callback(err, stack.message);
this._poll();
}
}
2011-02-23 21:23:37 +00:00
2018-07-06 20:31:11 +00:00
/**
* @param {SMTPOptions} server smtp options
* @returns {Client} the client
*/
export const connect = server => new Client(server);