emailjs/smtp/connection.ts

942 lines
23 KiB
TypeScript
Raw Normal View History

import { createHmac } from 'crypto';
import { EventEmitter } from 'events';
import { Socket } from 'net';
import { hostname } from 'os';
import {
connect,
createSecureContext,
ConnectionOptions,
TLSSocket,
} from 'tls';
import { SMTPError, SMTPErrorStates } from './error';
import { SMTPResponseMonitor } from './response';
2020-04-23 04:26:49 +00:00
2020-05-01 16:25:32 +00:00
/**
* @readonly
* @enum
*/
export const AUTH_METHODS = {
PLAIN: 'PLAIN',
'CRAM-MD5': 'CRAM-MD5',
LOGIN: 'LOGIN',
XOAUTH2: 'XOAUTH2',
} as const;
2020-04-23 04:26:49 +00:00
2020-05-01 16:25:32 +00:00
/**
* @readonly
* @enum
*/
export const SMTPState = {
NOTCONNECTED: 0,
CONNECTING: 1,
CONNECTED: 2,
} as const;
export const DEFAULT_TIMEOUT = 5000 as const;
2020-05-01 16:25:32 +00:00
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;
const GREYLIST_DELAY = 300 as const;
2020-05-01 16:25:32 +00:00
let DEBUG: 0 | 1 = 0;
/**
2020-09-04 15:48:12 +00:00
* @param {...any[]} args the message(s) to log
* @returns {void}
*/
2020-05-01 16:25:32 +00:00
const log = (...args: any[]) => {
2018-07-06 20:34:48 +00:00
if (DEBUG === 1) {
2020-04-23 04:26:49 +00:00
args.forEach((d) =>
2018-07-12 16:46:37 +00:00
console.log(
typeof d === 'object'
? d instanceof Error
? d.message
: JSON.stringify(d)
: d
)
);
2018-05-27 04:25:08 +00:00
}
2011-02-23 21:23:37 +00:00
};
/**
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback the function to call
* @param {...any[]} args the arguments to apply to the function
* @returns {void}
*/
2020-05-01 16:25:32 +00:00
const caller = (callback?: (...rest: any[]) => void, ...args: any[]) => {
if (typeof callback === 'function') {
2020-05-24 14:47:49 +00:00
callback(...args);
2018-05-27 04:25:08 +00:00
}
2011-02-23 21:23:37 +00:00
};
export type SMTPSocketOptions = Omit<
ConnectionOptions,
'port' | 'host' | 'path' | 'socket' | 'timeout' | 'secureContext'
>;
export interface SMTPConnectionOptions {
2020-04-21 03:20:06 +00:00
timeout: number | null;
user: string;
password: string;
domain: string;
host: string;
port: number;
ssl: boolean | SMTPSocketOptions;
tls: boolean | SMTPSocketOptions;
2020-05-01 16:25:32 +00:00
authentication: (keyof typeof AUTH_METHODS)[];
logger: (...args: any[]) => void;
}
export interface ConnectOptions {
ssl?: boolean;
}
export class SMTPConnection extends EventEmitter {
public readonly user: () => string;
public readonly password: () => string;
public readonly timeout: number = DEFAULT_TIMEOUT;
protected readonly log = log;
protected readonly authentication: (keyof typeof AUTH_METHODS)[] = [
AUTH_METHODS['CRAM-MD5'],
AUTH_METHODS.LOGIN,
AUTH_METHODS.PLAIN,
AUTH_METHODS.XOAUTH2,
];
protected _state: 0 | 1 | 2 = SMTPState.NOTCONNECTED;
protected _secure = false;
protected loggedin = false;
protected sock: Socket | TLSSocket | null = null;
protected features: { [index: string]: string | boolean } | null = null;
protected monitor: SMTPResponseMonitor | null = null;
protected domain = hostname();
protected host = 'localhost';
protected ssl: boolean | SMTPSocketOptions = false;
protected tls: boolean | SMTPSocketOptions = false;
2020-05-01 16:25:32 +00:00
protected port: number;
private greylistResponseTracker = new WeakMap<
(...rest: any[]) => void,
boolean
>();
/**
2020-05-26 07:12:25 +00:00
* SMTP class written using python's (2.7) smtplib.py as a base.
*
* To target a Message Transfer Agent (MTA), omit all options.
2020-05-26 14:29:15 +00:00
*
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.
*/
2018-07-06 17:43:46 +00:00
constructor({
timeout,
host,
user,
password,
domain,
port,
ssl,
tls,
2018-07-12 16:46:37 +00:00
logger,
2018-07-06 17:43:46 +00:00
authentication,
}: Partial<SMTPConnectionOptions> = {}) {
super();
if (Array.isArray(authentication)) {
this.authentication = authentication;
}
if (typeof timeout === 'number') {
this.timeout = timeout;
}
if (typeof domain === 'string') {
this.domain = domain;
}
if (typeof host === 'string') {
this.host = host;
}
2020-04-23 04:26:49 +00:00
if (
ssl != null &&
(typeof ssl === 'boolean' ||
(typeof ssl === 'object' && Array.isArray(ssl) === false))
) {
this.ssl = ssl;
}
2020-04-23 04:26:49 +00:00
if (
tls != null &&
(typeof tls === 'boolean' ||
(typeof tls === 'object' && Array.isArray(tls) === false))
) {
this.tls = tls;
}
2020-05-01 16:25:32 +00:00
this.port = port || (ssl ? SMTP_SSL_PORT : tls ? SMTP_TLS_PORT : SMTP_PORT);
this.loggedin = user && password ? false : true;
if (!user && (password?.length ?? 0) > 0) {
throw new Error('`password` cannot be set without `user`');
}
// keep these strings hidden when quicky debugging/logging
this.user = () => user as string;
this.password = () => password as string;
2020-05-01 16:25:32 +00:00
if (typeof logger === 'function') {
this.log = log;
}
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-05-01 16:25:32 +00:00
* @param {0 | 1} level -
* @returns {void}
*/
public debug(level: 0 | 1) {
DEBUG = level;
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-05-01 16:25:32 +00:00
* @returns {SMTPState} the current state
*/
public state() {
return this._state;
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-05-01 16:25:32 +00:00
* @returns {boolean} whether or not the instance is authorized
*/
public authorized() {
return this.loggedin;
2018-05-27 04:25:08 +00:00
}
/**
2020-05-26 14:29:15 +00:00
* Establish an 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-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {number} [port] the port to use for the connection
* @param {string} [host] the hostname to use for the connection
2018-06-28 02:57:53 +00:00
* @param {ConnectOptions} [options={}] the options
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public connect(
2020-04-23 04:26:49 +00:00
callback: (...rest: any[]) => void,
port: number = this.port,
host: string = this.host,
options: ConnectOptions = {}
2020-05-01 16:25:32 +00:00
) {
this.port = port;
this.host = host;
2018-05-27 04:25:08 +00:00
this.ssl = options.ssl || this.ssl;
if (this._state !== SMTPState.NOTCONNECTED) {
2020-04-23 04:26:49 +00:00
this.quit(() => this.connect(callback, port, host, options));
2018-05-27 04:25:08 +00:00
}
2018-06-29 03:25:10 +00:00
/**
* @returns {void}
*/
2020-05-01 16:25:32 +00:00
const connected = () => {
2018-07-12 16:46:37 +00:00
this.log(`connected: ${this.host}:${this.port}`);
2018-06-29 03:25:10 +00:00
if (this.ssl && !this.tls) {
// if key/ca/cert was passed in, check if connection is authorized
if (
typeof this.ssl !== 'boolean' &&
this.sock instanceof TLSSocket &&
!this.sock.authorized
) {
this.close(true);
caller(
callback,
SMTPError.create(
2018-06-29 03:25:10 +00:00
'could not establish an ssl connection',
SMTPErrorStates.CONNECTIONAUTH
2018-06-29 03:25:10 +00:00
)
);
} else {
2020-05-01 16:25:32 +00:00
this._secure = true;
2018-05-27 04:25:08 +00:00
}
2018-06-29 03:25:10 +00:00
}
};
/**
* @param {Error} err err
* @returns {void}
*/
2020-05-01 16:25:32 +00:00
const connectedErrBack = (err?: Error) => {
2018-06-29 03:25:10 +00:00
if (!err) {
connected();
2018-05-27 04:25:08 +00:00
} else {
this.close(true);
2018-07-12 16:46:37 +00:00
this.log(err);
2018-05-27 04:25:08 +00:00
caller(
callback,
SMTPError.create(
2020-04-23 04:26:49 +00:00
'could not connect',
SMTPErrorStates.COULDNOTCONNECT,
err
)
2018-05-27 04:25:08 +00:00
);
}
};
2020-04-23 04:26:49 +00:00
const response = (
2020-05-01 16:25:32 +00:00
err: Error | null | undefined,
2020-04-23 04:26:49 +00:00
msg: { code: string | number; data: string }
) => {
2018-05-27 04:25:08 +00:00
if (err) {
if (this._state === SMTPState.NOTCONNECTED && !this.sock) {
return;
}
this.close(true);
caller(callback, err);
} else if (msg.code == '220') {
2018-07-12 16:46:37 +00:00
this.log(msg.data);
2018-05-27 04:25:08 +00:00
// might happen first, so no need to wait on connected()
this._state = SMTPState.CONNECTED;
caller(callback, null, msg.data);
} else {
2018-07-12 16:46:37 +00:00
this.log(`response (data): ${msg.data}`);
2018-05-27 04:25:08 +00:00
this.quit(() => {
2018-06-28 02:57:53 +00:00
caller(
callback,
SMTPError.create(
2018-06-28 02:57:53 +00:00
'bad response on connection',
SMTPErrorStates.BADRESPONSE,
2018-06-28 02:57:53 +00:00
err,
msg.data
)
2018-05-27 04:25:08 +00:00
);
});
}
};
this._state = SMTPState.CONNECTING;
2018-07-12 16:46:37 +00:00
this.log(`connecting: ${this.host}:${this.port}`);
2018-05-27 04:25:08 +00:00
if (this.ssl) {
this.sock = connect(
2018-06-04 16:40:23 +00:00
this.port,
2020-05-26 14:29:15 +00:00
this.host.trim(),
2018-07-06 18:18:29 +00:00
typeof this.ssl === 'object' ? this.ssl : {},
2018-06-04 16:40:23 +00:00
connected
);
2018-05-27 04:25:08 +00:00
} else {
this.sock = new Socket();
2020-05-26 14:29:15 +00:00
this.sock.connect(this.port, this.host.trim(), connectedErrBack);
2018-05-27 04:25:08 +00:00
}
this.monitor = new SMTPResponseMonitor(this.sock, this.timeout, () =>
2018-05-27 04:25:08 +00:00
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
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @param {string} str the string to send
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-24 14:47:49 +00:00
public send(str: string, callback: (...args: any[]) => void) {
if (this.sock != null && this._state === SMTPState.CONNECTED) {
2018-07-12 16:46:37 +00:00
this.log(str);
2018-05-27 04:25:08 +00:00
this.sock.once('response', (err, msg) => {
if (err) {
caller(callback, err);
} else {
2018-07-12 16:46:37 +00:00
this.log(msg.data);
2018-05-27 04:25:08 +00:00
caller(callback, null, msg);
}
});
if (this.sock.writable) {
this.sock.write(str);
}
2018-05-27 04:25:08 +00:00
} else {
this.close(true);
caller(
callback,
SMTPError.create(
2020-04-23 04:26:49 +00:00
'no connection has been established',
SMTPErrorStates.NOCONNECTION
)
2018-05-27 04:25:08 +00:00
);
}
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @param {string} cmd command to issue
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {(number[] | number)} [codes=[250]] array codes
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public command(
2020-04-23 04:26:49 +00:00
cmd: string,
callback: (...rest: any[]) => void,
codes: number[] | number = [250]
2020-05-01 16:25:32 +00:00
) {
2018-06-28 02:57:53 +00:00
const codesArray = Array.isArray(codes)
2018-05-27 04:25:08 +00:00
? codes
: typeof codes === 'number'
2020-04-23 04:26:49 +00:00
? [codes]
: [250];
2018-05-27 04:25:08 +00:00
2020-04-23 04:26:49 +00:00
const response = (
2020-05-01 16:25:32 +00:00
err: Error | null | undefined,
2020-04-23 04:26:49 +00:00
msg: { code: string | number; data: string; message: string }
) => {
2018-05-27 04:25:08 +00:00
if (err) {
caller(callback, err);
} else {
const code = Number(msg.code);
if (codesArray.indexOf(code) !== -1) {
2018-05-27 04:25:08 +00:00
caller(callback, err, msg.data, msg.message);
} else if (
(code === 450 || code === 451) &&
msg.message.toLowerCase().includes('greylist') &&
this.greylistResponseTracker.get(response) === false
) {
this.greylistResponseTracker.set(response, true);
setTimeout(() => {
this.send(cmd + CRLF, response);
}, GREYLIST_DELAY);
2018-05-27 04:25:08 +00:00
} else {
const suffix = msg.message ? `: ${msg.message}` : '';
2018-05-27 04:25:08 +00:00
const errorMessage = `bad response on command '${
cmd.split(' ')[0]
2020-04-23 04:26:49 +00:00
}'${suffix}`;
2018-05-27 04:25:08 +00:00
caller(
callback,
SMTPError.create(
2020-04-23 04:26:49 +00:00
errorMessage,
SMTPErrorStates.BADRESPONSE,
2020-05-01 16:25:32 +00:00
null,
2020-04-23 04:26:49 +00:00
msg.data
)
2018-05-27 04:25:08 +00:00
);
}
}
};
this.greylistResponseTracker.set(response, false);
2018-05-27 04:25:08 +00:00
this.send(cmd + CRLF, response);
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @description SMTP 'helo' command.
*
* Hostname to send for self command defaults to the FQDN of the local
* host.
*
* As this command was deprecated by rfc2821, it should only be used for compatibility with non-compliant servers.
* @see https://tools.ietf.org/html/rfc2821#appendix-F.3
*
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {string} domain the domain to associate with the 'helo' request
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public helo(callback: (...rest: any[]) => void, domain?: string) {
2018-05-27 04:25:08 +00:00
this.command(`helo ${domain || this.domain}`, (err, data) => {
if (err) {
caller(callback, err);
} else {
this.parse_smtp_features(data);
caller(callback, err, data);
}
});
2018-05-27 04:25:08 +00:00
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public starttls(callback: (...rest: any[]) => void) {
2020-05-24 14:47:49 +00:00
const response = (err: Error, msg: { data: unknown }) => {
2020-04-21 03:20:06 +00:00
if (this.sock == null) {
throw new Error('null socket');
}
2018-05-27 04:25:08 +00:00
if (err) {
err.message += ' while establishing a starttls session';
caller(callback, err);
} else {
2018-07-06 18:18:29 +00:00
const secureContext = createSecureContext(
typeof this.tls === 'object' ? this.tls : {}
);
2018-06-26 05:07:40 +00:00
const secureSocket = new TLSSocket(this.sock, { secureContext });
2018-05-27 04:25:08 +00:00
2020-04-21 03:20:06 +00:00
secureSocket.on('error', (err: Error) => {
2018-06-26 05:07:40 +00:00
this.close(true);
caller(callback, err);
});
2018-05-27 04:25:08 +00:00
2020-05-01 16:25:32 +00:00
this._secure = true;
2018-06-26 05:07:40 +00:00
this.sock = secureSocket;
new SMTPResponseMonitor(this.sock, this.timeout, () =>
this.close(true)
);
2018-06-26 05:07:40 +00:00
caller(callback, msg.data);
2018-05-27 04:25:08 +00:00
}
};
this.command('starttls', response, [220]);
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @param {string} data the string to parse for features
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public parse_smtp_features(data: string) {
2018-05-27 04:25:08 +00:00
// According to RFC1869 some (badly written)
// MTA's will disconnect on an ehlo. Toss an exception if
// that happens -ddm
2020-04-23 04:26:49 +00:00
data.split('\n').forEach((ext) => {
2018-05-27 04:25:08 +00:00
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.
2020-05-01 16:25:32 +00:00
if (parse != null && this.features != null) {
2018-05-27 04:25:08 +00:00
// 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;
}
});
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {string} domain the domain to associate with the 'ehlo' request
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public ehlo(callback: (...rest: any[]) => void, domain?: string) {
2018-05-27 04:25:08 +00:00
this.features = {};
this.command(`ehlo ${domain || this.domain}`, (err, data) => {
if (err) {
caller(callback, err);
} else {
this.parse_smtp_features(data);
2020-05-01 16:25:32 +00:00
if (this.tls && !this._secure) {
this.starttls(() => this.ehlo(callback, domain));
} else {
caller(callback, err, data);
}
}
});
2018-05-27 04:25:08 +00:00
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @param {string} opt the features keyname to check
* @returns {boolean} whether the extension exists
*/
2020-09-04 05:12:49 +00:00
public has_extn(opt: string) {
2020-05-01 16:25:32 +00:00
return (this.features ?? {})[opt.toLowerCase()] === undefined;
2018-05-27 04:25:08 +00:00
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @description SMTP 'help' command, returns text from the server
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {string} domain the domain to associate with the 'help' request
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public help(callback: (...rest: any[]) => void, domain: string) {
this.command(domain ? `help ${domain}` : 'help', callback, [211, 214]);
2018-05-27 04:25:08 +00:00
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public rset(callback: (...rest: any[]) => void) {
2018-05-27 04:25:08 +00:00
this.command('rset', callback);
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public noop(callback: (...rest: any[]) => void) {
2018-05-27 04:25:08 +00:00
this.send('noop', callback);
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {string} from the sender
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public mail(callback: (...rest: any[]) => void, from: string) {
2018-05-27 04:25:08 +00:00
this.command(`mail FROM:${from}`, callback);
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {string} to the receiver
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public rcpt(callback: (...rest: any[]) => void, to: string) {
2018-05-27 04:25:08 +00:00
this.command(`RCPT TO:${to}`, callback, [250, 251]);
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public data(callback: (...rest: any[]) => void) {
2018-05-27 04:25:08 +00:00
this.command('data', callback, [354]);
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public data_end(callback: (...rest: any[]) => void) {
2018-05-27 04:25:08 +00:00
this.command(`${CRLF}.`, callback);
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @param {string} data the message to send
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public message(data: string) {
2018-07-12 16:46:37 +00:00
this.log(data);
2020-04-21 03:20:06 +00:00
this.sock?.write(data) ?? this.log('no socket to write to');
2018-05-27 04:25:08 +00:00
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @description SMTP 'verify' command -- checks for address validity.
* @param {string} address the address to validate
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public verify(address: string, callback: (...rest: any[]) => void) {
2018-05-27 04:25:08 +00:00
this.command(`vrfy ${address}`, callback, [250, 251, 252]);
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @description SMTP 'expn' command -- expands a mailing list.
* @param {string} address the mailing list to expand
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public expn(address: string, callback: (...rest: any[]) => void) {
2018-05-27 04:25:08 +00:00
this.command(`expn ${address}`, callback);
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @description Calls this.ehlo() and, if an error occurs, this.helo().
*
* If there has been no previous EHLO or HELO command self session, self
* method tries ESMTP EHLO first.
*
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {string} [domain] the domain to associate with the command
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public ehlo_or_helo_if_needed(
callback: (...rest: any[]) => void,
domain?: string
) {
// is this code callable...?
2020-05-01 16:25:32 +00:00
if (!this.features) {
2020-05-24 14:47:49 +00:00
const response = (err: Error, data: unknown) =>
caller(callback, err, data);
2018-05-27 04:25:08 +00:00
this.ehlo((err, data) => {
if (err) {
this.helo(response, domain);
} else {
caller(callback, err, data);
}
2018-05-27 04:25:08 +00:00
}, domain);
}
}
/**
2020-05-02 00:33:07 +00:00
* @public
*
* Log in on an SMTP server that requires authentication.
*
* If there has been no previous EHLO or HELO command self session, self
* method tries ESMTP EHLO first.
*
* This method will return normally if the authentication was successful.
*
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} callback function to call after response
* @param {string} [user] the username to authenticate with
* @param {string} [password] the password for the authentication
* @param {{ method: string, domain: string }} [options] login options
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public login(
2020-04-23 04:26:49 +00:00
callback: (...rest: any[]) => void,
2020-05-01 16:25:32 +00:00
user?: string,
password?: string,
2020-04-23 04:26:49 +00:00
options: { method?: string; domain?: string } = {}
2020-05-01 16:25:32 +00:00
) {
2018-05-27 04:25:08 +00:00
const login = {
2020-05-01 16:25:32 +00:00
user: user ? () => user : this.user,
password: password ? () => password : this.password,
method: options?.method?.toUpperCase() ?? '',
};
2020-05-01 16:25:32 +00:00
const domain = options?.domain || this.domain;
2020-05-24 14:47:49 +00:00
const initiate = (err: Error | null | undefined, data: unknown) => {
if (err) {
caller(callback, err);
return;
}
2020-05-01 16:25:32 +00:00
let method: keyof typeof AUTH_METHODS | null = null;
2018-06-29 03:25:10 +00:00
/**
* @param {string} challenge challenge
* @returns {string} base64 cram hash
*/
2020-09-04 05:12:49 +00:00
const encodeCramMd5 = (challenge: string) => {
const hmac = createHmac('md5', login.password());
hmac.update(Buffer.from(challenge, 'base64').toString('ascii'));
2018-05-27 04:25:08 +00:00
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString(
'base64'
);
};
2018-06-29 03:25:10 +00:00
/**
* @returns {string} base64 login/password
*/
2020-09-04 05:12:49 +00:00
const encodePlain = () =>
2018-05-27 04:25:08 +00:00
Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString(
'base64'
);
2018-06-29 03:25:10 +00:00
/**
2018-07-06 20:31:45 +00:00
* @see https://developers.google.com/gmail/xoauth2_protocol
2018-06-29 03:25:10 +00:00
* @returns {string} base64 xoauth2 auth token
*/
2020-09-04 05:12:49 +00:00
const encodeXoauth2 = () =>
2018-05-27 04:25:08 +00:00
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.
if (!method) {
const preferred = this.authentication;
let auth = '';
2020-05-01 16:25:32 +00:00
if (typeof this.features?.auth === 'string') {
auth = this.features.auth;
}
for (let i = 0; i < preferred.length; i++) {
if (auth.includes(preferred[i])) {
method = preferred[i];
break;
}
}
}
2018-06-29 03:25:10 +00:00
/**
* handle bad responses from command differently
* @param {Error} err err
2020-09-04 15:48:12 +00:00
* @param {unknown} data data
2018-06-29 03:25:10 +00:00
* @returns {void}
*/
2020-05-24 14:47:49 +00:00
const failed = (err: Error, data: unknown) => {
2020-05-01 16:25:32 +00:00
this.loggedin = false;
this.close(); // if auth is bad, close the connection, it won't get better by itself
2018-05-27 04:25:08 +00:00
caller(
callback,
SMTPError.create(
2020-04-23 04:26:49 +00:00
'authorization.failed',
SMTPErrorStates.AUTHFAILED,
err,
data
)
2018-05-27 04:25:08 +00:00
);
};
2018-06-29 03:25:10 +00:00
/**
* @param {Error} err err
2020-09-04 15:48:12 +00:00
* @param {unknown} data data
2018-06-29 03:25:10 +00:00
* @returns {void}
*/
2020-05-24 14:47:49 +00:00
const response = (err: Error | null | undefined, data: unknown) => {
if (err) {
failed(err, data);
} else {
2020-05-01 16:25:32 +00:00
this.loggedin = true;
caller(callback, err, data);
}
};
2018-06-29 03:25:10 +00:00
/**
* @param {Error} err err
2020-09-04 15:48:12 +00:00
* @param {unknown} data data
2018-06-29 03:25:10 +00:00
* @param {string} msg msg
* @returns {void}
*/
2020-05-01 16:25:32 +00:00
const attempt = (
err: Error | null | undefined,
2020-05-24 14:47:49 +00:00
data: unknown,
2020-05-01 16:25:32 +00:00
msg: string
) => {
if (err) {
failed(err, data);
} else {
2020-05-01 16:25:32 +00:00
if (method === AUTH_METHODS['CRAM-MD5']) {
this.command(encodeCramMd5(msg), response, [235, 503]);
} else if (method === AUTH_METHODS.LOGIN) {
2018-05-27 04:25:08 +00:00
this.command(
Buffer.from(login.password()).toString('base64'),
response,
[235, 503]
);
}
}
};
2018-06-29 03:25:10 +00:00
/**
* @param {Error} err err
2020-09-04 15:48:12 +00:00
* @param {unknown} data data
2018-06-29 03:25:10 +00:00
* @param {string} msg msg
* @returns {void}
*/
const attemptUser = (err: Error, data: unknown) => {
if (err) {
failed(err, data);
} else {
if (method === AUTH_METHODS.LOGIN) {
2018-05-27 04:25:08 +00:00
this.command(
Buffer.from(login.user()).toString('base64'),
attempt,
[334]
);
}
}
};
switch (method) {
2020-05-01 16:25:32 +00:00
case AUTH_METHODS['CRAM-MD5']:
this.command(`AUTH ${AUTH_METHODS['CRAM-MD5']}`, attempt, [334]);
break;
case AUTH_METHODS.LOGIN:
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attemptUser, [334]);
break;
case AUTH_METHODS.PLAIN:
2018-05-27 04:25:08 +00:00
this.command(
`AUTH ${AUTH_METHODS.PLAIN} ${encodePlain()}`,
2018-05-27 04:25:08 +00:00
response,
[235, 503]
);
break;
case AUTH_METHODS.XOAUTH2:
2018-05-27 04:25:08 +00:00
this.command(
`AUTH ${AUTH_METHODS.XOAUTH2} ${encodeXoauth2()}`,
2018-05-27 04:25:08 +00:00
response,
[235, 503]
);
break;
default:
2020-09-04 05:12:49 +00:00
caller(
callback,
SMTPError.create(
'no form of authorization supported',
SMTPErrorStates.AUTHNOTSUPPORTED,
null,
data
)
2020-04-23 04:26:49 +00:00
);
break;
}
};
2018-05-27 04:25:08 +00:00
this.ehlo_or_helo_if_needed(initiate, domain);
}
/**
2020-05-02 00:33:07 +00:00
* @public
* @param {boolean} [force=false] whether or not to force destroy the connection
* @returns {void}
*/
2020-05-24 14:47:49 +00:00
public close(force = false) {
2018-05-27 04:25:08 +00:00
if (this.sock) {
if (force) {
2018-07-12 16:46:37 +00:00
this.log('smtp connection destroyed!');
2018-05-27 04:25:08 +00:00
this.sock.destroy();
} else {
2018-07-12 16:46:37 +00:00
this.log('smtp connection closed.');
2018-05-27 04:25:08 +00:00
this.sock.end();
}
}
if (this.monitor) {
this.monitor.stop();
this.monitor = null;
}
this._state = SMTPState.NOTCONNECTED;
2020-05-01 16:25:32 +00:00
this._secure = false;
2018-05-27 04:25:08 +00:00
this.sock = null;
2020-05-01 16:25:32 +00:00
this.features = null;
this.loggedin = !(this.user() && this.password());
2018-05-27 04:25:08 +00:00
}
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @param {function(...any[]): void} [callback] function to call after response
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public quit(callback?: (...rest: any[]) => void) {
2018-05-27 04:25:08 +00:00
this.command(
'quit',
(err, data) => {
caller(callback, err, data);
this.close();
},
[221, 250]
);
}
2011-02-23 21:23:37 +00:00
}