1
0
mirror of https://github.com/eleith/emailjs.git synced 2024-07-02 11:08:51 +00:00

smtp: repair differences with pre-conversion code

This commit is contained in:
Zack Schuster 2020-04-24 19:45:21 -07:00
parent 70620710bc
commit 1727412b98
4 changed files with 140 additions and 247 deletions

View File

@ -18,51 +18,16 @@ export interface MessageStack {
export class Client {
public smtp: SMTP;
public queue: MessageStack[] = [];
public timer: any;
public sending: boolean;
public ready: boolean;
public timer: NodeJS.Timer | null = null;
public sending = false;
public ready = false;
/**
* @typedef {Object} MessageStack
*
* @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]
*
* @constructor
* @param {SMTPOptions} server smtp options
* @param {*} server smtp options
*/
constructor(server: Partial<import('./smtp').SMTPOptions>) {
this.smtp = new SMTP(server);
//this.smtp.debug(1);
/**
* @type {NodeJS.Timer}
*/
this.timer = null;
/**
* @type {boolean}
*/
this.sending = false;
/**
* @type {boolean}
*/
this.ready = false;
}
send(msg: Message, callback: (err: Error, msg: Message) => void): void {
@ -107,7 +72,7 @@ export class Client {
this.queue.push(stack);
this._poll();
} else {
callback(new Error(why), /** @type {MessageStack} */ msg);
callback(new Error(why), msg);
}
});
}
@ -117,13 +82,15 @@ export class Client {
* @returns {void}
*/
_poll(): void {
clearTimeout(this.timer);
if (this.timer != null) {
clearTimeout(this.timer);
}
if (this.queue.length > 0) {
if (this.smtp.state == SMTPState.NOTCONNECTED) {
if (this.smtp.state() == SMTPState.NOTCONNECTED) {
this._connect(this.queue[0]);
} else if (
this.smtp.state == SMTPState.CONNECTED &&
this.smtp.state() == SMTPState.CONNECTED &&
!this.sending &&
this.ready
) {
@ -132,7 +99,7 @@ export class Client {
}
// 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) {
else if (this.smtp.state() == SMTPState.CONNECTED) {
this.timer = setTimeout(() => this.smtp.quit(), 1000);
}
}
@ -162,10 +129,10 @@ export class Client {
}
};
if (!this.smtp.isAuthorized) {
this.smtp.login(begin);
} else {
if (this.smtp.authorized()) {
this.smtp.ehlo_or_helo_if_needed(begin);
} else {
this.smtp.login(begin);
}
} else {
stack.callback(err, stack.message);
@ -189,16 +156,16 @@ export class Client {
return !!(
msg.from &&
(msg.to || msg.cc || msg.bcc) &&
(msg.text != null || this._containsInlinedHtml(msg.attachment))
(msg.text !== undefined || this._containsInlinedHtml(msg.attachment))
);
}
/**
* @private
* @param {*} attachment attachment
* @returns {boolean} does contain
* @returns {*} whether the attachment contains inlined html
*/
_containsInlinedHtml(attachment: any): boolean {
_containsInlinedHtml(attachment: any) {
if (Array.isArray(attachment)) {
return attachment.some((att) => {
return this._isAttachmentInlinedHtml(att);
@ -211,9 +178,9 @@ export class Client {
/**
* @private
* @param {*} attachment attachment
* @returns {boolean} is inlined
* @returns {boolean} whether the attachment is inlined html
*/
_isAttachmentInlinedHtml(attachment: any): boolean {
_isAttachmentInlinedHtml(attachment: any) {
return (
attachment &&
(attachment.data || attachment.path) &&
@ -267,9 +234,12 @@ export class Client {
throw new TypeError('stack.to must be array');
}
const { address: to } = stack.to.shift() ?? {};
const to = stack.to.shift()?.address;
this.smtp.rcpt(
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
this._sendsmtp(
stack,
stack.to.length > 0 ? this._sendrcpt : this._senddata
),
`<${to}>`
);
}

View File

@ -10,22 +10,22 @@ import { getRFC2822Date } from './date';
type Indexed = import('@ledge/types').Indexed;
const CRLF = '\r\n';
const CRLF = '\r\n' as const;
/**
* MIME standard wants 76 char chunks when sending out.
*/
export const MIMECHUNK: 76 = 76;
export const MIMECHUNK = 76 as const;
/**
* meets both base64 and mime divisibility
*/
export const MIME64CHUNK: 456 = (MIMECHUNK * 6) as 456;
export const MIME64CHUNK = (MIMECHUNK * 6) as 456;
/**
* size of the message stream buffer
*/
export const BUFFERSIZE: 12768 = (MIMECHUNK * 24 * 7) as 12768;
export const BUFFERSIZE = (MIMECHUNK * 24 * 7) as 12768;
export interface MessageAttachmentHeaders extends Indexed {
'content-type'?: string;
@ -34,7 +34,7 @@ export interface MessageAttachmentHeaders extends Indexed {
}
export interface AlternateMessageAttachment extends Indexed {
headers: MessageAttachmentHeaders;
headers?: MessageAttachmentHeaders;
inline: boolean;
alternative?: MessageAttachment;
related?: MessageAttachment[];
@ -97,35 +97,33 @@ function convertDashDelimitedTextToSnakeCase(text: string) {
export class Message {
attachments: any[] = [];
alternative: AlternateMessageAttachment | null = null;
header: Partial<MessageHeaders>;
content: string;
header: Partial<MessageHeaders> = {
'message-id': `<${new Date().getTime()}.${counter++}.${
process.pid
}@${hostname()}>`,
date: getRFC2822Date(),
};
content = 'text/plain; charset=utf-8';
text: any;
constructor(headers: Partial<MessageHeaders>) {
this.header = {
'message-id': `<${new Date().getTime()}.${counter++}.${
process.pid
}@${hostname()}>`,
date: getRFC2822Date(),
};
this.content = 'text/plain; charset=utf-8';
for (const 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') {
} else if (
header === 'attachment' &&
typeof headers[header] === 'object'
) {
const attachment = headers[header];
if (attachment != null) {
if (Array.isArray(attachment)) {
for (let i = 0; i < attachment.length; i++) {
this.attach(attachment[i]);
}
} else {
this.attach(attachment);
if (Array.isArray(attachment)) {
for (let i = 0; i < attachment.length; i++) {
this.attach(attachment[i]);
}
} else if (attachment != null) {
this.attach(attachment);
}
} else if (header === 'subject') {
this.header.subject = mimeWordEncode(headers.subject);
@ -166,11 +164,10 @@ export class Message {
* @param {string} [charset='utf-8'] the charset to encode as
* @returns {Message} the current Message instance
*/
attach_alternative(html: string, charset = 'utf-8'): Message {
attach_alternative(html: string, charset: string): Message {
this.alternative = {
headers: {},
data: html,
charset,
charset: charset || 'utf-8',
type: 'text/html',
inline: true,
};
@ -182,7 +179,7 @@ export class Message {
* @param {function(boolean, string): void} callback This callback is displayed as part of the Requester class.
* @returns {void}
*/
valid(callback: (arg0: boolean, arg1?: string) => void): void {
valid(callback: (arg0: boolean, arg1?: string) => void) {
if (!this.header.from) {
callback(false, 'message does not have a valid sender');
}
@ -213,10 +210,9 @@ export class Message {
}
/**
* returns a stream of the current message
* @returns {MessageStream} a stream of the current message
* @returns {*} a stream of the current message
*/
stream(): MessageStream {
stream() {
return new MessageStream(this);
}
@ -224,7 +220,7 @@ export class Message {
* @param {function(Error, string): void} callback the function to call with the error and buffer
* @returns {void}
*/
read(callback: (arg0: Error, arg1: string) => void): void {
read(callback: (err: Error, buffer: string) => void) {
let buffer = '';
const str = this.stream();
str.on('data', (data) => (buffer += data));
@ -234,46 +230,18 @@ export class Message {
}
class MessageStream extends Stream {
message: Message;
readable: boolean;
paused: boolean;
buffer: Buffer | null;
bufferIndex: number;
readable = true;
paused = false;
buffer: Buffer | null = Buffer.alloc(MIMECHUNK * 24 * 7);
bufferIndex = 0;
/**
* @param {Message} message the message to stream
* @param {*} message the message to stream
*/
constructor(message: Message) {
constructor(private message: Message) {
super();
/**
* @type {Message}
*/
this.message = message;
/**
* @type {boolean}
*/
this.readable = true;
/**
* @type {boolean}
*/
this.paused = false;
/**
* @type {Buffer}
*/
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
/**
* @type {number}
*/
this.bufferIndex = 0;
/**
* @returns {void}
*/
const output_mixed = (): void => {
const output_mixed = () => {
const boundary = generate_boundary();
output(
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
@ -303,7 +271,7 @@ class MessageStream extends Stream {
list: MessageAttachment[],
index: number,
callback: () => void
): void => {
) => {
if (index < list.length) {
output(`--${boundary}${CRLF}`);
if (list[index].related) {
@ -327,7 +295,7 @@ class MessageStream extends Stream {
*/
const output_attachment_headers = (
attachment: MessageAttachment | AlternateMessageAttachment
): void => {
) => {
let data: string[] = [];
const headers: Partial<MessageHeaders> = {
'content-type':
@ -341,8 +309,10 @@ class MessageStream extends Stream {
};
// allow sender to override default headers
for (const header in attachment.headers || {}) {
headers[header.toLowerCase()] = attachment.headers[header];
if (attachment.headers != null) {
for (const header in attachment.headers) {
headers[header.toLowerCase()] = attachment.headers[header];
}
}
for (const header in headers) {
@ -360,7 +330,7 @@ class MessageStream extends Stream {
const output_attachment = (
attachment: MessageAttachment | AlternateMessageAttachment,
callback: () => void
): void => {
) => {
const build = attachment.path
? output_file
: attachment.stream
@ -378,7 +348,7 @@ class MessageStream extends Stream {
const output_data = (
attachment: MessageAttachment | AlternateMessageAttachment,
callback: () => void
): void => {
) => {
output_base64(
attachment.encoded
? attachment.data
@ -390,7 +360,7 @@ class MessageStream extends Stream {
const output_file = (
attachment: MessageAttachment | AlternateMessageAttachment,
next: (err: NodeJS.ErrnoException) => void
): void => {
) => {
const chunk = MIME64CHUNK * 16;
const buffer = Buffer.alloc(chunk);
const closed = (fd: number) => fs.closeSync(fd);
@ -400,7 +370,7 @@ class MessageStream extends Stream {
* @param {number} fd the file descriptor
* @returns {void}
*/
const opened = (err: Error, fd: number): void => {
const opened = (err: Error, fd: number) => {
if (!err) {
const read = (err: Error, bytes: number) => {
if (!err && this.readable) {
@ -452,13 +422,13 @@ class MessageStream extends Stream {
const output_stream = (
attachment: MessageAttachment | AlternateMessageAttachment,
callback: () => void
): void => {
) => {
if (attachment.stream.readable) {
let previous = Buffer.alloc(0);
attachment.stream.resume();
(attachment as MessageAttachment).on('end', () => {
(attachment as MessageAttachment).stream.on('end', () => {
output_base64(previous.toString('base64'), callback);
this.removeListener('pause', attachment.stream.pause);
this.removeListener('resume', attachment.stream.resume);
@ -497,7 +467,7 @@ class MessageStream extends Stream {
* @param {function(): void} [callback] the function to call after output is finished
* @returns {void}
*/
const output_base64 = (data: string, callback?: () => void): void => {
const output_base64 = (data: string, callback?: () => void) => {
const loops = Math.ceil(data.length / MIMECHUNK);
let loop = 0;
while (loop < loops) {
@ -513,7 +483,7 @@ class MessageStream extends Stream {
* @param {Message} message the message to output
* @returns {void}
*/
const output_text = (message: Message): void => {
const output_text = (message: Message) => {
let data: string[] = [];
data = data.concat([
@ -537,7 +507,7 @@ class MessageStream extends Stream {
const output_alternative = (
message: Message & { alternative: AlternateMessageAttachment },
callback: () => void
): void => {
) => {
const boundary = generate_boundary();
output(
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
@ -548,7 +518,7 @@ class MessageStream extends Stream {
/**
* @returns {void}
*/
const finish = (): void => {
const finish = () => {
output([CRLF, '--', boundary, '--', CRLF, CRLF].join(''));
callback();
};
@ -568,7 +538,7 @@ class MessageStream extends Stream {
const output_related = (
message: AlternateMessageAttachment,
callback: () => void
): void => {
) => {
const boundary = generate_boundary();
output(
`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
@ -584,7 +554,7 @@ class MessageStream extends Stream {
/**
* @returns {void}
*/
const output_header_data = (): void => {
const output_header_data = () => {
if (this.message.attachments.length || this.message.alternative) {
output(`MIME-Version: 1.0${CRLF}`);
output_mixed();
@ -598,14 +568,15 @@ class MessageStream extends Stream {
/**
* @returns {void}
*/
const output_header = (): void => {
const output_header = () => {
let data: string[] = [];
for (const header in this.message.header) {
// do not output BCC in the headers (regex) nor custom Object.prototype functions...
if (
!/bcc/i.test(header) &&
Object.prototype.hasOwnProperty.call(this.message.header, header)
// eslint-disable-next-line no-prototype-builtins
this.message.header.hasOwnProperty(header)
) {
data = data.concat([
convertDashDelimitedTextToSnakeCase(header),
@ -626,11 +597,7 @@ class MessageStream extends Stream {
* @param {any[]} [args] array of arguments to pass to the callback
* @returns {void}
*/
const output = (
data: string,
callback?: (...args: any[]) => void,
args: any[] = []
) => {
const output = (data: string) => {
// can we buffer the data?
if (this.buffer != null) {
const bytes = Buffer.byteLength(data);
@ -638,9 +605,6 @@ class MessageStream extends Stream {
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) {
@ -673,24 +637,15 @@ class MessageStream extends Stream {
);
this.buffer.write(data, 0);
this.bufferIndex = bytes;
// we could get paused after emitting data...
if (typeof callback === 'function') {
if (this.paused) {
this.once('resume', () => callback.apply(null, args));
} else {
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));
} else {
// we can't empty out the buffer, so let's wait till we resume before adding to it
this.once('resume', () => output(data));
}
}
}
};
const close = (err?: any): void => {
const close = (err?: any) => {
if (err) {
this.emit('error', err);
} else {
@ -718,7 +673,7 @@ class MessageStream extends Stream {
* pause the stream
* @returns {void}
*/
pause(): void {
pause() {
this.paused = true;
this.emit('pause');
}
@ -727,7 +682,7 @@ class MessageStream extends Stream {
* resume the stream
* @returns {void}
*/
resume(): void {
resume() {
this.paused = false;
this.emit('resume');
}
@ -736,7 +691,7 @@ class MessageStream extends Stream {
* destroy the stream
* @returns {void}
*/
destroy(): void {
destroy() {
this.emit(
'destroy',
this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null
@ -747,7 +702,7 @@ class MessageStream extends Stream {
* destroy the stream at first opportunity
* @returns {void}
*/
destroySoon(): void {
destroySoon() {
this.emit('destroy');
}
}

View File

@ -12,8 +12,8 @@ export class SMTPResponse {
) {
const watch = (data: Parameters<SMTPResponse['watch']>[0]) =>
this.watch(data);
const end = () => this.end();
const close = () => this.close();
const end = (err: Error) => this.end(err);
const close = (err: Error) => this.close(err);
const error = (data: Parameters<SMTPResponse['error']>[0]) =>
this.error(data);
const timedout = (data: Parameters<SMTPResponse['timedout']>[0]) =>
@ -43,12 +43,13 @@ export class SMTPResponse {
// parse buffer for response codes
const line = this.buffer.replace('\r', '');
if (
!line
.trim()
.split(/\n/)
.pop()
?.match(/^(\d{3})\s/) ??
false
!(
line
.trim()
.split(/\n/)
.pop()
?.match(/^(\d{3})\s/) ?? false
)
) {
return;
}
@ -75,13 +76,6 @@ export class SMTPResponse {
);
}
protected watch(data: string | Buffer) {
if (data !== null) {
this.buffer += data.toString();
this.notify();
}
}
protected timedout(err: Error) {
this.stream.end();
this.stream.emit(
@ -94,17 +88,32 @@ export class SMTPResponse {
);
}
protected close() {
protected watch(data: string | Buffer) {
if (data !== null) {
this.buffer += data.toString();
this.notify();
}
}
protected close(err: Error) {
this.stream.emit(
'response',
makeSMTPError('connection has closed', SMTPErrorStates.CONNECTIONCLOSED)
makeSMTPError(
'connection has closed',
SMTPErrorStates.CONNECTIONCLOSED,
err
)
);
}
protected end() {
protected end(err: Error) {
this.stream.emit(
'response',
makeSMTPError('connection has ended', SMTPErrorStates.CONNECTIONENDED)
makeSMTPError(
'connection has ended',
SMTPErrorStates.CONNECTIONENDED,
err
)
);
}
}

View File

@ -8,10 +8,6 @@ import { SMTPResponse } from './response';
import { makeSMTPError, SMTPErrorStates } from './error';
/* eslint-disable no-unused-vars */
/**
* @readonly
* @enum
*/
export enum AUTH_METHODS {
PLAIN = 'PLAIN',
CRAM_MD5 = 'CRAM-MD5',
@ -19,10 +15,6 @@ export enum AUTH_METHODS {
XOAUTH2 = 'XOAUTH2',
}
/**
* @readonly
* @enum
*/
export enum SMTPState {
NOTCONNECTED = 0,
CONNECTING = 1,
@ -30,39 +22,11 @@ export enum SMTPState {
}
/* eslint-enable no-unused-vars */
/**
* @readonly
* @type {5000}
*/
export const DEFAULT_TIMEOUT: 5000 = 5000;
/**
* @readonly
* @type {25}
*/
const SMTP_PORT: 25 = 25;
/**
* @readonly
* @type {465}
*/
const SMTP_SSL_PORT: 465 = 465;
/**
* @readonly
* @type {587}
*/
const SMTP_TLS_PORT: 587 = 587;
/**
* @readonly
* @type {'\r\n'}
*/
const CRLF: '\r\n' = '\r\n';
/**
* @type {0 | 1}
*/
export const DEFAULT_TIMEOUT = 5000 as const;
const SMTP_PORT = 25 as const;
const SMTP_SSL_PORT = 465 as const;
const SMTP_TLS_PORT = 587 as const;
const CRLF = '\r\n' as const;
let DEBUG: 0 | 1 = 0;
/**
@ -118,34 +82,28 @@ export interface ConnectOptions {
}
export class SMTP extends EventEmitter {
private _state: 0 | 1 | 2 = SMTPState.NOTCONNECTED;
private _state: SMTPState = SMTPState.NOTCONNECTED;
private _isAuthorized = false;
private _isSecure = false;
private _user?: string = '';
private _password?: string = '';
private _timeout: number = DEFAULT_TIMEOUT;
public timeout: number = DEFAULT_TIMEOUT;
public set debug(level: 0 | 1) {
DEBUG = level;
}
public get state() {
public state() {
return this._state;
}
public get timeout() {
return this._timeout;
}
public user: () => string;
public password: () => string;
public get user() {
return this._user;
}
public get password() {
return this._password;
}
public get isAuthorized() {
/**
* @returns {boolean} whether or not the instance is authorized
*/
public authorized() {
return this._isAuthorized;
}
@ -177,9 +135,6 @@ export class SMTP extends EventEmitter {
}: Partial<SMTPOptions> = {}) {
super();
this._user = user;
this._password = password;
this.authentication = Array.isArray(authentication)
? authentication
: [
@ -190,7 +145,7 @@ export class SMTP extends EventEmitter {
];
if (typeof timeout === 'number') {
this._timeout = timeout;
this.timeout = timeout;
}
if (typeof domain === 'string') {
@ -230,6 +185,10 @@ export class SMTP extends EventEmitter {
}
this._isAuthorized = user && password ? false : true;
// keep these strings hidden when quicky debugging/logging
this.user = () => user as string;
this.password = () => password as string;
}
/**
@ -351,7 +310,7 @@ export class SMTP extends EventEmitter {
this.sock.connect(this.port, this.host, connectedErrBack);
}
this.monitor = new SMTPResponse(this.sock, this._timeout, () =>
this.monitor = new SMTPResponse(this.sock, this.timeout, () =>
this.close(true)
);
this.sock.once('response', response);
@ -483,7 +442,7 @@ export class SMTP extends EventEmitter {
this._isSecure = true;
this.sock = secureSocket;
new SMTPResponse(this.sock, this._timeout, () => this.close(true));
new SMTPResponse(this.sock, this.timeout, () => this.close(true));
caller(callback, msg.data);
}
};
@ -689,8 +648,8 @@ export class SMTP extends EventEmitter {
options: { method?: string; domain?: string } = {}
): void {
const login = {
user: () => user || this.user || '',
password: () => password || this.password || '',
user: (user?.length ?? 0) > 0 ? () => user : this.user,
password: (password?.length ?? 0) > 0 ? () => password : this.password,
method: options && options.method ? options.method.toUpperCase() : '',
};