Skip to content

Commit

Permalink
Replace nestjs mailer with nodemailer (#197)
Browse files Browse the repository at this point in the history
* Replace nestjs mailer with nodemailer

* Fix format date package

* Remove @nestjs-modules/mailer in package.json
  • Loading branch information
PhamAnhHoang authored Feb 2, 2024
1 parent fe430a7 commit 951a91d
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 1,052 deletions.
12 changes: 9 additions & 3 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { OrganizationService } from '../organization/organization.service'
import { ConfigService } from '@nestjs/config'
import * as bcrypt from 'bcrypt'
import { pages } from '@isomera/impl'

Check warning on line 25 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'pages' is defined but never used
import { HandlebarsTemplate } from '../mailer/types/mailer.types'

@Injectable()
export class AuthService {
Expand All @@ -48,7 +49,7 @@ export class AuthService {
await this.mailerService.sendEmail(
user,
'Email verification',
'email-confirmation',
HandlebarsTemplate.EMAIL_CONFIRMATION,
{
name: user.firstName,
code: code.code
Expand Down Expand Up @@ -143,7 +144,12 @@ export class AuthService {
}

async sendGreetings(user: UserEntity) {
return this.mailerService.sendEmail(user, 'Welcome!', 'welcome', { user })
return this.mailerService.sendEmail(
user,
'Welcome!',
HandlebarsTemplate.WELCOME,
{ user }
)
}

async requestPasswordReset(email: string): Promise<boolean> {
Expand All @@ -161,7 +167,7 @@ export class AuthService {
void this.mailerService.sendEmail(
user,
'Password reset code',
'password-reset-code',
HandlebarsTemplate.PASSWORD_RESET_CODE,
{
name: `${user.firstName} ${user.lastName}`,
code: passwordResetCode,
Expand Down
38 changes: 2 additions & 36 deletions apps/api/src/mailer/mailer.module.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,9 @@
import { MailerModule as MailModule } from '@nestjs-modules/mailer'
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { MailerService } from './mailer.service'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { config as dotenvConfig } from 'dotenv'

dotenvConfig({ path: '.env' })

@Module({
imports: [
MailModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
transport: {
host: configService.get('MAIL_HOST', 'localhost'),
port: configService.get('MAIL_PORT', 1025),
secure: false,
...(configService.get('MAIL_USER') && {
auth: {
type: 'login',
user: configService.get('MAIL_USER'),
pass: configService.get('MAIL_PASSWORD')
}
})
},
defaults: {
from: '"No Reply" <[email protected]>'
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
options: {
strict: true
}
}
})
})
],
imports: [ConfigModule.forRoot()],
providers: [MailerService],
exports: [MailerService]
})
Expand Down
16 changes: 10 additions & 6 deletions apps/api/src/mailer/mailer.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import { Test, TestingModule } from '@nestjs/testing'
import { MailerService } from './mailer.service'
import { MailerService as Mailer } from '@nestjs-modules/mailer'
import { createMock } from '@golevelup/ts-jest'
import { ConfigService } from '@nestjs/config'

jest.mock('nodemailer', () => ({
createTransport: jest.fn().mockReturnValue({
sendMail: jest.fn().mockResolvedValue({ messageId: 'test-message-id' })
})
}))

describe('MailerService', () => {
let service: MailerService
let mockedMailerService: jest.Mocked<Mailer>
let configService: ConfigService

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MailerService]
})
.useMocker(token => {
if (Object.is(token, Mailer)) {
return createMock<Mailer>()
if (Object.is(token, ConfigService)) {
return createMock<ConfigService>()
}
})
.compile()

mockedMailerService = module.get<Mailer, jest.Mocked<Mailer>>(Mailer)

service = module.get<MailerService>(MailerService)
})

Expand Down
82 changes: 74 additions & 8 deletions apps/api/src/mailer/mailer.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,86 @@
import { MailerService as Mailer } from '@nestjs-modules/mailer'
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { UserEntity } from '../entities/user.entity'
import * as nodemailer from 'nodemailer'
import * as handlebars from 'handlebars'
import * as fs from 'fs'
import * as path from 'path'
import { HandlebarsTemplate } from './types/mailer.types'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class MailerService {
constructor(private mailerService: Mailer) {}
private transporter: nodemailer.Transporter
private templateCache: Map<HandlebarsTemplate, handlebars.TemplateDelegate>

constructor(protected readonly configService: ConfigService) {
this.templateCache = new Map()

const host = this.configService.get<string>('MAIL_HOST', 'localhost')
const port = this.configService.get<number>('MAIL_PORT', 1025)
const secure = this.configService.get<boolean>('MAILER_SECURE', false)
const user = this.configService.get<string>('MAIL_USER')
const pass = this.configService.get<string>('MAIL_PASSWORD')
const fromName = this.configService.get<string>(
'MAIL_FROM_NAME',
'No-reply'
)
const fromAddress = this.configService.get<string>(
'MAIL_FROM_ADDRESS',
'[email protected]'
)

this.transporter = nodemailer.createTransport(
{
host,
port,
secure,
auth: {
user,
pass
}
},
{
from: {
name: fromName,
address: fromAddress
}
}
)
}

private loadTemplate(templateName: HandlebarsTemplate, data: object): string {
if (this.templateCache.has(templateName)) {
const templateRenderFunction = this.templateCache.get(templateName)

return templateRenderFunction(data)
}

const templatesFolderPath = path.join(__dirname, './templates')
const templatePath = path.join(templatesFolderPath, templateName)

const templateSource = fs.readFileSync(templatePath, 'utf8')

const templateRenderFunction = handlebars.compile(templateSource)
this.templateCache.set(templateName, templateRenderFunction)

const finalHtml = templateRenderFunction(data)

return finalHtml
}

async sendEmail(
user: UserEntity,
subject: string,
template: HandlebarsTemplate,
data?: object
) {
const html = this.loadTemplate(template, data)

async sendEmail(user: UserEntity, subject: string, template: string, data) {
try {
return await this.mailerService.sendMail({
await this.transporter.sendMail({
to: user.email,
subject: subject,
template: template,
context: {
...data
}
html: html
})
} catch (err) {
console.error(err)
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/mailer/types/mailer.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum HandlebarsTemplate {
PASSWORD_RESET_CODE = 'password-reset-code',
WELCOME = 'welcome',
EMAIL_CONFIRMATION = 'email-confirmation'
}
3 changes: 1 addition & 2 deletions apps/api/src/user/confirm-code.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { MoreThan, Repository } from 'typeorm'
import { generateRandomStringUtil } from '@isomera/utils'
import { format } from 'date-fns'
import { ConfirmCodeEntity } from '../entities/confirm-code.entity'
import { UserEntity } from '../entities/user.entity'
import { DateTime } from 'luxon'
Expand Down Expand Up @@ -38,7 +37,7 @@ export class ConfirmCodeService {
.createQueryBuilder()
.where({
code: code,
expiresIn: MoreThan(format(new Date(), 'yyyy-MM-dd HH:MM:ss'))
expiresIn: MoreThan(DateTime.now().toFormat('yyyy-MM-dd HH:MM:ss'))
})
.andWhere('"userId" = :userId', { userId: user.id })
.limit(1)
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"dependencies": {
"@compodoc/compodoc": "^1.1.22",
"@expo/metro-config": "~0.10.7",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/common": "^10.0.2",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.2",
Expand Down
Loading

0 comments on commit 951a91d

Please sign in to comment.