emailjs/smtp/client.ts

335 lines
8.2 KiB
TypeScript
Raw Normal View History

2020-06-17 03:11:19 +00:00
import { addressparser } from './address';
import { Message, MessageAttachment, MessageHeaders } from './message';
import { SMTPConnection, SMTPConnectionOptions, SMTPState } from './connection';
export interface MessageStack {
2020-04-21 03:20:06 +00:00
callback: (error: Error | null, message: Message) => void;
message: Message;
attachment: MessageAttachment;
text: string;
returnPath: string;
from: string;
to: ReturnType<typeof addressparser>;
cc: string[];
bcc: string[];
}
2020-05-27 18:15:55 +00:00
export class SMTPClient {
public readonly smtp: SMTPConnection;
public readonly queue: MessageStack[] = [];
protected sending = false;
protected ready = false;
protected timer: NodeJS.Timer | null = null;
2018-06-29 00:55:20 +00:00
/**
2020-05-26 14:29:15 +00:00
* Create a standard SMTP client backed by a self-managed SMTP connection.
*
2020-05-26 15:04:55 +00:00
* NOTE: `host` is trimmed before being used to establish a connection; however, the original untrimmed value will still be visible in configuration.
2020-05-26 14:29:15 +00:00
*
2020-05-01 20:29:07 +00:00
* @param {SMTPConnectionOptions} server smtp options
2018-06-29 00:55:20 +00:00
*/
constructor(server: Partial<SMTPConnectionOptions>) {
this.smtp = new SMTPConnection(server);
}
2020-05-02 00:33:07 +00:00
/**
* @public
* @param {Message} msg the message to send
* @param {function(err: Error, msg: Message): void} callback .
2020-05-02 00:33:07 +00:00
* @returns {void}
*/
2020-05-27 05:47:24 +00:00
public send(
msg: Message,
callback: (err: Error | null, msg: Message) => void
) {
2020-04-21 03:20:06 +00:00
const message: Message | null =
2018-07-06 20:31:45 +00:00
msg instanceof Message
? msg
: this._canMakeMessage(msg)
2020-04-23 04:26:49 +00:00
? new Message(msg)
: null;
2018-07-06 20:31:45 +00:00
if (message == null) {
2020-04-21 03:20:06 +00:00
callback(new Error('message is not a valid Message instance'), msg);
2018-07-06 20:31:45 +00:00
return;
}
message.valid((valid, why) => {
if (valid) {
const stack = this.createMessageStack(message, callback);
2020-05-27 05:47:24 +00:00
if (stack.to.length === 0) {
return callback(new Error('No recipients found in message'), msg);
}
2018-07-06 20:31:45 +00:00
this.queue.push(stack);
this._poll();
} else {
callback(new Error(why), msg);
2018-07-06 20:31:45 +00:00
}
});
}
/**
* @public
* @description Converts a message to the raw object used by the internal stack.
* @param {Message} message message to convert
* @param {function(err: Error, msg: Message): void} callback errback
* @returns {MessageStack} raw message object
*/
public createMessageStack(
message: Message,
callback: (err: Error | null, msg: Message) => void = function () {
/* ø */
}
) {
const [{ address: from }] = addressparser(message.header.from);
const stack = {
message,
to: [] as ReturnType<typeof addressparser>,
from,
callback: callback.bind(this),
} as MessageStack;
const {
header: { to, cc, bcc, 'return-path': returnPath },
} = message;
if ((typeof to === 'string' || Array.isArray(to)) && to.length > 0) {
stack.to = addressparser(to);
}
if ((typeof cc === 'string' || Array.isArray(cc)) && cc.length > 0) {
stack.to = stack.to.concat(
addressparser(cc).filter(
(x) => stack.to.some((y) => y.address === x.address) === false
)
);
}
if ((typeof bcc === 'string' || Array.isArray(bcc)) && bcc.length > 0) {
stack.to = stack.to.concat(
addressparser(bcc).filter(
(x) => stack.to.some((y) => y.address === x.address) === false
)
);
}
if (typeof returnPath === 'string' && returnPath.length > 0) {
const parsedReturnPath = addressparser(returnPath);
if (parsedReturnPath.length > 0) {
const [{ address: returnPathAddress }] = parsedReturnPath;
2020-06-17 03:11:19 +00:00
stack.returnPath = returnPathAddress as string;
}
}
return stack;
}
2018-06-29 00:55:20 +00:00
/**
2020-05-02 00:33:07 +00:00
* @protected
2018-06-29 00:55:20 +00:00
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
protected _poll() {
if (this.timer != null) {
clearTimeout(this.timer);
}
2020-05-01 16:25:32 +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
) {
2020-04-21 03:20:06 +00:00
this._sendmail(this.queue.shift() as MessageStack);
}
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
/**
2020-05-02 00:33:07 +00:00
* @protected
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
protected _connect(stack: MessageStack) {
2018-06-29 00:55:20 +00:00
/**
* @param {Error} err callback error
* @returns {void}
*/
2020-04-26 16:20:56 +00:00
const connect = (err: Error) => {
2018-05-27 04:25:08 +00:00
if (!err) {
2020-04-21 03:20:06 +00:00
const begin = (err: Error) => {
2018-05-27 04:25:08 +00:00
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();
}
};
2020-05-01 16:25:32 +00:00
if (!this.smtp.authorized()) {
this.smtp.login(begin);
2020-05-01 16:25:32 +00:00
} else {
this.smtp.ehlo_or_helo_if_needed(begin);
2018-06-04 16:40:23 +00:00
}
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);
}
/**
2020-05-02 00:33:07 +00:00
* @protected
* @param {MessageStack} msg message stack
* @returns {boolean} can make message
*/
2020-05-02 00:33:07 +00:00
protected _canMakeMessage(msg: MessageHeaders) {
2020-05-01 16:25:32 +00:00
return (
msg.from &&
(msg.to || msg.cc || msg.bcc) &&
(msg.text !== undefined || this._containsInlinedHtml(msg.attachment))
);
}
2018-06-29 00:55:20 +00:00
/**
2020-05-02 00:33:07 +00:00
* @protected
2020-09-04 15:48:12 +00:00
* @param {MessageAttachment | MessageAttachment[]} attachment attachment
* @returns {boolean} whether the attachment contains inlined html
2018-06-29 00:55:20 +00:00
*/
2020-05-02 00:33:07 +00:00
protected _containsInlinedHtml(
attachment: MessageAttachment | MessageAttachment[]
) {
2018-05-27 04:25:08 +00:00
if (Array.isArray(attachment)) {
2020-04-23 04:26:49 +00:00
return attachment.some((att) => {
2018-06-29 00:55:20 +00:00
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
/**
2020-05-02 00:33:07 +00:00
* @protected
* @param {MessageAttachment} attachment attachment
* @returns {boolean} whether the attachment is inlined html
2018-06-29 00:55:20 +00:00
*/
2020-05-02 00:33:07 +00:00
protected _isAttachmentInlinedHtml(attachment: MessageAttachment) {
2018-05-27 04:25:08 +00:00
return (
attachment &&
(attachment.data || attachment.path) &&
attachment.alternative === true
);
}
2018-06-29 00:55:20 +00:00
/**
2020-05-02 00:33:07 +00:00
* @protected
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
*/
2020-05-02 00:33:07 +00:00
protected _sendsmtp(stack: MessageStack, next: (msg: MessageStack) => void) {
2018-06-29 00:55:20 +00:00
/**
* @param {Error} [err] error
* @returns {void}
*/
2020-05-01 16:25:32 +00:00
return (err: Error) => {
2018-05-27 04:25:08 +00:00
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
/**
2020-05-02 00:33:07 +00:00
* @protected
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
protected _sendmail(stack: MessageStack) {
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
/**
2020-05-02 00:33:07 +00:00
* @protected
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
protected _sendrcpt(stack: MessageStack) {
2018-07-06 17:42:59 +00:00
if (stack.to == null || typeof stack.to === 'string') {
throw new TypeError('stack.to must be array');
}
2020-05-24 14:47:49 +00:00
const to = stack.to.shift()?.address;
2018-05-27 04:25:08 +00:00
this.smtp.rcpt(
2020-05-01 16:25:32 +00:00
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
/**
2020-05-02 00:33:07 +00:00
* @protected
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
protected _senddata(stack: MessageStack) {
2018-05-27 04:25:08 +00:00
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
}
2018-06-29 00:55:20 +00:00
/**
2020-05-02 00:33:07 +00:00
* @protected
2018-06-29 00:55:20 +00:00
* @param {MessageStack} stack stack
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
protected _sendmessage(stack: MessageStack) {
2018-05-27 04:25:08 +00:00
const stream = stack.message.stream();
2020-04-23 04:26:49 +00:00
stream.on('data', (data) => this.smtp.message(data));
2018-05-27 04:25:08 +00:00
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
2020-04-23 04:26:49 +00:00
stream.on('error', (err) => {
2018-05-27 04:25:08 +00:00
this.smtp.close();
this._senddone(err, stack);
});
}
2018-06-29 00:55:20 +00:00
/**
2020-05-02 00:33:07 +00:00
* @protected
2018-06-29 00:55:20 +00:00
* @param {Error} err err
* @param {MessageStack} stack stack
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
protected _senddone(err: Error | null, stack: MessageStack) {
2018-05-27 04:25:08 +00:00
this.sending = false;
stack.callback(err, stack.message);
this._poll();
}
}