Compare commits

...

21 Commits

Author SHA1 Message Date
Zack Schuster c7a4de9557 test: enable worker threads 2022-05-06 21:33:45 -07:00
Zack Schuster 64734da2f8 smtp/message: make chunk const types more strict 2022-05-06 21:23:16 -07:00
Zack Schuster 3ea0c716e1 chore: remove jsdoc bureaucracy 2022-05-06 21:23:16 -07:00
Zack Schuster 39a4b765a9 build: use default import of module package 2022-05-06 20:48:00 -07:00
Zack Schuster 9427305919 build: enable no-undef lint rule in bundle 2022-05-06 20:45:45 -07:00
Zack Schuster 69806921d1 smtp/client: fix last type error in bundle 2022-05-06 20:36:16 -07:00
Zack Schuster f9db729f6e smtp/address: convert address to array when tokenizing 2022-05-06 20:36:00 -07:00
Zack Schuster 207de70fa9 smtp/message: add type check to helper function 2022-05-06 20:28:27 -07:00
Zack Schuster 8608b929db chore: run linter on bundle prior to publish 2022-05-06 18:16:24 -07:00
Zack Schuster fabbca8de5 smtp/client: fix type error in bundle 2022-05-06 17:57:47 -07:00
Zack Schuster f0728717b9 smtp/client: fix MessageStack type 2022-05-06 17:52:49 -07:00
Zack Schuster 8cf66b9adf chore: add jsdoc comments 2022-05-06 17:52:49 -07:00
Zack Schuster 8eb674dc83 chore: upgrade deps 2022-05-06 17:52:49 -07:00
Zack Schuster 69d78cf6fe test/message: create new client for each request 2022-05-06 17:52:49 -07:00
Zack Schuster d4b15d1e74 test/queue: improve logging & increase client timeout 2022-05-06 17:52:49 -07:00
Zack Schuster 1073f165ce test/queue: refactor to differentiate between successful and failed task attempts 2022-05-06 17:52:49 -07:00
Zack Schuster f481adbe80 chore: update readme 2022-05-06 17:52:49 -07:00
Zack Schuster ad2e3231a3 chore: upgrade yarn 2022-05-06 17:52:49 -07:00
Zack Schuster 73c15c6f42 chore: add packageManager field to manifest 2022-05-06 17:52:49 -07:00
Zack Schuster 24c17bffa6 test/auth: don't wait for server to close to fulfill connection promise 2022-05-06 14:25:10 -07:00
Zack Schuster e5fd4ed8af test/queue: add failure test 2022-05-06 14:25:08 -07:00
16 changed files with 502 additions and 301 deletions

View File

@ -17,6 +17,12 @@
"error",
"unix"
],
"valid-jsdoc": "error"
"valid-jsdoc": [
"error",
{
"requireParamDescription": false,
"requireReturnDescription": false
}
]
}
}

View File

@ -3,8 +3,9 @@
send emails, html and attachments (files, streams and strings) from node.js to any smtp server
## INSTALLING
npm install emailjs
```console
$ npm install emailjs # or yarn, pnpm, etc.
```
## FEATURES
@ -22,6 +23,29 @@ send emails, html and attachments (files, streams and strings) from node.js to a
- auth access to an SMTP Server
- if your service (ex: gmail) uses two-step authentication, use an application specific password
## DEVELOPMENT
issues and pull requests are welcome!
### Setup
#### node 14+
```console
$ corepack prepare # if yarn is not installed
$ yarn
```
#### node 12
```console
$ npm install --global yarn # if yarn is not installed; see https://classic.yarnpkg.com/en/docs/install
$ yarn
```
### Testing
```console
$ yarn test
```
## EXAMPLE USAGE - text only emails
```js
@ -309,12 +333,3 @@ associative array of currently supported SMTP authentication mechanisms
eleith
zackschuster
## Testing
npm install -d
npm test
## Contributions
issues and pull requests are welcome

View File

@ -7,6 +7,4 @@ export default {
},
files: ['test/*.ts'],
nodeArguments: ['--loader=ts-node/esm'],
// makes tests far slower
workerThreads: false,
};

View File

@ -15,21 +15,22 @@
"url": "http://github.com/eleith/emailjs.git"
},
"type": "module",
"packageManager": "yarn@1.22.18",
"devDependencies": {
"@ledge/configs": "23.3.23322",
"@rollup/plugin-typescript": "8.3.2",
"@types/mailparser": "3.4.0",
"@types/node": "12.12.6",
"@types/smtp-server": "3.5.7",
"@typescript-eslint/eslint-plugin": "5.21.0",
"@typescript-eslint/parser": "5.21.0",
"@typescript-eslint/eslint-plugin": "5.22.0",
"@typescript-eslint/parser": "5.22.0",
"ava": "4.2.0",
"eslint": "8.14.0",
"eslint": "8.15.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.0.0",
"mailparser": "3.5.0",
"prettier": "2.6.2",
"rollup": "2.70.2",
"rollup": "2.72.0",
"smtp-server": "3.11.0",
"ts-node": "10.7.0",
"tslib": "2.4.0",
@ -44,7 +45,7 @@
}
},
"resolutions": {
"nodemailer": "6.7.4"
"nodemailer": "6.7.5"
},
"engines": {
"node": ">=12"
@ -61,6 +62,7 @@
"scripts": {
"build": "rollup -c rollup.config.ts",
"lint": "eslint *.ts \"+(smtp|test)/*.ts\"",
"prepublishOnly": "eslint --fix email.js",
"pretest": "yarn build",
"test": "ava"
},

View File

@ -1,4 +1,4 @@
import { builtinModules } from 'module';
import module from 'module';
import typescript from '@rollup/plugin-typescript';
export default {
@ -7,7 +7,6 @@ export default {
file: 'email.js',
format: 'es',
sourcemap: true,
banner: '/* eslint-disable no-undef */',
footer: `
/**
* @typedef {{ [index: string]: any }} AddressObject
@ -27,7 +26,7 @@ export default {
.trim()
.replace(/\t/g, ''),
},
external: builtinModules,
external: module.builtinModules,
plugins: [
typescript({ removeComments: false, include: ['email.ts', 'smtp/*'] }),
],

View File

@ -9,7 +9,7 @@ export interface AddressObject {
group?: AddressObject[];
}
/*
/**
* Operator tokens and which tokens are expected to end the sequence
*/
const OPERATORS = new Map([
@ -32,14 +32,14 @@ const OPERATORS = new Map([
* Tokenizes the original input string
*
* @param {string | string[] | undefined} address string(s) to tokenize
* @return {AddressToken[]} An array of operator|text tokens
* @return {AddressToken[]} An array of operator & text tokens
*/
function tokenizeAddress(address: string | string[] = '') {
const tokens: AddressToken[] = [];
let token: AddressToken | undefined = undefined;
let operator: string | undefined = undefined;
for (const character of address.toString()) {
for (const character of address.toString().split('')) {
if ((operator?.length ?? 0) > 0 && character === operator) {
tokens.push({ type: 'operator', value: character });
token = undefined;
@ -69,8 +69,8 @@ function tokenizeAddress(address: string | string[] = '') {
/**
* Converts tokens for a single address into an address object
*
* @param {AddressToken[]} tokens Tokens object
* @return {AddressObject[]} addresses object array
* @param {AddressToken[]} tokens
* @return {AddressObject[]}
*/
function convertAddressTokens(tokens: AddressToken[]) {
const addressObjects: AddressObject[] = [];
@ -208,8 +208,8 @@ function convertAddressTokens(tokens: AddressToken[]) {
*
* [{name: "Name", address: "address@domain"}]
*
* @param {string | string[] | undefined} address Address field
* @return {AddressObject[]} An array of address objects
* @param {string | string[] | undefined} address
* @return {AddressObject[]}
*/
export function addressparser(address?: string | string[]) {
const addresses: AddressObject[] = [];

View File

@ -1,3 +1,5 @@
import { clearTimeout, setTimeout } from 'timers';
import { addressparser } from './address.js';
import type { MessageAttachment, MessageHeaders } from './message.js';
import { Message } from './message.js';
@ -15,13 +17,9 @@ export type MessageCallback<T = Message | MessageHeaders> = <
export interface MessageStack {
callback: MessageCallback;
message: Message;
attachment: MessageAttachment;
text: string;
returnPath: string;
returnPath?: string;
from: string;
to: ReturnType<typeof addressparser>;
cc: string[];
bcc: string[];
}
export class SMTPClient {
@ -46,7 +44,7 @@ export class SMTPClient {
/**
* @public
* @template {Message | MessageHeaders} T
* @param {T} msg the message to send
* @param {T} msg
* @param {MessageCallback<T>} callback receiver for the error (if any) as well as the passed-in message / headers
* @returns {void}
*/
@ -73,12 +71,12 @@ export class SMTPClient {
/**
* @public
* @template {Message | MessageHeaders} T
* @param {T} msg the message to send
* @returns {Promise<T>} a promise that resolves to the passed-in message / headers
* @param {T} msg
* @returns {Promise<Message>} a promise that resolves to the message / headers
*/
public sendAsync<T extends Message | MessageHeaders>(msg: T) {
return new Promise<Message>((resolve, reject) => {
this.send(msg, (err, message) => {
this.send(msg, (err, /** @type {any} */ message) => {
if (err != null) {
reject(err);
} else {
@ -103,18 +101,21 @@ export class SMTPClient {
/* ø */
}
) {
const [{ address: from }] = addressparser(message.header.from);
const stack = {
message,
to: [] as ReturnType<typeof addressparser>,
from,
callback: callback.bind(this),
} as MessageStack;
const [{ address: from = '' }] = addressparser(message.header.from);
const {
header: { to, cc, bcc, 'return-path': returnPath },
} = message;
const stack: MessageStack = {
message,
to:
(typeof to === 'string' || Array.isArray(to)) && to.length > 0
? addressparser(to)
: [],
from,
callback: callback.bind(this),
};
if ((typeof to === 'string' || Array.isArray(to)) && to.length > 0) {
stack.to = addressparser(to);
}
@ -139,7 +140,7 @@ export class SMTPClient {
const parsedReturnPath = addressparser(returnPath);
if (parsedReturnPath.length > 0) {
const [{ address: returnPathAddress }] = parsedReturnPath;
stack.returnPath = returnPathAddress as string;
stack.returnPath = returnPathAddress;
}
}
@ -175,12 +176,20 @@ export class SMTPClient {
/**
* @protected
* @param {MessageStack} stack stack
* @param {MessageStack} stack
* @returns {void}
*/
protected _connect(stack: MessageStack) {
/**
* @param {Error | null} err
* @returns {void}
*/
const connect = (err: Error | null) => {
if (!err) {
/**
* @param {Error | null} err
* @returns {void}
*/
const begin = (err: Error | null) => {
if (!err) {
this.ready = true;
@ -214,8 +223,8 @@ export class SMTPClient {
/**
* @protected
* @param {MessageAttachment | MessageAttachment[]} attachment attachment
* @returns {boolean} whether the attachment contains inlined html
* @param {MessageAttachment | MessageAttachment[]} attachment
* @returns {boolean}
*/
protected _containsInlinedHtml(
attachment?: MessageAttachment | MessageAttachment[]
@ -231,8 +240,8 @@ export class SMTPClient {
/**
* @protected
* @param {MessageAttachment} attachment attachment
* @returns {boolean} whether the attachment is inlined html
* @param {MessageAttachment} attachment
* @returns {boolean}
*/
protected _isAttachmentInlinedHtml(attachment?: MessageAttachment) {
return (
@ -244,9 +253,9 @@ export class SMTPClient {
/**
* @protected
* @param {MessageStack} stack stack
* @param {function(MessageStack): void} next next
* @returns {function(Error): void} callback
* @param {MessageStack} stack
* @param {function(MessageStack): void} next
* @returns {function(Error): void}
*/
protected _sendsmtp(stack: MessageStack, next: (msg: MessageStack) => void) {
return (err: Error | null) => {
@ -262,7 +271,7 @@ export class SMTPClient {
/**
* @protected
* @param {MessageStack} stack stack
* @param {MessageStack} stack
* @returns {void}
*/
protected _sendmail(stack: MessageStack) {
@ -273,7 +282,7 @@ export class SMTPClient {
/**
* @protected
* @param {MessageStack} stack stack
* @param {MessageStack} stack
* @returns {void}
*/
protected _sendrcpt(stack: MessageStack) {
@ -290,7 +299,7 @@ export class SMTPClient {
/**
* @protected
* @param {MessageStack} stack stack
* @param {MessageStack} stack
* @returns {void}
*/
protected _senddata(stack: MessageStack) {
@ -299,7 +308,7 @@ export class SMTPClient {
/**
* @protected
* @param {MessageStack} stack stack
* @param {MessageStack} stack
* @returns {void}
*/
protected _sendmessage(stack: MessageStack) {
@ -322,8 +331,8 @@ export class SMTPClient {
/**
* @protected
* @param {Error | null} err err
* @param {MessageStack} stack stack
* @param {Error | null} err
* @param {MessageStack} stack
* @returns {void}
*/
protected _senddone(err: Error | null, stack: MessageStack) {

View File

@ -1,7 +1,10 @@
import { Buffer } from 'buffer';
import console from 'console';
import { createHmac } from 'crypto';
import { EventEmitter } from 'events';
import { Socket } from 'net';
import { hostname } from 'os';
import { setTimeout } from 'timers';
import { connect, createSecureContext, TLSSocket } from 'tls';
import type { ConnectionOptions } from 'tls';
@ -101,6 +104,7 @@ export class SMTPConnection extends EventEmitter {
AUTH_METHODS.XOAUTH2,
];
/** @type {SMTPState} */
protected _state: 0 | 1 | 2 = SMTPState.NOTCONNECTED;
protected _secure = false;
protected loggedin = false;
@ -123,7 +127,7 @@ export class SMTPConnection extends EventEmitter {
*
* NOTE: `host` is trimmed before being used to establish a connection; however, the original untrimmed value will still be visible in configuration.
*
* @param {Partial<SMTPConnectionOptions>} options options
* @param {Partial<SMTPConnectionOptions>} options
*/
constructor({
timeout,
@ -189,7 +193,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {0 | 1} level -
* @param {0 | 1} level
* @returns {void}
*/
public debug(level: 0 | 1) {
@ -198,7 +202,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @returns {0 | 1 | 2} the current state
* @returns {SMTPState}
*/
public state() {
return this._state;
@ -206,7 +210,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @returns {boolean} whether or not the instance is authorized
* @returns {boolean}
*/
public authorized() {
return this.loggedin;
@ -218,10 +222,10 @@ export class SMTPConnection extends EventEmitter {
* NOTE: `host` is trimmed before being used to establish a connection; however, the original untrimmed value will still be visible in configuration.
*
* @public
* @param {SMTPCommandCallback} 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
* @param {ConnectOptions} [options={}] the options
* @param {SMTPCommandCallback} callback
* @param {number} [port]
* @param {string} [host]
* @param {ConnectOptions} [options={}]
* @returns {void}
*/
public connect(
@ -265,7 +269,7 @@ export class SMTPConnection extends EventEmitter {
};
/**
* @param {Error} err err
* @param {Error} err
* @returns {void}
*/
const connectedErrBack = (err?: Error) => {
@ -339,8 +343,8 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {string} str the string to send
* @param {SMTPCommandCallback} callback function to call after response
* @param {string} str
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public send(str: string, callback: SMTPCommandCallback) {
@ -372,9 +376,9 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {string} cmd command to issue
* @param {SMTPCommandCallback} callback function to call after response
* @param {(number[] | number)} [codes=[250]] array codes
* @param {string} cmd
* @param {SMTPCommandCallback} callback
* @param {(number[] | number)} [codes=[250]] SMTP response code(s)
* @returns {void}
*/
public command(
@ -444,8 +448,8 @@ export class SMTPConnection extends EventEmitter {
* 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
*
* @param {SMTPCommandCallback} callback function to call after response
* @param {string} [domain] the domain to associate with the 'helo' request
* @param {SMTPCommandCallback} callback
* @param {string} [domain]
* @returns {void}
*/
public helo(callback: SMTPCommandCallback, domain?: string) {
@ -461,7 +465,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public starttls(callback: SMTPCommandCallback) {
@ -499,7 +503,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {string} data the string to parse for features
* @param {string} data
* @returns {void}
*/
public parse_smtp_features(data: string) {
@ -529,8 +533,8 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {string} [domain] the domain to associate with the 'ehlo' request
* @param {SMTPCommandCallback} callback
* @param {string} [domain]
* @returns {void}
*/
public ehlo(callback: SMTPCommandCallback, domain?: string) {
@ -553,7 +557,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {string} opt the features keyname to check
* @returns {boolean} whether the extension exists
* @returns {boolean}
*/
public has_extn(opt: string) {
return (this.features ?? {})[opt.toLowerCase()] === undefined;
@ -562,8 +566,8 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @description SMTP 'help' command, returns text from the server
* @param {SMTPCommandCallback} callback function to call after response
* @param {string} domain the domain to associate with the 'help' request
* @param {SMTPCommandCallback} callback
* @param {string} domain
* @returns {void}
*/
public help(callback: SMTPCommandCallback, domain: string) {
@ -572,7 +576,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public rset(callback: SMTPCommandCallback) {
@ -581,7 +585,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public noop(callback: SMTPCommandCallback) {
@ -590,7 +594,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @param {string} from the sender
* @returns {void}
*/
@ -600,7 +604,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @param {string} to the receiver
* @returns {void}
*/
@ -610,7 +614,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public data(callback: SMTPCommandCallback) {
@ -619,7 +623,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public data_end(callback: SMTPCommandCallback) {
@ -639,8 +643,8 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @description SMTP 'verify' command -- checks for address validity.
* @param {string} address the address to validate
* @param {SMTPCommandCallback} callback function to call after response
* @param {string} address
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public verify(address: string, callback: SMTPCommandCallback) {
@ -650,8 +654,8 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @description SMTP 'expn' command -- expands a mailing list.
* @param {string} address the mailing list to expand
* @param {SMTPCommandCallback} callback function to call after response
* @param {string} address
* @param {SMTPCommandCallback} callback
* @returns {void}
*/
public expn(address: string, callback: SMTPCommandCallback) {
@ -665,7 +669,7 @@ export class SMTPConnection extends EventEmitter {
* If there has been no previous EHLO or HELO command self session, self
* method tries ESMTP EHLO first.
*
* @param {SMTPCommandCallback} callback function to call after response
* @param {SMTPCommandCallback} callback
* @param {string} [domain] the domain to associate with the command
* @returns {void}
*/
@ -694,12 +698,12 @@ export class SMTPConnection extends EventEmitter {
*
* This method will return normally if the authentication was successful.
*
* @param {SMTPCommandCallback} callback function to call after response
* @param {string} [user] the username to authenticate with
* @param {string} [password] the password for the authentication
* @param {Object} [options] login options
* @param {string} [options.method] login method
* @param {string} [options.domain] login domain
* @param {SMTPCommandCallback} callback
* @param {string} [user]
* @param {string} [password]
* @param {Object} [options]
* @param {string} [options.method]
* @param {string} [options.domain]
* @returns {void}
*/
public login(
@ -716,6 +720,11 @@ export class SMTPConnection extends EventEmitter {
const domain = options?.domain || this.domain;
/**
* @param {Error | null} err
* @param {unknown} data
* @returns {void}
*/
const initiate = (err: Error | null | undefined, data: unknown) => {
if (err) {
callback(err);
@ -725,7 +734,7 @@ export class SMTPConnection extends EventEmitter {
let method: keyof typeof AUTH_METHODS | null = null;
/**
* @param {string} challenge challenge
* @param {string} challenge
* @returns {string} base64 cram hash
*/
const encodeCramMd5 = (challenge: string) => {
@ -776,8 +785,8 @@ export class SMTPConnection extends EventEmitter {
/**
* handle bad responses from command differently
* @param {Error} err err
* @param {unknown} data data
* @param {Error} err
* @param {unknown} data
* @returns {void}
*/
const failed = (err: Error, data: unknown) => {
@ -793,6 +802,15 @@ export class SMTPConnection extends EventEmitter {
);
};
/**
* @param {Error | SMTPError | null} err
* @param {(
* string |
* { code: (string | number), data: string, message: string } |
* null
* )} [data]
* @returns {void}
*/
const response: SMTPCommandCallback = (err, data) => {
if (err) {
failed(err, data);
@ -802,6 +820,16 @@ export class SMTPConnection extends EventEmitter {
}
};
/**
* @param {Error | SMTPError | null} err
* @param {(
* string |
* { code: (string | number), data: string, message: string } |
* null
* )} data
* @param {string} msg
* @returns {void}
*/
const attempt: SMTPCommandCallback = (err, data, msg) => {
if (err) {
failed(err, data);
@ -818,6 +846,15 @@ export class SMTPConnection extends EventEmitter {
}
};
/**
* @param {Error | SMTPError | null} err
* @param {(
* string |
* { code: (string | number), data: string, message: string } |
* null
* )} [data]
* @returns {void}
*/
const attemptUser: SMTPCommandCallback = (err, data) => {
if (err) {
failed(err, data);
@ -871,7 +908,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {boolean} [force=false] whether or not to force destroy the connection
* @param {boolean} [force=false]
* @returns {void}
*/
public close(force = false) {
@ -899,7 +936,7 @@ export class SMTPConnection extends EventEmitter {
/**
* @public
* @param {SMTPCommandCallback} [callback] function to call after response
* @param {SMTPCommandCallback} [callback]
* @returns {void}
*/
public quit(callback?: SMTPCommandCallback) {

View File

@ -44,7 +44,7 @@ const rfc2822re =
/**
* @param {string} date a string to check for conformance to the [rfc2822](https://tools.ietf.org/html/rfc2822#section-3.3) standard
* @returns {boolean} the result of the conformance check
* @returns {boolean}
*/
export function isRFC2822Date(date: string) {
return rfc2822re.test(date);

View File

@ -22,7 +22,7 @@ export class SMTPError extends Error {
/**
* @protected
* @param {string} message error message
* @param {string} message
*/
protected constructor(message: string) {
super(message);
@ -30,11 +30,11 @@ export class SMTPError extends Error {
/**
*
* @param {string} message error message
* @param {string} message
* @param {number} code smtp error state
* @param {Error | null} [error] previous error
* @param {unknown} [smtp] arbitrary data
* @returns {SMTPError} error
* @returns {SMTPError}
*/
public static create(
message: string,

View File

@ -1,3 +1,4 @@
import { Buffer } from 'buffer';
import type { PathLike } from 'fs';
import {
existsSync,
@ -7,6 +8,7 @@ import {
read as readFile,
} from 'fs';
import { hostname } from 'os';
import { nextTick, pid } from 'process';
import { Stream } from 'stream';
import type { Readable } from 'stream';
@ -18,18 +20,21 @@ const CRLF = '\r\n' as const;
/**
* MIME standard wants 76 char chunks when sending out.
* @type {76}
*/
export const MIMECHUNK = 76 as const;
/**
* meets both base64 and mime divisibility
* @type {456}
*/
export const MIME64CHUNK = (MIMECHUNK * 6) as 456;
export const MIME64CHUNK = 456 as const; // MIMECHUNK * 6
/**
* size of the message stream buffer
* @type {12768}
*/
export const BUFFERSIZE = (MIMECHUNK * 24 * 7) as 12768;
export const BUFFERSIZE = 12768 as const; // MIMECHUNK * 24 * 7;
export interface MessageAttachmentHeaders {
[index: string]: string | undefined;
@ -117,9 +122,7 @@ function convertDashDelimitedTextToSnakeCase(text: string) {
export class Message {
public readonly attachments: MessageAttachment[] = [];
public readonly header: Partial<MessageHeaders> = {
'message-id': `<${new Date().getTime()}.${counter++}.${
process.pid
}@${hostname()}>`,
'message-id': `<${new Date().getTime()}.${counter++}.${pid}@${hostname()}>`,
date: getRFC2822Date(),
};
public readonly content: string = 'text/plain; charset=utf-8';
@ -135,7 +138,7 @@ export class Message {
* - You can also add whatever other headers you want.
*
* @see https://tools.ietf.org/html/rfc2822
* @param {Partial<MessageHeaders>} headers Message headers
* @param {Partial<MessageHeaders>} headers
*/
constructor(headers: Partial<MessageHeaders> = {}) {
for (const header in headers) {
@ -175,7 +178,7 @@ export class Message {
* Can be called multiple times, each adding a new attachment.
*
* @public
* @param {MessageAttachment} options attachment options
* @param {MessageAttachment} options
* @returns {Message} the current instance for chaining
*/
public attach(options: MessageAttachment) {
@ -257,7 +260,7 @@ export class Message {
/**
* @public
* @param {function(Error, string): void} callback the function to call with the error and buffer
* @param {function(Error, string): void} callback
* @returns {void}
*/
public read(callback: (err: Error, buffer: string) => void) {
@ -268,6 +271,10 @@ export class Message {
str.on('error', (err) => callback(err, buffer));
}
/**
* @public
* @returns {Promise<string>}
*/
public readAsync() {
return new Promise<string>((resolve, reject) => {
this.read((err, buffer) => {
@ -282,19 +289,20 @@ export class Message {
}
class MessageStream extends Stream {
readable = true;
paused = false;
/** @type {Buffer | null} */
buffer: Buffer | null = Buffer.alloc(MIMECHUNK * 24 * 7);
bufferIndex = 0;
paused = false;
readable = true;
/**
* @param {Message} message the message to stream
* @param {Message} message
*/
constructor(private message: Message) {
super();
/**
* @param {string} data the data to output
* @param {string} data
* @returns {void}
*/
const output = (data: string) => {
@ -346,7 +354,7 @@ class MessageStream extends Stream {
};
/**
* @param {MessageAttachment} attachment the attachment whose headers you would like to output
* @param {MessageAttachment} attachment
* @returns {void}
*/
const outputAttachmentHeaders = (attachment: MessageAttachment) => {
@ -384,8 +392,8 @@ class MessageStream extends Stream {
};
/**
* @param {string} data the data to output as base64
* @param {function(): void} [callback] the function to call after output is finished
* @param {string} data
* @param {function(): void} [callback]
* @returns {void}
*/
const outputBase64 = (data: string, callback?: () => void) => {
@ -400,6 +408,11 @@ class MessageStream extends Stream {
}
};
/**
* @param {MessageAttachment} attachment
* @param {function((NodeJS.ErrnoException | null)): void} next
* @returns {void}
*/
const outputFile = (
attachment: MessageAttachment,
next: (err: NodeJS.ErrnoException | null) => void
@ -417,7 +430,7 @@ class MessageStream extends Stream {
: inputEncoding;
/**
* @param {NodeJS.ErrnoException | null} err the error to emit
* @param {NodeJS.ErrnoException | null} err
* @param {number} fd the file descriptor
* @returns {void}
*/
@ -457,8 +470,8 @@ class MessageStream extends Stream {
};
/**
* @param {MessageAttachment} attachment the metadata to use as headers
* @param {function(): void} callback the function to call after output is finished
* @param {MessageAttachment} attachment
* @param {function(): void} callback
* @returns {void}
*/
const outputStream = (
@ -505,6 +518,11 @@ class MessageStream extends Stream {
}
};
/**
* @param {MessageAttachment} attachment
* @param {function(): void} callback
* @returns {void}
*/
const outputAttachment = (
attachment: MessageAttachment,
callback: () => void
@ -548,6 +566,9 @@ class MessageStream extends Stream {
}
};
/**
* @returns {void}
*/
const outputMixed = () => {
const boundary = generateBoundary();
output(
@ -567,8 +588,8 @@ class MessageStream extends Stream {
};
/**
* @param {MessageAttachment} attachment the metadata to use as headers
* @param {function(): void} callback the function to call after output is finished
* @param {MessageAttachment} attachment
* @param {function(): void} callback
* @returns {void}
*/
const outputData = (
@ -584,7 +605,7 @@ class MessageStream extends Stream {
};
/**
* @param {Message} message the message to output
* @param {Message} message
* @returns {void}
*/
const outputText = (message: Message) => {
@ -604,8 +625,8 @@ class MessageStream extends Stream {
};
/**
* @param {MessageAttachment} message the message to output
* @param {function(): void} callback the function to call after output is finished
* @param {MessageAttachment} message
* @param {function(): void} callback
* @returns {void}
*/
const outputRelated = (
@ -625,14 +646,18 @@ class MessageStream extends Stream {
};
/**
* @param {Message} message the message to output
* @param {function(): void} callback the function to call after output is finished
* @param {Message} message
* @param {function(): void} callback
* @returns {void}
*/
const outputAlternative = (
message: Message & { alternative: MessageAttachment },
callback: () => void
) => {
const outputAlternative = (message: Message, callback: () => void) => {
const { alternative } = message;
if (alternative == null) {
throw new Error(
`Message passed to outputAlternative without its "alternative" property set: ${message.header.subject}`
);
}
const boundary = generateBoundary();
output(
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
@ -648,7 +673,6 @@ class MessageStream extends Stream {
callback();
};
const { alternative } = message;
if (alternative.related) {
outputRelated(alternative, finish);
} else {
@ -656,6 +680,10 @@ class MessageStream extends Stream {
}
};
/**
* @param {Error} [err]
* @returns {void}
*/
const close = (err?: Error) => {
if (err) {
this.emit('error', err);
@ -716,12 +744,12 @@ class MessageStream extends Stream {
};
this.once('destroy', close);
process.nextTick(outputHeader);
nextTick(outputHeader);
}
/**
* @public
* pause the stream
* @description pause the stream
* @returns {void}
*/
public pause() {
@ -731,7 +759,7 @@ class MessageStream extends Stream {
/**
* @public
* resume the stream
* @description resume the stream
* @returns {void}
*/
public resume() {
@ -741,7 +769,7 @@ class MessageStream extends Stream {
/**
* @public
* destroy the stream
* @description destroy the stream
* @returns {void}
*/
public destroy() {
@ -753,7 +781,7 @@ class MessageStream extends Stream {
/**
* @public
* destroy the stream at first opportunity
* @description destroy the stream at first opportunity
* @returns {void}
*/
public destroySoon() {

View File

@ -3,9 +3,7 @@ import { TextDecoder, TextEncoder } from 'util';
const encoder = new TextEncoder();
/**
* @see https://tools.ietf.org/html/rfc2045#section-6.7
*/
/** @see https://tools.ietf.org/html/rfc2045#section-6.7 */
const RANGES = [
[0x09], // <TAB>
[0x0a], // <LF>
@ -114,8 +112,7 @@ function splitMimeEncodedString(str: string, maxlen = 12) {
}
/**
*
* @param {number} nr number
* @param {number} nr
* @returns {boolean} if number is in range
*/
function checkRanges(nr: number) {

View File

@ -46,13 +46,12 @@ function connect({
: { ssl: secure, user: 'pooh', password: 'honey' }
);
new SMTPConnection(options).connect((err) => {
server.close(() => {
if (err) {
reject(err.message);
} else {
resolve();
}
});
server.close();
if (err) {
reject(err.message);
} else {
resolve();
}
});
});
});

View File

@ -1,7 +1,9 @@
import { createReadStream, readFileSync } from 'fs';
import { performance } from 'perf_hooks';
import { URL } from 'url';
import test from 'ava';
import type { ExecutionContext, LogFn } from 'ava';
import { simpleParser } from 'mailparser';
import type { AddressObject, ParsedMail } from 'mailparser';
import { SMTPServer } from 'smtp-server';
@ -25,20 +27,15 @@ const tarFixtureUrl = new URL(
const tarFixture = readFileSync(tarFixtureUrl, 'base64');
/**
* \@types/mailparser@3.0.2 breaks our code
* \@types/mailparser\@3.0.2 breaks our code
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50744
*/
type ParsedMailCompat = Omit<ParsedMail, 'to'> & { to?: AddressObject };
const port = 5555;
const parseMap = new Map<string, ParsedMailCompat>();
const client = new SMTPClient({
port,
user: 'pooh',
password: 'honey',
ssl: true,
});
const parseMap = new Map<string, ParsedMailCompat>();
const logFnMap = new Map<string, LogFn>();
const server = new SMTPServer({
secure: true,
onAuth(auth, _session, callback) {
@ -48,49 +45,57 @@ const server = new SMTPServer({
return callback(new Error('invalid user / pass'));
}
},
async onData(stream, _session, callback: () => void) {
async onData(stream, _session, callback) {
const now = performance.now();
const mail = (await simpleParser(stream, {
skipHtmlToText: true,
skipTextToHtml: true,
skipImageLinks: true,
} as Record<string, unknown>)) as ParsedMailCompat;
parseMap.set(mail.subject as string, mail);
})) as ParsedMailCompat;
const { subject = '' } = mail;
(logFnMap.get(subject) as LogFn)(
`Time to parse message: ${Math.round(performance.now() - now)}ms`
);
parseMap.set(subject, mail);
callback();
},
});
function send(headers: Partial<MessageHeaders>) {
test.before((t) => server.listen(port, t.pass));
test.after((t) => server.close(t.pass));
function send(t: ExecutionContext, headers: Partial<MessageHeaders>) {
return new Promise<ParsedMailCompat>((resolve, reject) => {
const { subject = '' } = headers;
logFnMap.set(subject, t.log);
const client = new SMTPClient({
port,
user: 'pooh',
password: 'honey',
ssl: true,
});
client.send(new Message(headers), (err) => {
if (err) {
reject(err);
} else {
resolve(parseMap.get(headers.subject as string) as ParsedMailCompat);
resolve(parseMap.get(subject) as ParsedMailCompat);
}
});
});
}
test.before(async (t) => {
server.listen(port, t.pass);
});
test.after(async (t) => {
server.close(t.pass);
});
test('simple text message', async (t) => {
const msg = {
subject: 'this is a test TEXT message from emailjs',
subject: t.title,
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
cc: 'gannon@gmail.com',
bcc: 'gannon@gmail.com',
text: 'hello friend, i hope this message finds you well.',
'message-id': 'this is a special id',
'message-id': 'special id',
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.text, msg.text + '\n\n\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -100,39 +105,39 @@ test('simple text message', async (t) => {
test('null text message', async (t) => {
const msg = {
subject: 'this is a test TEXT message from emailjs',
subject: t.title,
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: null,
'message-id': 'this is a special id',
'message-id': 'special id',
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.text, '\n\n\n');
});
test('empty text message', async (t) => {
const msg = {
subject: 'this is a test TEXT message from emailjs',
subject: t.title,
from: 'zelda@gmail.com',
to: 'gannon@gmail.com',
text: '',
'message-id': 'this is a special id',
'message-id': 'special id',
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.text, '\n\n\n');
});
test('simple unicode text message', async (t) => {
test('simple unicode text message', async (t) => {
const msg = {
subject: 'this ✓ is a test ✓ TEXT message from emailjs',
subject: t.title,
from: 'zelda✓ <zelda@gmail.com>',
to: 'gannon✓ <gannon@gmail.com>',
text: 'hello ✓ friend, i hope this message finds you well.',
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.text, msg.text + '\n\n\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -142,24 +147,24 @@ test('simple unicode text message', async (t) => {
test('very large text message', async (t) => {
// thanks to jart+loberstech for this one!
const msg = {
subject: 'this is a test TEXT message from emailjs',
subject: t.title,
from: 'ninjas@gmail.com',
to: 'pirates@gmail.com',
text: textFixture,
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.text, msg.text.replace(/\r/g, '') + '\n\n\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
t.is(mail.to?.text, msg.to);
});
test('very large text data message', async (t) => {
test('very large text + data message', async (t) => {
const text = '<html><body><pre>' + textFixture + '</pre></body></html>';
const msg = {
subject: 'this is a test TEXT+DATA message from emailjs',
subject: t.title,
from: 'lobsters@gmail.com',
to: 'lizards@gmail.com',
text: 'hello friend if you are seeing this, you can not view html emails. it is attached inline.',
@ -169,7 +174,7 @@ test('very large text data message', async (t) => {
},
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.html, text.replace(/\r/g, ''));
t.is(mail.text, msg.text + '\n');
t.is(mail.subject, msg.subject);
@ -177,9 +182,9 @@ test('very large text data message', async (t) => {
t.is(mail.to?.text, msg.to);
});
test('html data message', async (t) => {
test('text + html + data message', async (t) => {
const msg = {
subject: 'this is a test TEXT+HTML+DATA message from emailjs',
subject: t.title,
from: 'obama@gmail.com',
to: 'mitt@gmail.com',
attachment: {
@ -188,7 +193,7 @@ test('html data message', async (t) => {
},
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.html, htmlFixture.replace(/\r/g, ''));
t.is(mail.text, '\n');
t.is(mail.subject, msg.subject);
@ -198,7 +203,7 @@ test('html data message', async (t) => {
test('html file message', async (t) => {
const msg = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
subject: t.title,
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: {
@ -207,7 +212,7 @@ test('html file message', async (t) => {
},
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.html, htmlFixture.replace(/\r/g, ''));
t.is(mail.text, '\n');
t.is(mail.subject, msg.subject);
@ -215,11 +220,11 @@ test('html file message', async (t) => {
t.is(mail.to?.text, msg.to);
});
test('html with image embed message', async (t) => {
test('html + image embed message', async (t) => {
const htmlFixture2Url = new URL('attachments/smtp2.html', import.meta.url);
const imageFixtureUrl = new URL('attachments/smtp.gif', import.meta.url);
const msg = {
subject: 'this is a test TEXT+HTML+IMAGE message from emailjs',
subject: t.title,
from: 'ninja@gmail.com',
to: 'pirate@gmail.com',
attachment: {
@ -236,7 +241,7 @@ test('html with image embed message', async (t) => {
},
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(
mail.attachments[0].content.toString('base64'),
readFileSync(imageFixtureUrl, 'base64')
@ -248,9 +253,9 @@ test('html with image embed message', async (t) => {
t.is(mail.to?.text, msg.to);
});
test('html data and attachment message', async (t) => {
test('html + data + two attachments message', async (t) => {
const msg = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
subject: t.title,
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: [
@ -262,7 +267,7 @@ test('html data and attachment message', async (t) => {
] as MessageAttachment[],
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.html, htmlFixture.replace(/\r/g, ''));
t.is(mail.text, '\n');
t.is(mail.subject, msg.subject);
@ -270,9 +275,9 @@ test('html data and attachment message', async (t) => {
t.is(mail.to?.text, msg.to);
});
test('attachment message', async (t) => {
test('text + attachment message', async (t) => {
const msg = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
subject: t.title,
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
@ -283,7 +288,7 @@ test('attachment message', async (t) => {
} as MessageAttachment,
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.text, msg.text + '\n');
t.is(mail.subject, msg.subject);
@ -291,9 +296,9 @@ test('attachment message', async (t) => {
t.is(mail.to?.text, msg.to);
});
test('attachment sent with unicode filename message', async (t) => {
test('text + attachment + unicode filename message', async (t) => {
const msg = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
subject: t.title,
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
@ -304,7 +309,7 @@ test('attachment sent with unicode filename message', async (t) => {
} as MessageAttachment,
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.attachments[0].filename, 'smtp-✓-info.pdf');
t.is(mail.text, msg.text + '\n');
@ -313,9 +318,9 @@ test('attachment sent with unicode filename message', async (t) => {
t.is(mail.to?.text, msg.to);
});
test('attachments message', async (t) => {
test('text + two attachments message', async (t) => {
const msg = {
subject: 'this is a test TEXT+2+ATTACHMENTS message from emailjs',
subject: t.title,
from: 'sergey@gmail.com',
to: 'jobs@gmail.com',
text: 'hello friend, i hope this message and attachments finds you well.',
@ -333,7 +338,7 @@ test('attachments message', async (t) => {
] as MessageAttachment[],
};
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.attachments[1].content.toString('base64'), tarFixture);
t.is(mail.text, msg.text + '\n');
@ -342,9 +347,9 @@ test('attachments message', async (t) => {
t.is(mail.to?.text, msg.to);
});
test('streams message', async (t) => {
test('text + two attachments message (streams)', async (t) => {
const msg = {
subject: 'this is a test TEXT+2+STREAMED+ATTACHMENTS message from emailjs',
subject: t.title,
from: 'stanford@gmail.com',
to: 'mit@gmail.com',
text: 'hello friend, i hope this message and streamed attachments finds you well.',
@ -366,7 +371,7 @@ test('streams message', async (t) => {
stream.pause();
}
const mail = await send(msg);
const mail = await send(t, msg);
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.attachments[1].content.toString('base64'), tarFixture);
t.is(mail.text, msg.text + '\n');

106
test/queue.ts Normal file
View File

@ -0,0 +1,106 @@
import { performance } from 'perf_hooks';
import test from 'ava';
import { SMTPServer } from 'smtp-server';
import { SMTPClient, Message } from '../email.js';
const port = 7777;
test('synchronous queue failures are handled gracefully by client', async (t) => {
const tlsClient = new SMTPClient({ port, timeout: 200, tls: true });
const secureServer = new SMTPServer({ secure: true });
let attemptCount = 0;
let failureCount = 0;
const mailQueue: (() => Promise<void>)[] = [];
function* mailQueueGenerator() {
while (mailQueue.length > 0) {
yield mailQueue.shift();
}
}
await t.throwsAsync(
new Promise<void>((resolve, reject) => {
secureServer
.on('error', () => {
/** intentionally swallow errors */
})
.listen(port, async () => {
const mailTask = async () => {
try {
await tlsClient.sendAsync(
new Message({
from: 'piglet@gmail.com',
to: 'pooh@gmail.com',
subject: 'this is a test TEXT message from emailjs',
text: 'hello friend, i hope this message finds you well.',
})
);
resolve();
} catch (err) {
if (attemptCount < 5) {
void mailQueue.push(mailTask);
} else {
reject(err);
}
throw err;
}
};
void mailQueue.push(mailTask);
for (const task of mailQueueGenerator()) {
const now = performance.now();
const initialAttemptCount = attemptCount++;
try {
t.log(
`Attempting task #${attemptCount}...${
attemptCount > 1
? ` (succeeded: ${
initialAttemptCount - failureCount
} / ${initialAttemptCount})`
: ''
}`
);
await task?.();
t.log(
`Task succeeded (${Math.round(performance.now() - now)}ms).`
);
} catch (err) {
failureCount++;
t.log(
`Task failed: ${err.message} (${Math.round(
performance.now() - now
)}ms)`
);
}
}
t.log(
`Finished after ${attemptCount} attempts (succeeded: ${
attemptCount - failureCount
} / ${attemptCount}).`
);
});
})
);
t.log(
`SMTPClient ${JSON.stringify(
{
// @ts-expect-error need to check protected prop
ready: tlsClient.ready,
// @ts-expect-error need to check protected prop
sending: tlsClient.sending,
state: tlsClient.smtp.state(),
},
null,
'\t'
).replace(/"/g, '')}`
);
// @ts-expect-error need to check protected prop
t.false(tlsClient.ready);
// @ts-expect-error need to check protected prop
t.false(tlsClient.sending);
t.is(tlsClient.smtp.state(), 0);
});

166
yarn.lock
View File

@ -14,19 +14,19 @@
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
"@eslint/eslintrc@^1.2.2":
version "1.2.2"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae"
integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==
"@eslint/eslintrc@^1.2.3":
version "1.2.3"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886"
integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.3.1"
espree "^9.3.2"
globals "^13.9.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.0.4"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@humanwhocodes/config-array@^0.9.2":
@ -152,14 +152,14 @@
"@types/node" "*"
"@types/nodemailer" "*"
"@typescript-eslint/eslint-plugin@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.21.0.tgz#bfc22e0191e6404ab1192973b3b4ea0461c1e878"
integrity sha512-fTU85q8v5ZLpoZEyn/u1S2qrFOhi33Edo2CZ0+q1gDaWWm0JuPh3bgOyU8lM0edIEYgKLDkPFiZX2MOupgjlyg==
"@typescript-eslint/eslint-plugin@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.22.0.tgz#7b52a0de2e664044f28b36419210aea4ab619e2a"
integrity sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg==
dependencies:
"@typescript-eslint/scope-manager" "5.21.0"
"@typescript-eslint/type-utils" "5.21.0"
"@typescript-eslint/utils" "5.21.0"
"@typescript-eslint/scope-manager" "5.22.0"
"@typescript-eslint/type-utils" "5.22.0"
"@typescript-eslint/utils" "5.22.0"
debug "^4.3.2"
functional-red-black-tree "^1.0.1"
ignore "^5.1.8"
@ -167,72 +167,72 @@
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/parser@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.21.0.tgz#6cb72673dbf3e1905b9c432175a3c86cdaf2071f"
integrity sha512-8RUwTO77hstXUr3pZoWZbRQUxXcSXafZ8/5gpnQCfXvgmP9gpNlRGlWzvfbEQ14TLjmtU8eGnONkff8U2ui2Eg==
"@typescript-eslint/parser@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz#7bedf8784ef0d5d60567c5ba4ce162460e70c178"
integrity sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==
dependencies:
"@typescript-eslint/scope-manager" "5.21.0"
"@typescript-eslint/types" "5.21.0"
"@typescript-eslint/typescript-estree" "5.21.0"
"@typescript-eslint/scope-manager" "5.22.0"
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/typescript-estree" "5.22.0"
debug "^4.3.2"
"@typescript-eslint/scope-manager@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.21.0.tgz#a4b7ed1618f09f95e3d17d1c0ff7a341dac7862e"
integrity sha512-XTX0g0IhvzcH/e3393SvjRCfYQxgxtYzL3UREteUneo72EFlt7UNoiYnikUtmGVobTbhUDByhJ4xRBNe+34kOQ==
"@typescript-eslint/scope-manager@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz#590865f244ebe6e46dc3e9cab7976fc2afa8af24"
integrity sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==
dependencies:
"@typescript-eslint/types" "5.21.0"
"@typescript-eslint/visitor-keys" "5.21.0"
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/visitor-keys" "5.22.0"
"@typescript-eslint/type-utils@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.21.0.tgz#ff89668786ad596d904c21b215e5285da1b6262e"
integrity sha512-MxmLZj0tkGlkcZCSE17ORaHl8Th3JQwBzyXL/uvC6sNmu128LsgjTX0NIzy+wdH2J7Pd02GN8FaoudJntFvSOw==
"@typescript-eslint/type-utils@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.22.0.tgz#0c0e93b34210e334fbe1bcb7250c470f4a537c19"
integrity sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA==
dependencies:
"@typescript-eslint/utils" "5.21.0"
"@typescript-eslint/utils" "5.22.0"
debug "^4.3.2"
tsutils "^3.21.0"
"@typescript-eslint/types@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.21.0.tgz#8cdb9253c0dfce3f2ab655b9d36c03f72e684017"
integrity sha512-XnOOo5Wc2cBlq8Lh5WNvAgHzpjnEzxn4CJBwGkcau7b/tZ556qrWXQz4DJyChYg8JZAD06kczrdgFPpEQZfDsA==
"@typescript-eslint/types@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz#50a4266e457a5d4c4b87ac31903b28b06b2c3ed0"
integrity sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==
"@typescript-eslint/typescript-estree@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.21.0.tgz#9f0c233e28be2540eaed3df050f0d54fb5aa52de"
integrity sha512-Y8Y2T2FNvm08qlcoSMoNchh9y2Uj3QmjtwNMdRQkcFG7Muz//wfJBGBxh8R7HAGQFpgYpdHqUpEoPQk+q9Kjfg==
"@typescript-eslint/typescript-estree@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz#e2116fd644c3e2fda7f4395158cddd38c0c6df97"
integrity sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==
dependencies:
"@typescript-eslint/types" "5.21.0"
"@typescript-eslint/visitor-keys" "5.21.0"
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/visitor-keys" "5.22.0"
debug "^4.3.2"
globby "^11.0.4"
is-glob "^4.0.3"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.21.0.tgz#51d7886a6f0575e23706e5548c7e87bce42d7c18"
integrity sha512-q/emogbND9wry7zxy7VYri+7ydawo2HDZhRZ5k6yggIvXa7PvBbAAZ4PFH/oZLem72ezC4Pr63rJvDK/sTlL8Q==
"@typescript-eslint/utils@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.22.0.tgz#1f2c4897e2cf7e44443c848a13c60407861babd8"
integrity sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ==
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.21.0"
"@typescript-eslint/types" "5.21.0"
"@typescript-eslint/typescript-estree" "5.21.0"
"@typescript-eslint/scope-manager" "5.22.0"
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/typescript-estree" "5.22.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/visitor-keys@5.21.0":
version "5.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.21.0.tgz#453fb3662409abaf2f8b1f65d515699c888dd8ae"
integrity sha512-SX8jNN+iHqAF0riZQMkm7e8+POXa/fXw5cxL+gjpyP+FI+JVNhii53EmQgDAfDcBpFekYSlO0fGytMQwRiMQCA==
"@typescript-eslint/visitor-keys@5.22.0":
version "5.22.0"
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz#f49c0ce406944ffa331a1cfabeed451ea4d0909c"
integrity sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==
dependencies:
"@typescript-eslint/types" "5.21.0"
"@typescript-eslint/types" "5.22.0"
eslint-visitor-keys "^3.0.0"
acorn-jsx@^5.3.1:
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
@ -242,7 +242,7 @@ acorn-walk@^8.1.1, acorn-walk@^8.2.0:
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.4.1, acorn@^8.7.0:
acorn@^8.4.1, acorn@^8.7.0, acorn@^8.7.1:
version "8.7.1"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
@ -492,9 +492,9 @@ clean-stack@^2.0.0:
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
clean-stack@^4.0.0:
version "4.1.0"
resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-4.1.0.tgz#5ce5a2fd19a12aecdce8570daefddb7ac94b6b4e"
integrity sha512-dxXQYI7mfQVcaF12s6sjNFoZ6ZPDQuBBLp3QJ5156k9EvUFClUoZ11fo8HnLQO241DDVntHEug8MOuFO5PSfRg==
version "4.2.0"
resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz#c464e4cde4ac789f4e0735c5d75beb49d7b30b31"
integrity sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==
dependencies:
escape-string-regexp "5.0.0"
@ -781,12 +781,12 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@8.14.0:
version "8.14.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz#62741f159d9eb4a79695b28ec4989fcdec623239"
integrity sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==
eslint@8.15.0:
version "8.15.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz#fea1d55a7062da48d82600d2e0974c55612a11e9"
integrity sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==
dependencies:
"@eslint/eslintrc" "^1.2.2"
"@eslint/eslintrc" "^1.2.3"
"@humanwhocodes/config-array" "^0.9.2"
ajv "^6.10.0"
chalk "^4.0.0"
@ -797,7 +797,7 @@ eslint@8.14.0:
eslint-scope "^7.1.1"
eslint-utils "^3.0.0"
eslint-visitor-keys "^3.3.0"
espree "^9.3.1"
espree "^9.3.2"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@ -813,7 +813,7 @@ eslint@8.14.0:
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.0.4"
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.1"
regexpp "^3.2.0"
@ -822,13 +822,13 @@ eslint@8.14.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^9.3.1:
version "9.3.1"
resolved "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd"
integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==
espree@^9.3.2:
version "9.3.2"
resolved "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596"
integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==
dependencies:
acorn "^8.7.0"
acorn-jsx "^5.3.1"
acorn "^8.7.1"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.3.0"
esprima@^4.0.0:
@ -1084,9 +1084,9 @@ iconv-lite@0.6.3, iconv-lite@^0.6.3:
safer-buffer ">= 2.1.2 < 3.0.0"
ignore-by-default@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.0.0.tgz#537092018540640459569fe7c8c7a408af581146"
integrity sha512-+mQSgMRiFD3L3AOxLYOCxjIq4OnAmo5CIuC+lj5ehCJcPtV++QacEV7FdpzvYxH6DaOySWzQU6RR0lPLy37ckA==
version "2.1.0"
resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.1.0.tgz#c0e0de1a99b6065bdc93315a6f728867981464db"
integrity sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==
ignore@^5.1.8, ignore@^5.2.0:
version "5.2.0"
@ -1385,7 +1385,7 @@ mimic-fn@^4.0.0:
resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
minimatch@^3.0.4:
minimatch@^3.0.4, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@ -1427,10 +1427,10 @@ nearley@^2.20.1:
railroad-diagrams "^1.0.0"
randexp "0.4.6"
nodemailer@6.7.3, nodemailer@6.7.4:
version "6.7.4"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.4.tgz#28771bda3dda8f2dad1912aca0f8727ce7f09d89"
integrity sha512-TBSS3qS8WG45ycUwEvEA/3UM1o3sLz9jUl4TPUKPz4ImWWM6UgRCb5pLO+HOouDKEj57yNLOrzQlO8+9IjWZoA==
nodemailer@6.7.3, nodemailer@6.7.5:
version "6.7.5"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.5.tgz#b30b1566f5fa2249f7bd49ced4c58bec6b25915e"
integrity sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg==
nofilter@^3.1.0:
version "3.1.0"
@ -1678,10 +1678,10 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@2.70.2:
version "2.70.2"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.70.2.tgz#808d206a8851628a065097b7ba2053bd83ba0c0d"
integrity sha512-EitogNZnfku65I1DD5Mxe8JYRUCy0hkK5X84IlDtUs+O6JRMpRciXTzyCUuX11b5L5pvjH+OmFXiQ3XjabcXgg==
rollup@2.72.0:
version "2.72.0"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.72.0.tgz#f94280b003bcf9f2f1f2594059a9db5abced371e"
integrity sha512-KqtR2YcO35/KKijg4nx4STO3569aqCUeGRkKWnJ6r+AvBBrVY9L4pmf4NHVrQr4mTOq6msbohflxr2kpihhaOA==
optionalDependencies:
fsevents "~2.3.2"