fix(mail.service): use sendgrid api instead of nodemailer for better deliverability

This commit is contained in:
Amruth Pillai 2022-03-08 07:46:06 +01:00
parent e96b090904
commit 9df12194bf
No known key found for this signature in database
GPG Key ID: E3C57DF9B80855AD
11 changed files with 105 additions and 340 deletions

View File

@ -28,8 +28,6 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
/>
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<script src="/__ENV.js" />
</Head>
<ReduxProvider store={store}>

View File

@ -0,0 +1,22 @@
import { NextPage } from 'next';
import NextDocument, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
const Document: NextPage = () => (
<Html>
<Head />
<body>
<Main />
<NextScript />
<script src="/__ENV.js" />
</body>
</Html>
);
Document.getInitialProps = async (ctx: DocumentContext) => {
const initialProps = await NextDocument.getInitialProps(ctx);
return initialProps;
};
export default Document;

View File

@ -188,6 +188,8 @@ importers:
'@nestjs/serve-static': ^2.2.2
'@nestjs/typeorm': ^8.0.3
'@reactive-resume/schema': workspace:*
'@sendgrid/mail': ^7.6.1
'@types/bcrypt': ^5.0.0
'@types/express': ^4.17.13
'@types/multer': ^1.4.7
'@types/node': ^17.0.21
@ -238,6 +240,7 @@ importers:
'@nestjs/schedule': 1.0.2_1ce925e2290a1cea9e3700e8a60baeb5
'@nestjs/serve-static': 2.2.2_31e7036b193d6d3c9cadab18cbb4af84
'@nestjs/typeorm': 8.0.3_a8e966c473b8cac7d0f44522ae3cdd56
'@sendgrid/mail': 7.6.1
'@types/passport': 1.0.7
bcrypt: 5.0.1
cache-manager: 3.6.0
@ -269,6 +272,7 @@ importers:
'@nestjs/cli': 8.2.2
'@nestjs/schematics': 8.0.7_typescript@4.5.5
'@reactive-resume/schema': link:../schema
'@types/bcrypt': 5.0.0
'@types/express': 4.17.13
'@types/multer': 1.4.7
'@types/node': 17.0.21
@ -1885,6 +1889,33 @@ packages:
resolution: {integrity: sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==}
dev: true
/@sendgrid/client/7.6.1:
resolution: {integrity: sha512-q4U5OhcbJjs+lLVv/LhZSc28feiVCFMgvG9aYcRI5X4tKArnrrGDWb5HMITR9vaAtX42TXhyPFjHr1fk/Q1loQ==}
engines: {node: 6.* || 8.* || >=10.*}
dependencies:
'@sendgrid/helpers': 7.6.0
axios: 0.21.4
transitivePeerDependencies:
- debug
dev: false
/@sendgrid/helpers/7.6.0:
resolution: {integrity: sha512-0uWD+HSXLl4Z/X3cN+UMQC20RE7xwAACgppnfjDyvKG0KvJcUgDGz7HDdQkiMUdcVWfmyk6zKSg7XKfKzBjTwA==}
engines: {node: '>= 6.0.0'}
dependencies:
deepmerge: 4.2.2
dev: false
/@sendgrid/mail/7.6.1:
resolution: {integrity: sha512-F+HXpDLIU4PGZyZznOiFLDGJDwLn2qh7/wD5MvwurrldDx5DaGQHrYBKHopceOl15FVuq9ElU9VIxQJF8SMvTg==}
engines: {node: 6.* || 8.* || >=10.*}
dependencies:
'@sendgrid/client': 7.6.1
'@sendgrid/helpers': 7.6.0
transitivePeerDependencies:
- debug
dev: false
/@sideway/address/4.1.3:
resolution: {integrity: sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==}
dependencies:
@ -1919,6 +1950,12 @@ packages:
resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==}
dev: true
/@types/bcrypt/5.0.0:
resolution: {integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==}
dependencies:
'@types/node': 17.0.21
dev: true
/@types/body-parser/1.19.2:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies:
@ -2704,6 +2741,14 @@ packages:
engines: {node: '>=4'}
dev: true
/axios/0.21.4:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
dependencies:
follow-redirects: 1.14.9
transitivePeerDependencies:
- debug
dev: false
/axios/0.26.0:
resolution: {integrity: sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==}
dependencies:
@ -3509,7 +3554,6 @@ packages:
/deepmerge/4.2.2:
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
engines: {node: '>=0.10.0'}
dev: true
/defaults/1.0.3:
resolution: {integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=}

View File

@ -21,6 +21,7 @@
"@nestjs/schedule": "^1.0.2",
"@nestjs/serve-static": "^2.2.2",
"@nestjs/typeorm": "^8.0.3",
"@sendgrid/mail": "^7.6.1",
"@types/passport": "^1.0.7",
"bcrypt": "^5.0.1",
"cache-manager": "^3.6.0",
@ -30,13 +31,11 @@
"csvtojson": "^2.0.10",
"dayjs": "^1.10.8",
"googleapis": "^95.0.0",
"handlebars": "^4.7.7",
"joi": "^17.6.0",
"lodash": "^4.17.21",
"multer": "^1.4.4",
"nanoid": "^3.3.1",
"node-stream-zip": "^1.15.0",
"nodemailer": "^6.7.2",
"passport": "^0.5.2",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
@ -52,6 +51,8 @@
"devDependencies": {
"@nestjs/cli": "^8.2.2",
"@nestjs/schematics": "^8.0.7",
"@reactive-resume/schema": "workspace:*",
"@types/bcrypt": "^5.0.0",
"@types/express": "^4.17.13",
"@types/multer": "^1.4.7",
"@types/node": "^17.0.21",
@ -62,7 +63,6 @@
"ts-node": "^10.7.0",
"tsconfig-paths": "^3.13.0",
"typescript": "<4.6.0",
"webpack": "^5.70.0",
"@reactive-resume/schema": "workspace:*"
"webpack": "^5.70.0"
}
}

View File

@ -1,288 +0,0 @@
<html>
<head>
<meta charset='utf-8' />
<meta http-equiv='x-ua-compatible' content='ie=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<title>Reset Your Password</title>
<style type='text/css'>
@media screen { @font-face { font-family: "Source Sans Pro"; font-style: normal; font-weight: 400; src:
local("Source Sans Pro Regular"), local("SourceSansPro-Regular"),
url(https://fonts.gstatic.com/s/sourcesanspro/v10/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff)
format("woff"); } @font-face { font-family: "Source Sans Pro"; font-style: normal; font-weight: 700; src:
local("Source Sans Pro Bold"), local("SourceSansPro-Bold"),
url(https://fonts.gstatic.com/s/sourcesanspro/v10/toadOcfmlt9b38dHJxOBGFkQc6VGVFSmCnC_l7QZG60.woff)
format("woff"); } } body, table, td, a { -ms-text-size-adjust: 100%; /* 1 */ -webkit-text-size-adjust: 100%; /* 2
*/ } /** * Remove extra space added to tables and cells in Outlook. */ table, td { mso-table-rspace: 0pt;
mso-table-lspace: 0pt; } /** * Better fluid images in Internet Explorer. */ img { -ms-interpolation-mode: bicubic;
} /** * Remove blue links for iOS devices. */ a[x-apple-data-detectors] { font-family: inherit !important;
font-size: inherit !important; font-weight: inherit !important; line-height: inherit !important; color: inherit
!important; text-decoration: none !important; } /** * Fix centering issues in Android 4.4. */ div[style*="margin:
16px 0; "] { margin: 0 !important; } body { width: 100% !important; height: 100% !important; padding: 0
!important; margin: 0 !important; } /** * Collapse table borders to avoid space between cells. */ table {
border-collapse: collapse !important; } a { color: #1a82e2; } img { height: auto; line-height: 100%;
text-decoration: none; border: 0; outline: none; }
</style>
</head>
<body style='background-color: #e9ecef'>
<!-- start preheader -->
<div
class='preheader'
style='
display: none;
max-width: 0;
max-height: 0;
overflow: hidden;
font-size: 1px;
line-height: 1px;
color: #fff;
opacity: 0;
'
>
Inside this email, you will find a unique link to reset your password. Do not share this email with anyone as your
account may become compromised.
</div>
<!-- end preheader -->
<!-- start body -->
<table border='0' cellpadding='0' cellspacing='0' width='100%'>
<!-- start logo -->
<tr>
<td align='center' bgcolor='#e9ecef'>
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border='0' cellpadding='0' cellspacing='0' width='100%' style='max-width: 600px'>
<tr>
<td align='center' valign='top' style='padding: 36px 24px'>
<a href='https://rxresu.me/' target='_blank' style='display: inline-block'>
<img
src='https://i.imgur.com/pc8Ingg.png'
alt='Reactive Resume Logo'
border='0'
width='128'
style='
display: block;
width: 128px;
max-width: 128px;
min-width: 128px;
'
/>
</a>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- end logo -->
<!-- start hero -->
<tr>
<td align='center' bgcolor='#e9ecef'>
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border='0' cellpadding='0' cellspacing='0' width='100%' style='max-width: 600px'>
<tr>
<td
align='left'
bgcolor='#ffffff'
style="
padding: 36px 24px 0;
font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
border-top: 3px solid #d4dadf;
"
>
<h1
style='
margin: 0;
font-size: 32px;
font-weight: 700;
letter-spacing: -1px;
line-height: 48px;
'
>
Hello,
{{name}}!
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- end hero -->
<!-- start copy block -->
<tr>
<td align='center' bgcolor='#e9ecef'>
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border='0' cellpadding='0' cellspacing='0' width='100%' style='max-width: 600px'>
<!-- start copy -->
<tr>
<td
align='left'
bgcolor='#ffffff'
style="
padding: 24px;
font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
"
>
<p style='margin: 0'>
Tap the button below to reset your account password. If you didn't request a new password, you can
safely delete this email.
</p>
</td>
</tr>
<!-- end copy -->
<!-- start button -->
<tr>
<td align='left' bgcolor='#ffffff'>
<table border='0' cellpadding='0' cellspacing='0' width='100%'>
<tr>
<td align='center' bgcolor='#ffffff' style='padding: 12px'>
<table border='0' cellpadding='0' cellspacing='0'>
<tr>
<td align='center' bgcolor='#1a82e2' style='border-radius: 6px'>
<a
href='{{url}}'
target='_blank'
style="
display: inline-block;
padding: 16px 36px;
font-family: 'Source Sans Pro', Helvetica, Arial,
sans-serif;
font-size: 16px;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
"
>Reset Your Password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- end button -->
<!-- start copy -->
<tr>
<td
align='left'
bgcolor='#ffffff'
style="
padding: 24px;
font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
"
>
<p style='margin: 0'>
If that doesn't work, copy and paste the following link in your browser:
</p>
<p style='margin: 0'>
<a href='{{url}}' target='_blank'>{{url}}</a>
</p>
</td>
</tr>
<!-- end copy -->
<!-- start copy -->
<tr>
<td
align='left'
bgcolor='#ffffff'
style="
padding: 24px;
font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
border-bottom: 3px solid #d4dadf;
"
>
<p style='margin: 0'>
Cheers,<br />
<a href='https://www.amruthpillai.com/'>Amruth Pillai</a>
</p>
</td>
</tr>
<!-- end copy -->
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- end copy block -->
<!-- start footer -->
<tr>
<td align='center' bgcolor='#e9ecef' style='padding: 24px'>
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border='0' cellpadding='0' cellspacing='0' width='100%' style='max-width: 600px'>
<!-- start permission -->
<tr>
<td
align='center'
bgcolor='#e9ecef'
style="
padding: 12px 24px;
font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #666;
"
>
<p style='margin: 0'>
You received this email because we received a request for resetting the password for your account. If
you didn't request a password reset you can safely delete this email.
</p>
</td>
</tr>
<!-- end permission -->
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- end footer -->
</table>
<!-- end body -->
</body>
</html>

View File

@ -2,7 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { SchedulerRegistry } from '@nestjs/schedule';
import * as bcrypt from 'bcrypt';
import bcrypt from 'bcrypt';
import { google } from 'googleapis';
import { PostgresErrorCode } from '@/database/errorCodes.enum';

View File

@ -6,7 +6,7 @@ import appConfig from './app.config';
import authConfig from './auth.config';
import databaseConfig from './database.config';
import googleConfig from './google.config';
import mailConfig from './mail.config';
import sendgridConfig from './sendgrid.config';
const validationSchema = Joi.object({
// App
@ -31,22 +31,19 @@ const validationSchema = Joi.object({
JWT_SECRET: Joi.string().required(),
JWT_EXPIRY_TIME: Joi.number().required(),
// Mail
MAIL_HOST: Joi.string().allow(''),
MAIL_PORT: Joi.number().default(465),
MAIL_USERNAME: Joi.string().allow(''),
MAIL_PASSWORD: Joi.string().allow(''),
// Google
GOOGLE_API_KEY: Joi.string().allow(''),
GOOGLE_CLIENT_SECRET: Joi.string().allow(''),
PUBLIC_GOOGLE_CLIENT_ID: Joi.string().allow(''),
// SendGrid
SENDGRID_API_KEY: Joi.string().allow(''),
});
@Module({
imports: [
NestConfigModule.forRoot({
load: [appConfig, authConfig, databaseConfig, googleConfig, mailConfig],
load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig],
validationSchema: validationSchema,
}),
],

View File

@ -1,9 +0,0 @@
import { registerAs } from '@nestjs/config';
export default registerAs('mail', () => ({
host: process.env.MAIL_HOST,
port: parseInt(process.env.MAIL_PORT, 10) || 465,
username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD,
from: process.env.MAIL_FROM,
}));

View File

@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export default registerAs('sendgrid', () => ({
apiKey: process.env.SENDGRID_API_KEY,
forgotPasswordTemplateId: process.env.SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID,
fromName: process.env.SENDGRID_FROM_NAME,
fromEmail: process.env.SENDGRID_FROM_EMAIL,
}));

View File

@ -1,43 +1,36 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { readFileSync } from 'fs';
import { compile } from 'handlebars';
import { createTransport, Transporter } from 'nodemailer';
import { join } from 'path';
import SendGrid from '@sendgrid/mail';
import { User } from '@/users/entities/user.entity';
@Injectable()
export class MailService {
private readonly transporter: Transporter;
constructor(private configService: ConfigService) {
this.transporter = createTransport(
{
host: this.configService.get<string>('mail.host'),
port: this.configService.get<number>('mail.port'),
auth: {
user: this.configService.get<string>('mail.username'),
pass: this.configService.get<string>('mail.password'),
},
},
{
from: this.configService.get<string>('mail.from'),
}
);
SendGrid.setApiKey(this.configService.get<string>('sendgrid.apiKey'));
}
async sendForgotPasswordEmail(user: User, resetToken: string) {
async sendEmail(mail: SendGrid.MailDataRequired) {
return SendGrid.send(mail);
}
async sendForgotPasswordEmail(user: User, resetToken: string): Promise<void> {
const appUrl = this.configService.get<string>('app.url');
const url = `${appUrl}?modal=auth.reset&resetToken=${resetToken}`;
const templateSource = readFileSync(join(__dirname, 'assets/templates/forgot-password.hbs'), 'utf-8');
const template = compile(templateSource);
const html = template({ name: user.name, url });
await this.transporter.sendMail({
const mailData: SendGrid.MailDataRequired = {
from: {
name: this.configService.get<string>('sendgrid.fromName'),
email: this.configService.get<string>('sendgrid.fromEmail'),
},
to: user.email,
subject: 'Reset your Reactive Resume password',
html,
});
hideWarnings: true,
dynamicTemplateData: { url },
templateId: this.configService.get<string>('sendgrid.forgotPasswordTemplateId'),
};
await SendGrid.send(mailData);
return;
}
}