feat(SPG-388) BE Send Mail with Google Mail

pull/31/head
Aswin Ashar Abdullah 2024-07-17 19:10:35 +07:00
parent 05c98dcb14
commit 403a75c835
12 changed files with 1005 additions and 4 deletions

9
env/env.development vendored
View File

@ -22,10 +22,15 @@ CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *" CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *" CRON_EVERY_HOUR="0 * * * *"
EMAIL_HOST="sandbox.smtp.mailtrap.io"
EMAIL_POST=465
EMAIL_USER="developer@eigen.co.id"
EMAIL_TOKEN="bitqkbkzjzfywxqx"
MIDTRANS_URL=https://app.sandbox.midtrans.com MIDTRANS_URL=https://app.sandbox.midtrans.com
MIDTRANS_PRODUCTION=false MIDTRANS_PRODUCTION=false
MIDTRANS_SERVER_KEY=SB-Mid-server-ITmSD6C0nXfIcmgi4TXm6J7i MIDTRANS_SERVER_KEY=
MIDTRANS_CLIENT_KEY=SB-Mid-client-VFaU_cPL6kh2DKir MIDTRANS_CLIENT_KEY=
EXPORT_LIMIT_PARTITION=200 EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/" ASSETS="https://asset.sky.eigen.co.id/"

9
env/env.production vendored
View File

@ -22,10 +22,15 @@ CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *" CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *" CRON_EVERY_HOUR="0 * * * *"
EMAIL_HOST="sandbox.smtp.mailtrap.io"
EMAIL_POST=465
EMAIL_USER=
EMAIL_TOKEN=
MIDTRANS_URL=https://app.midtrans.com MIDTRANS_URL=https://app.midtrans.com
MIDTRANS_PRODUCTION=true MIDTRANS_PRODUCTION=true
MIDTRANS_SERVER_KEY=Mid-server-6lA4Nnmov2BSOcwVq1sLSOpC MIDTRANS_SERVER_KEY=
MIDTRANS_CLIENT_KEY=Mid-client-JiPIkIPd_RGooF8U MIDTRANS_CLIENT_KEY=
EXPORT_LIMIT_PARTITION=200 EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/" ASSETS="https://asset.sky.eigen.co.id/"

View File

@ -46,10 +46,12 @@
"elastic-apm-node": "^4.5.4", "elastic-apm-node": "^4.5.4",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"googleapis": "^140.0.0", "googleapis": "^140.0.0",
"handlebars": "^4.7.8",
"mathjs": "^13.0.2", "mathjs": "^13.0.2",
"midtrans-client": "^1.3.1", "midtrans-client": "^1.3.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"nano": "^10.1.3", "nano": "^10.1.3",
"nodemailer": "^6.9.14",
"pg": "^8.11.5", "pg": "^8.11.5",
"plop": "^4.0.1", "plop": "^4.0.1",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",

View File

@ -68,6 +68,7 @@ import { NewsModule } from './modules/web-information/news/news.module';
import { NewsModel } from './modules/web-information/news/data/models/news.model'; import { NewsModel } from './modules/web-information/news/data/models/news.model';
import { BannerModule } from './modules/web-information/banner/banner.module'; import { BannerModule } from './modules/web-information/banner/banner.module';
import { BannerModel } from './modules/web-information/banner/data/models/banner.model'; import { BannerModel } from './modules/web-information/banner/data/models/banner.model';
import { MailModule } from './modules/configuration/mail/mail.module';
@Module({ @Module({
imports: [ imports: [
@ -122,6 +123,7 @@ import { BannerModel } from './modules/web-information/banner/data/models/banner
CronModule, CronModule,
GoogleCalendarModule, GoogleCalendarModule,
LogModule, LogModule,
MailModule,
MidtransModule, MidtransModule,
SessionModule, SessionModule,
UploadModule, UploadModule,

View File

@ -9,6 +9,7 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
protected result: Entity; protected result: Entity;
protected oldData: Entity; protected oldData: Entity;
protected dataStatus: STATUS; protected dataStatus: STATUS;
protected relations = [];
protected duplicateColumn: string[]; protected duplicateColumn: string[];
abstract get entityTarget(): any; abstract get entityTarget(): any;
@ -22,6 +23,7 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
where: { where: {
id: this.dataId, id: this.dataId,
}, },
relations: this.relations,
}); });
this.oldData = _.cloneDeep(this.data); this.oldData = _.cloneDeep(this.data);

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateTableTransactions1721216510569
implements MigrationInterface
{
name = 'UpdateTableTransactions1721216510569';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" ADD "payment_midtrans_token" character varying`,
);
await queryRunner.query(
`ALTER TABLE "transactions" ADD "payment_midtrans_url" character varying`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" DROP COLUMN "payment_midtrans_url"`,
);
await queryRunner.query(
`ALTER TABLE "transactions" DROP COLUMN "payment_midtrans_token"`,
);
}
}

View File

@ -0,0 +1,413 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Ibunda</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
width: 100%;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn>tbody>tr>td {
padding-bottom: 15px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
ol {
padding: 0 0 0 1em;
}
ol li {
margin: 1em 0;
}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Halo {{customer_name}}</p>
<p>Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran</p>
<br>
<p>
<b>PEMBAYARAN DAPAT MELALUI</b>
<ul>
{{#each payment_methods}}
<li>
<p>
<b>{{issuer_name}}</b><br>
<span>Name: <b>{{account_name}}</b></span><br>
<span>Number: <b>{{account_number}}</b></span>
</p>
</li>
{{/each}}
</ul>
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block powered-by">
Powered by Skyworld
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,404 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Ibunda</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
width: 100%;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn>tbody>tr>td {
padding-bottom: 15px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
ol {
padding: 0 0 0 1em;
}
ol li {
margin: 1em 0;
}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Halo {{customer_name}}</p>
<p>Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran dengan klik button dibawah ini</p>
</td>
</tr>
<tr>
<td align="center"
style="font-family: 'Lato', sans-serif; font-size:22px; color:#e5eaf5; line-height:24px; font-weight: 600;">
<a href="{{payment_midtrans_url}}">Lanjutkan Pembayaran</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block powered-by">
Powered by Skyworld
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,60 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { STATUS } from 'src/core/strings/constants/base.constants';
import { PaymentMethodDataService } from 'src/modules/transaction/payment-method/data/services/payment-method-data.service';
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { TransactionChangeStatusEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event';
import { sendEmail } from '../helpers/send-email.helper';
import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants';
@EventsHandler(TransactionChangeStatusEvent)
export class PaymentTransactionHandler
implements IEventHandler<TransactionChangeStatusEvent>
{
constructor(
private dataService: TransactionDataService,
private paymentService: PaymentMethodDataService,
) {}
async handle(event: TransactionChangeStatusEvent) {
const data_id = event.data.id;
const old_data = event.data.old;
const current_data = event.data.data;
let payments = [];
if (
old_data.status == STATUS.DRAFT &&
current_data.status == STATUS.PENDING &&
current_data.payment_type != TransactionPaymentType.COUNTER
) {
if (current_data.payment_type != TransactionPaymentType.MIDTRANS) {
payments = await this.paymentService.getManyByOptions({
where: {
status: STATUS.ACTIVE,
},
});
}
const transaction = await this.dataService.getOneByOptions({
where: {
id: data_id,
},
relations: ['items'],
});
try {
sendEmail(
[
{
...transaction,
email: transaction.customer_email,
payment_methods: payments,
},
],
'Payment Confirmation',
);
} catch (error) {
console.log(error);
}
}
}
}

View File

@ -0,0 +1,50 @@
import * as nodemailer from 'nodemailer';
import * as handlebars from 'handlebars';
import * as path from 'path';
import * as fs from 'fs';
import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants';
export async function sendEmail(receivers, subject) {
const smtpTransport = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_POST,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_TOKEN,
},
});
let templateName = 'payment-confirmation-bank';
for (const receiver of receivers) {
if (receiver.payment_type == TransactionPaymentType.MIDTRANS)
templateName = 'payment-confirmation-midtrans';
let templatePath = path.resolve(
__dirname,
`../email-template/${templateName}.html`,
);
templatePath = templatePath.replace(/dist/g, 'src');
const templateSource = fs.readFileSync(templatePath, 'utf8');
const template = handlebars.compile(templateSource);
const htmlToSend = template(receiver);
const emailContext = {
from: 'no-reply@eigen.co.id',
to: receiver.email,
subject: subject,
html: htmlToSend,
attachDataUrls: true,
};
await new Promise((f) => setTimeout(f, 2000));
smtpTransport.sendMail(emailContext, function (err, data) {
if (err) {
console.log(`Error occurs on send to ${receiver.email}`);
} else {
console.log(`Email sent to ${receiver.email}`);
}
});
}
}

View File

@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { PaymentMethodModel } from 'src/modules/transaction/payment-method/data/models/payment-method.model';
import { PaymentMethodDataService } from 'src/modules/transaction/payment-method/data/services/payment-method-data.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { PaymentTransactionHandler } from './domain/handlers/payment-transaction.handler';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forFeature(
[PaymentMethodModel, TransactionModel],
CONNECTION_NAME.DEFAULT,
),
CqrsModule,
],
controllers: [],
providers: [
PaymentTransactionHandler,
PaymentMethodDataService,
TransactionDataService,
],
})
export class MailModule {}

View File

@ -5364,6 +5364,11 @@ node-releases@^2.0.14:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
nodemailer@^6.9.14:
version "6.9.14"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75"
integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==
nopt@^5.0.0: nopt@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"