mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
feat: added basic email-service (#780)
* feat: added basic email-service * feat: preview endpoint for plaintext/html templates * chore: Updated docs fixes: #757
This commit is contained in:
parent
5d41e63077
commit
99fd210392
@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 4.0.0-alpha.2
|
||||
|
||||
- feat: Email service (#757)
|
||||
|
||||
## 4.0.0-alpha.1
|
||||
|
||||
- chore: upgrade frontend 4.0.0-alpha.1
|
||||
|
@ -64,6 +64,14 @@ unleash.start(unleashOptions);
|
||||
- **checkVersion** - the checkVersion object deciding where to check for latest version
|
||||
- `url` - The url to check version (Defaults to `https://version.unleash.run`) - Overridable with (`UNLEASH_VERSION_URL`)
|
||||
- `enable` - Whether version checking is enabled (defaults to true) - Overridable with (`CHECK_VERSION`) (if anything other than `true`, does not check)
|
||||
- **email** - the email object configuring an SMTP server for sending welcome mails and password reset mails
|
||||
- `host` - The server URL to your SMTP server
|
||||
- `port` - Which port the SMTP server is running on. Defaults to 465 (Secure SMTP)
|
||||
- `secure` (boolean) - Whether to use SMTPS or not.
|
||||
- `sender` - Which email should be set as sender of mails being sent from Unleash?
|
||||
- **auth** - For now a user/pass object containing auth details for your SMTP server
|
||||
- `user` - Username for your SMTP server
|
||||
- `pass` - Password for your SMTP server
|
||||
|
||||
### Disabling Auto-Start
|
||||
|
||||
|
10
package.json
10
package.json
@ -31,16 +31,18 @@
|
||||
"start": "node ./dist/server.js",
|
||||
"start:google": "node examples/google-auth-unleash.js",
|
||||
"start:dev": "NODE_ENV=development tsc-watch --onSuccess \"node dist/server-dev.js\"",
|
||||
"copy-templates": "copyfiles -u 1 src/mailtemplates/**/*.mustache dist/",
|
||||
"db-migrate": "db-migrate --migrations-dir ./src/migrations",
|
||||
"lint": "eslint ./src",
|
||||
"build:watch": "tsc -w",
|
||||
"build": "tsc",
|
||||
"build": "yarn run copy-templates && tsc --pretty",
|
||||
"prepare": "yarn run build",
|
||||
"test": "yarn build && NODE_ENV=test PORT=4243 ava",
|
||||
"test:docker": "./scripts/docker-postgres.sh",
|
||||
"test:watch": "yarn test --watch",
|
||||
"test:coverage": "nyc --reporter=lcov yarn test",
|
||||
"test:coverage-report": "nyc report --reporter=text-lcov | coveralls"
|
||||
"test:coverage-report": "nyc report --reporter=text-lcov | coveralls",
|
||||
"clean": "rimraf dist/"
|
||||
},
|
||||
"nyc": {
|
||||
"all": true,
|
||||
@ -89,6 +91,7 @@
|
||||
"multer": "^1.4.1",
|
||||
"mustache": "^4.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemailer": "^6.5.0",
|
||||
"parse-database-url": "^0.3.0",
|
||||
"pg": "^8.0.3",
|
||||
"pkginfo": "^0.4.1",
|
||||
@ -105,9 +108,11 @@
|
||||
"@passport-next/passport-google-oauth2": "^1.0.0",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/node": "^14.0.0",
|
||||
"@types/nodemailer": "^6.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.2",
|
||||
"@typescript-eslint/parser": "^4.15.2",
|
||||
"ava": "^3.7.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"coveralls": "^3.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb-base": "^14.1.0",
|
||||
@ -125,6 +130,7 @@
|
||||
"passport-google-auth": "^1.0.2",
|
||||
"prettier": "^1.19.1",
|
||||
"proxyquire": "^2.1.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"sinon": "^9.2.4",
|
||||
"source-map-support": "^0.5.19",
|
||||
"superagent": "^6.1.0",
|
||||
|
@ -20,7 +20,7 @@ const mapRow = row => ({
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
description: row.description,
|
||||
strategies: row.strategies,
|
||||
strategies: row.strategies || [],
|
||||
createdBy: row.created_by,
|
||||
url: row.url,
|
||||
color: row.color,
|
||||
|
@ -102,6 +102,16 @@ function defaultOptions() {
|
||||
},
|
||||
rbac: false,
|
||||
},
|
||||
email: {
|
||||
host: process.env.EMAIL_HOST,
|
||||
secure: !!process.env.EMAIL_SECURE,
|
||||
port: process.env.EMAIL_PORT || 465,
|
||||
sender: process.env.EMAIL_SENDER || 'noreply@unleash-hosted.com',
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
52
src/lib/routes/admin-api/email.js
Normal file
52
src/lib/routes/admin-api/email.js
Normal file
@ -0,0 +1,52 @@
|
||||
import { TemplateFormat } from '../../services/email-service';
|
||||
import { handleErrors } from './util';
|
||||
import { ADMIN } from '../../permissions';
|
||||
|
||||
const Controller = require('../controller');
|
||||
|
||||
class EmailController extends Controller {
|
||||
constructor(config, { emailService }) {
|
||||
super(config);
|
||||
this.emailService = emailService;
|
||||
this.logger = config.getLogger('routes/admin-api/email');
|
||||
this.get('/preview/html/:template', this.getHtmlPreview, ADMIN);
|
||||
this.get('/preview/text/:template', this.getTextPreview, ADMIN);
|
||||
}
|
||||
|
||||
async getHtmlPreview(req, res) {
|
||||
try {
|
||||
const { template } = req.params;
|
||||
const ctx = req.query;
|
||||
const data = await this.emailService.compileTemplate(
|
||||
template,
|
||||
TemplateFormat.HTML,
|
||||
ctx,
|
||||
);
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.status(200);
|
||||
res.send(data);
|
||||
res.end();
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
}
|
||||
|
||||
async getTextPreview(req, res) {
|
||||
try {
|
||||
const { template } = req.params;
|
||||
const ctx = req.query;
|
||||
const data = await this.emailService.compileTemplate(
|
||||
template,
|
||||
TemplateFormat.PLAIN,
|
||||
ctx,
|
||||
);
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.status(200);
|
||||
res.send(data);
|
||||
res.end();
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = EmailController;
|
64
src/lib/routes/admin-api/email.test.js
Normal file
64
src/lib/routes/admin-api/email.test.js
Normal file
@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const supertest = require('supertest');
|
||||
const { EventEmitter } = require('events');
|
||||
const store = require('../../../test/fixtures/store');
|
||||
const { createServices } = require('../../services');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const config = {
|
||||
baseUriPath: base,
|
||||
stores,
|
||||
eventBus,
|
||||
extendedPermissions: false,
|
||||
customContextFields: [{ name: 'tenantId' }],
|
||||
getLogger,
|
||||
};
|
||||
|
||||
const services = createServices(stores, config);
|
||||
const app = getApp(config, services);
|
||||
|
||||
return {
|
||||
base,
|
||||
request: supertest(app),
|
||||
};
|
||||
}
|
||||
|
||||
test('should render html preview of template', t => {
|
||||
t.plan(0);
|
||||
const { request, base } = getSetup();
|
||||
return request
|
||||
.get(
|
||||
`${base}/api/admin/email/preview/html/reset-password?name=Test%20Test`,
|
||||
)
|
||||
.expect('Content-Type', /html/)
|
||||
.expect(200)
|
||||
.expect(res => 'Test Test' in res.body);
|
||||
});
|
||||
|
||||
test('should render text preview of template', t => {
|
||||
t.plan(0);
|
||||
const { request, base } = getSetup();
|
||||
return request
|
||||
.get(
|
||||
`${base}/api/admin/email/preview/text/reset-password?name=Test%20Test`,
|
||||
)
|
||||
.expect('Content-Type', /plain/)
|
||||
.expect(200)
|
||||
.expect(res => 'Test Test' in res.body);
|
||||
});
|
||||
|
||||
test('Requesting a non-existing template should yield 404', t => {
|
||||
t.plan(0);
|
||||
const { request, base } = getSetup();
|
||||
return request
|
||||
.get(`${base}/api/admin/email/preview/text/some-non-existing-template`)
|
||||
.expect(404);
|
||||
});
|
@ -15,6 +15,7 @@ const TagController = require('./tag');
|
||||
const TagTypeController = require('./tag-type');
|
||||
const AddonController = require('./addon');
|
||||
const ApiTokenController = require('./api-token-controller');
|
||||
const EmailController = require('./email');
|
||||
const apiDef = require('./api-def.json');
|
||||
|
||||
class AdminApi extends Controller {
|
||||
@ -63,6 +64,7 @@ class AdminApi extends Controller {
|
||||
'/api-tokens',
|
||||
new ApiTokenController(config, services).router,
|
||||
);
|
||||
this.app.use('/email', new EmailController(config, services).router);
|
||||
}
|
||||
|
||||
index(req, res) {
|
||||
|
60
src/lib/services/email-service.test.ts
Normal file
60
src/lib/services/email-service.test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import test from 'ava';
|
||||
import { EmailService, TransporterType } from './email-service';
|
||||
import noLoggerProvider from '../../test/fixtures/no-logger';
|
||||
|
||||
test('Can send reset email', async t => {
|
||||
const emailService = new EmailService(
|
||||
{
|
||||
host: '',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: '',
|
||||
password: '',
|
||||
},
|
||||
sender: 'noreply@getunleash.ai',
|
||||
transporterType: TransporterType.JSON,
|
||||
},
|
||||
noLoggerProvider,
|
||||
);
|
||||
const content = await emailService.sendResetMail(
|
||||
'Some username',
|
||||
'test@test.com',
|
||||
'abc123',
|
||||
);
|
||||
const message = JSON.parse(content.message);
|
||||
t.is(message.from.address, 'noreply@getunleash.ai');
|
||||
t.is(message.subject, 'Someone has requested to reset your password');
|
||||
t.true(message.html.indexOf('Some username') > 0);
|
||||
t.true(message.text.indexOf('Some username') > 0);
|
||||
t.true(message.html.indexOf('abc123') > 0);
|
||||
t.true(message.text.indexOf('abc123') > 0);
|
||||
});
|
||||
|
||||
test('Can send welcome mail', async t => {
|
||||
const emailService = new EmailService(
|
||||
{
|
||||
host: '',
|
||||
port: 9999,
|
||||
secure: false,
|
||||
sender: 'noreply@getunleash.ai',
|
||||
auth: {
|
||||
user: '',
|
||||
password: '',
|
||||
},
|
||||
transporterType: TransporterType.JSON,
|
||||
},
|
||||
noLoggerProvider,
|
||||
);
|
||||
const content = await emailService.sendGettingStartedMail(
|
||||
'Some username',
|
||||
'test@test.com',
|
||||
'abc123456',
|
||||
);
|
||||
const message = JSON.parse(content.message);
|
||||
t.is(message.from.address, 'noreply@getunleash.ai');
|
||||
t.is(
|
||||
message.subject,
|
||||
'Welcome to Unleash. Please configure your password.',
|
||||
);
|
||||
});
|
173
src/lib/services/email-service.ts
Normal file
173
src/lib/services/email-service.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { createTransport, SentMessageInfo, Transporter } from 'nodemailer';
|
||||
import Mustache from 'mustache';
|
||||
import path from 'path';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
|
||||
export interface IAuthOptions {
|
||||
user: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export enum TemplateFormat {
|
||||
HTML = 'html',
|
||||
PLAIN = 'plain',
|
||||
}
|
||||
|
||||
export enum TransporterType {
|
||||
SMTP = 'smtp',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
export interface IEmailOptions {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
sender: string;
|
||||
auth: IAuthOptions;
|
||||
transporterType: TransporterType;
|
||||
}
|
||||
|
||||
const RESET_MAIL_SUBJECT = 'Someone has requested to reset your password';
|
||||
const GETTING_STARTED_SUBJECT =
|
||||
'Welcome to Unleash. Please configure your password.';
|
||||
|
||||
export class EmailService {
|
||||
private logger: Logger;
|
||||
|
||||
private mailer?: Transporter;
|
||||
|
||||
private readonly sender: string;
|
||||
|
||||
constructor(email: IEmailOptions | undefined, getLogger: LogProvider) {
|
||||
this.logger = getLogger('services/email-service.ts');
|
||||
if (email) {
|
||||
this.sender = email.sender;
|
||||
if (email.transporterType === TransporterType.JSON) {
|
||||
this.mailer = createTransport({ jsonTransport: true });
|
||||
} else {
|
||||
const connectionString = `${email.auth.user}:${email.auth.password}@${email.host}:${email.port}`;
|
||||
this.mailer = email.secure
|
||||
? createTransport(`smtps://${connectionString}`)
|
||||
: createTransport(`smtp://${connectionString}`);
|
||||
}
|
||||
this.logger.info(
|
||||
`Initialized transport to ${email.host} on port ${email.port} with user: ${email.auth.user}`,
|
||||
);
|
||||
} else {
|
||||
this.sender = 'not-configured';
|
||||
this.mailer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async sendResetMail(
|
||||
name: string,
|
||||
recipient: string,
|
||||
resetLink: string,
|
||||
): Promise<SentMessageInfo> {
|
||||
if (this.mailer !== undefined) {
|
||||
const year = new Date().getFullYear();
|
||||
const bodyHtml = await this.compileTemplate(
|
||||
'reset-password',
|
||||
TemplateFormat.HTML,
|
||||
{
|
||||
resetLink,
|
||||
name,
|
||||
year,
|
||||
},
|
||||
);
|
||||
const bodyText = await this.compileTemplate(
|
||||
'reset-password',
|
||||
TemplateFormat.PLAIN,
|
||||
{
|
||||
resetLink,
|
||||
name,
|
||||
year,
|
||||
},
|
||||
);
|
||||
const email = {
|
||||
from: this.sender,
|
||||
to: recipient,
|
||||
subject: RESET_MAIL_SUBJECT,
|
||||
html: bodyHtml,
|
||||
text: bodyText,
|
||||
};
|
||||
return this.mailer.sendMail(email);
|
||||
}
|
||||
return new Promise(res => {
|
||||
this.logger.warn(
|
||||
'No mailer is configured. Please read the docs on how to configure an emailservice',
|
||||
);
|
||||
res({});
|
||||
});
|
||||
}
|
||||
|
||||
async sendGettingStartedMail(
|
||||
name: string,
|
||||
recipient: string,
|
||||
passwordLink: string,
|
||||
): Promise<SentMessageInfo> {
|
||||
if (this.mailer !== undefined) {
|
||||
const year = new Date().getFullYear();
|
||||
const context = { passwordLink, name, year };
|
||||
const bodyHtml = await this.compileTemplate(
|
||||
'getting-started',
|
||||
TemplateFormat.HTML,
|
||||
context,
|
||||
);
|
||||
const bodyText = await this.compileTemplate(
|
||||
'getting-started',
|
||||
TemplateFormat.PLAIN,
|
||||
context,
|
||||
);
|
||||
const email = {
|
||||
from: this.sender,
|
||||
to: recipient,
|
||||
subject: GETTING_STARTED_SUBJECT,
|
||||
html: bodyHtml,
|
||||
text: bodyText,
|
||||
};
|
||||
return this.mailer.sendMail(email);
|
||||
}
|
||||
return new Promise(res => {
|
||||
this.logger.warn(
|
||||
'No mailer is configured. Please read the docs on how to configure an EmailService',
|
||||
);
|
||||
res({});
|
||||
});
|
||||
}
|
||||
|
||||
private async compileTemplate(
|
||||
templateName: string,
|
||||
format: TemplateFormat,
|
||||
context: any,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const template = this.resolveTemplate(templateName, format);
|
||||
return await Promise.resolve(Mustache.render(template, context));
|
||||
} catch (e) {
|
||||
this.logger.info(`Could not find template ${templateName}`);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveTemplate(
|
||||
templateName: string,
|
||||
format: TemplateFormat,
|
||||
): string {
|
||||
let topPath = path.resolve('mailtemplates');
|
||||
if (!existsSync(topPath)) {
|
||||
topPath = path.resolve('dist', 'mailtemplates');
|
||||
}
|
||||
const template = path.join(
|
||||
topPath,
|
||||
templateName,
|
||||
`${templateName}.${format}.mustache`,
|
||||
);
|
||||
if (existsSync(template)) {
|
||||
return readFileSync(template, 'utf-8');
|
||||
}
|
||||
throw new NotFoundError('Could not find template');
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ const StrategyService = require('./strategy-service');
|
||||
const AddonService = require('./addon-service');
|
||||
const ContextService = require('./context-service');
|
||||
const VersionService = require('./version-service');
|
||||
const { EmailService } = require('./email-service');
|
||||
const { AccessService } = require('./access-service');
|
||||
const { ApiTokenService } = require('./api-token-service');
|
||||
|
||||
@ -28,6 +29,7 @@ module.exports.createServices = (stores, config) => {
|
||||
const contextService = new ContextService(stores, config);
|
||||
const versionService = new VersionService(stores, config);
|
||||
const apiTokenService = new ApiTokenService(stores, config);
|
||||
const emailService = new EmailService(config.email, config.getLogger);
|
||||
|
||||
return {
|
||||
accessService,
|
||||
@ -42,5 +44,6 @@ module.exports.createServices = (stores, config) => {
|
||||
contextService,
|
||||
versionService,
|
||||
apiTokenService,
|
||||
emailService,
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Welcome to Unleash - {{ name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
Welcome to Unleash
|
||||
</header>
|
||||
<section>
|
||||
First step: Setup your password by visiting <a href="{{ passwordLink }}" title="Set Password link">{{ passwordLink }}</a>
|
||||
Second step: Visit your instance at {{ unleashUrl }}
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
© {{ year }} - Unleash
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Reset your password {{ name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
||||
</header>
|
||||
<section>
|
||||
Someone has requested a reset of your password. If this was you, great, click here <a href="{{ resetLink }}"
|
||||
title="Password reset link">{{resetLink}}</a>
|
||||
If this was not you, you might want to check your password still works
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
© {{ year }} - Unleash
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,4 @@
|
||||
Hello {{ name }}
|
||||
Someone has requested a reset of your password.
|
||||
If this was you, great, visit "{{ resetLink }}" to reset your password.
|
||||
If this was not you, you might want to check your password still works.
|
108
yarn.lock
108
yarn.lock
@ -575,6 +575,13 @@
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz"
|
||||
integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==
|
||||
|
||||
"@types/nodemailer@^6.4.1":
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.1.tgz#31f96f4410632f781d3613bd1f4293649e423f75"
|
||||
integrity sha512-8081UY/0XTTDpuGqCnDc8IY+Q3DSg604wB3dBH0CaZlj4nZWHWuxtZ3NRZ9c9WUrz1Vfm6wioAUnqL3bsh49uQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
|
||||
@ -1359,6 +1366,15 @@ cliui@^7.0.0:
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clone-response@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz"
|
||||
@ -1621,6 +1637,19 @@ copy-descriptor@^0.1.0:
|
||||
resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz"
|
||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copyfiles@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5"
|
||||
integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==
|
||||
dependencies:
|
||||
glob "^7.0.5"
|
||||
minimatch "^3.0.3"
|
||||
mkdirp "^1.0.4"
|
||||
noms "0.0.0"
|
||||
through2 "^2.0.1"
|
||||
untildify "^4.0.0"
|
||||
yargs "^16.1.0"
|
||||
|
||||
core-js@^3.0.0:
|
||||
version "3.8.3"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0"
|
||||
@ -2975,7 +3004,7 @@ glob-to-regexp@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
|
||||
glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
@ -3372,6 +3401,11 @@ inherits@2, inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
inherits@^2.0.1:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz"
|
||||
@ -4374,7 +4408,7 @@ mimic-response@^1.0.0, mimic-response@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz"
|
||||
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
minimatch@^3.0.3, minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
@ -4401,6 +4435,11 @@ mkdirp@0.x.x, mkdirp@^0.5.1, mkdirp@~0.5.0:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
module-not-found-error@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz"
|
||||
@ -4540,6 +4579,19 @@ node-preload@^0.2.1:
|
||||
dependencies:
|
||||
process-on-spawn "^1.0.0"
|
||||
|
||||
nodemailer@^6.5.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.5.0.tgz#d12c28d8d48778918e25f1999d97910231b175d9"
|
||||
integrity sha512-Tm4RPrrIZbnqDKAvX+/4M+zovEReiKlEXWDzG4iwtpL9X34MJY+D5LnQPH/+eghe8DLlAVshHAJZAZWBGhkguw==
|
||||
|
||||
noms@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859"
|
||||
integrity sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=
|
||||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "~1.0.31"
|
||||
|
||||
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz"
|
||||
@ -5386,7 +5438,7 @@ readable-stream@1.1.x:
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@^2.2.2:
|
||||
readable-stream@^2.2.2, readable-stream@~2.3.6:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
@ -5408,6 +5460,16 @@ readable-stream@^3.6.0:
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@~1.0.31:
|
||||
version "1.0.34"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
|
||||
integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readdirp@~3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz"
|
||||
@ -5621,7 +5683,7 @@ rimraf@2.x.x:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^3.0.0:
|
||||
rimraf@^3.0.0, rimraf@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
@ -6271,6 +6333,14 @@ text-table@^0.2.0:
|
||||
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
|
||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||
|
||||
through2@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
|
||||
dependencies:
|
||||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
|
||||
@ -6563,6 +6633,11 @@ unset-value@^1.0.0:
|
||||
has-value "^0.3.1"
|
||||
isobject "^3.0.0"
|
||||
|
||||
untildify@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
|
||||
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
|
||||
|
||||
update-notifier@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz"
|
||||
@ -6802,7 +6877,7 @@ xdg-basedir@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz"
|
||||
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
||||
|
||||
xtend@^4.0.0:
|
||||
xtend@^4.0.0, xtend@~4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
@ -6817,6 +6892,11 @@ y18n@^5.0.1:
|
||||
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.1.tgz"
|
||||
integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.7"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.7.tgz#0c514aba53fc40e2db911aeb8b51566a3374efe7"
|
||||
integrity sha512-oOhslryvNcA1lB9WYr+M6TMyLkLg81Dgmyb48ZDU0lvR+5bmNDTMz7iobM1QXooaLhbbrcHrlNaABhI6Vo6StQ==
|
||||
|
||||
yallist@^3.0.2:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
||||
@ -6840,6 +6920,11 @@ yargs-parser@^20.0.0:
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.0.tgz"
|
||||
integrity sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A==
|
||||
|
||||
yargs-parser@^20.2.2:
|
||||
version "20.2.7"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
|
||||
integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
|
||||
|
||||
yargs@^15.0.2, yargs@^15.3.1, yargs@^15.4.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz"
|
||||
@ -6870,6 +6955,19 @@ yargs@^16.0.3:
|
||||
y18n "^5.0.1"
|
||||
yargs-parser "^20.0.0"
|
||||
|
||||
yargs@^16.1.0:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.0"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
|
Loading…
Reference in New Issue
Block a user