Skip to content

Commit

Permalink
Merge pull request #354 from King-Mikaelson/feat/email
Browse files Browse the repository at this point in the history
feat: Implement email sending functionalities
  • Loading branch information
buka4rill authored Jul 24, 2024
2 parents 85af481 + 775896f commit b2bccb8
Show file tree
Hide file tree
Showing 11 changed files with 2,835 additions and 41 deletions.
2,498 changes: 2,458 additions & 40 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"@faker-js/faker": "^8.4.1",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
Expand All @@ -46,6 +47,7 @@
"class-validator": "^0.14.1",
"joi": "^17.6.0",
"nestjs-pino": "^4.1.0",
"nodemailer": "^6.9.14",
"passport-jwt": "^4.0.1",
"pg": "^8.12.0",
"reflect-metadata": "^0.2.2",
Expand Down Expand Up @@ -104,4 +106,4 @@
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
}
}
28 changes: 28 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import { SeedingModule } from './database/seeding/seeding.module';
import HealthController from './health.controller';
import { AuthModule } from './modules/auth/auth.module';
import { UserModule } from './modules/user/user.module';
import { EmailModule } from './modules/email/email.module';
import authConfig from '../config/auth.config';
import { OrganisationsModule } from './modules/organisations/organisations.module';
import { AuthGuard } from './guards/auth.guard';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';

@Module({
providers: [
Expand Down Expand Up @@ -63,6 +66,31 @@ import { AuthGuard } from './guards/auth.guard';
SeedingModule,
AuthModule,
UserModule,
EmailModule,
MailerModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
transport: {
host: configService.get<string>('SMTP_HOST'),
port: configService.get<number>('SMTP_PORT'),
auth: {
user: configService.get<string>('SMTP_USER'),
pass: configService.get<string>('SMTP_PASSWORD'),
},
},
defaults: {
from: `"Team Remote Bingo" <${configService.get<string>('SMTP_USER')}>`,
},
template: {
dir: process.cwd() + '/src/modules/email/templates',
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
}),
inject: [ConfigService],
}),
OrganisationsModule,
],
controllers: [HealthController],
Expand Down
5 changes: 5 additions & 0 deletions src/modules/email/article.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ArticleInterface {
title: string;
description: string;
link: string;
}
8 changes: 8 additions & 0 deletions src/modules/email/email.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { EmailService } from './email.service';

@Module({
providers: [EmailService],
exports: [EmailService],
})
export class EmailModule {}
113 changes: 113 additions & 0 deletions src/modules/email/email.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EmailService } from './email.service';
import { MailerService } from '@nestjs-modules/mailer';

describe('EmailService', () => {
let service: EmailService;
let mailerService: MailerService;

const mockMailerService = {
sendMail: jest.fn().mockResolvedValue({}),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EmailService,
{
provide: MailerService,
useValue: mockMailerService,
},
],
}).compile();

service = module.get<EmailService>(EmailService);
mailerService = module.get<MailerService>(MailerService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('should send user confirmation email', async () => {
const email = '[email protected]';
const url = 'http://example.com/confirm';
const token = 'mike';
const link = `${url}?token=${token}`;

await service.sendUserConfirmationMail(email, url, token);

expect(mailerService.sendMail).toHaveBeenCalledWith({
to: email,
subject: 'Welcome to My App! Confirm your Email',
template: 'confirmation',
context: {
link,
email,
},
});
});

it('should send forgot password email', async () => {
const email = '[email protected]';
const url = 'http://example.com/reset';
const token = 'mike';
const link = `${url}?token=${token}`;

await service.sendForgotPasswordMail(email, url, token);

expect(mailerService.sendMail).toHaveBeenCalledWith({
to: email,
subject: 'Reset Password',
template: 'reset-password',
context: {
link,
email,
},
});
});

it('should send waitlist confirmation email', async () => {
const email = '[email protected]';
const url = 'http://example.com/waitlist';

await service.sendWaitListMail(email, url);

expect(mailerService.sendMail).toHaveBeenCalledWith({
to: email,
subject: 'Waitlist Confirmation',
template: 'waitlist',
context: {
url,
email,
},
});
});

it('should send newsletter email', async () => {
const email = '[email protected]';
const articles = [
{
title: 'Article Title 1',
description: 'Short description of the article.',
link: 'https://example.com/article1',
},
{
title: 'Article Title 2',
description: 'Short description of the article.',
link: 'https://example.com/article2',
},
];
await service.sendNewsLetterMail(email, articles);

expect(mailerService.sendMail).toHaveBeenCalledWith({
to: email,
subject: 'Monthly Newsletter',
template: 'newsletter',
context: {
email,
articles,
},
});
});
});
58 changes: 58 additions & 0 deletions src/modules/email/email.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
import { ArticleInterface } from './article.interface';

@Injectable()
export class EmailService {
constructor(private readonly mailerService: MailerService) {}

async sendUserConfirmationMail(email: string, url: string, token: string) {
const link = `${url}?token=${token}`;
await this.mailerService.sendMail({
to: email,
subject: 'Welcome to My App! Confirm your Email',
template: 'confirmation',
context: {
link,
email,
},
});
}

async sendForgotPasswordMail(email: string, url: string, token: string) {
const link = `${url}?token=${token}`;
await this.mailerService.sendMail({
to: email,
subject: 'Reset Password',
template: 'reset-password',
context: {
link,
email,
},
});
}

async sendWaitListMail(email: string, url: string) {
await this.mailerService.sendMail({
to: email,
subject: 'Waitlist Confirmation',
template: 'waitlist',
context: {
url,
email,
},
});
}

async sendNewsLetterMail(email: string, articles: ArticleInterface[]) {
await this.mailerService.sendMail({
to: email,
subject: 'Monthly Newsletter',
template: 'newsletter',
context: {
email,
articles,
},
});
}
}
40 changes: 40 additions & 0 deletions src/modules/email/templates/confirmation.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>Email Confirmation</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; } .container { width:
100%; max-width: 600px; margin: 0 auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow:
0 0 10px rgba(0, 0, 0, 0.1); } .header { text-align: center; padding-bottom: 20px; } .header img { width: 120px;
/* Adjust based on your logo */ } .content { text-align: center; padding: 20px; } .content h1 { color: #333; /*
Dark gray for readability */ font-size: 24px; margin-bottom: 15px; } .content p { color: #666; /* Light gray for
softer text */ font-size: 16px; line-height: 1.5; margin-bottom: 20px; } .btn { display: inline-block; padding:
12px 25px; color: #ffffff; background-color: #28a745; /* Secondary brand color */ text-decoration: none;
border-radius: 5px; font-weight: bold; font-size: 16px; margin-top: 20px; } .footer { text-align: center;
padding-top: 20px; font-size: 12px; color: #888888; } .footer a { color: #28a745; /* Match button color for
consistency */ text-decoration: none; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<img
src='https://www.shutterstock.com/image-vector/circle-line-simple-design-logo-600nw-2174926871.jpg'
alt='Company Logo'
/>
</div>
<div class='content'>
<h1>Confirm Your Email Address</h1>
<p>Hello {{email}},</p>
<p>Thank you for registering. Please confirm your email address by clicking the button below:</p>
<a href='{{link}}' class='btn'>Confirm Email</a>
<p>If you didn't create an account with us, you can safely ignore this email.</p>
</div>
<div class='footer'>
<p>&copy; 2024 Company Name. All rights reserved.</p>
<p><a href='https://example.com/unsubscribe'>Unsubscribe</a></p>
</div>
</div>
</body>
</html>
43 changes: 43 additions & 0 deletions src/modules/email/templates/newsletter.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>Newsletter</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; } .container { width:
100%; max-width: 600px; margin: 0 auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow:
0 0 10px rgba(0, 0, 0, 0.1); } .header { text-align: center; padding-bottom: 20px; } .header img { width: 120px;
/* Adjust based on your logo */ } .content { padding: 20px; } .content h1 { color: #333; /* Dark gray for
readability */ font-size: 24px; margin-bottom: 15px; } .content p { color: #666; /* Light gray for softer text */
font-size: 16px; line-height: 1.5; margin-bottom: 20px; } .content .article { margin-bottom: 20px; } .content
.article h2 { font-size: 20px; color: #007bff; /* Primary brand color for headers */ margin-bottom: 10px; }
.content .article p { font-size: 14px; color: #666; } .content .article a { color: #007bff; /* Match header color
*/ text-decoration: none; } .footer { text-align: center; padding-top: 20px; font-size: 12px; color: #888888; }
.footer a { color: #007bff; /* Match brand color */ text-decoration: none; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<img
src='https://www.shutterstock.com/image-vector/circle-line-simple-design-logo-600nw-2174926871.jpg'
alt='Company Logo'
/>
</div>
<div class='content'>
<h1>Monthly Newsletter</h1>
<p>Hello {{email}},</p>
{{#each articles}}
<div class='article'>
<h2>{{this.title}}</h2>
<p>{{this.description}} <a href='{{this.link}}'>Read more</a></p>
</div>
{{/each}}
</div>
<div class='footer'>
<p>&copy; 2024 Company Name. All rights reserved.</p>
<p><a href='https://example.com/unsubscribe'>Unsubscribe</a></p>
</div>
</div>
</body>
</html>
40 changes: 40 additions & 0 deletions src/modules/email/templates/reset-password.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>Forgot Password</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; } .container { width:
100%; max-width: 600px; margin: 0 auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow:
0 0 10px rgba(0, 0, 0, 0.1); } .header { text-align: center; padding-bottom: 20px; } .header img { width: 120px;
/* Adjust based on your logo */ } .content { text-align: center; padding: 20px; } .content h1 { color: #333; /*
Dark gray for readability */ font-size: 24px; margin-bottom: 15px; } .content p { color: #666; /* Light gray for
softer text */ font-size: 16px; line-height: 1.5; margin-bottom: 20px; } .btn { display: inline-block; padding:
12px 25px; color: #ffffff; background-color: #28a745; /* Secondary brand color */ text-decoration: none;
border-radius: 5px; font-weight: bold; font-size: 16px; margin-top: 20px; } .footer { text-align: center;
padding-top: 20px; font-size: 12px; color: #888888; } .footer a { color: #28a745; /* Match button color for
consistency */ text-decoration: none; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<img
src='https://www.shutterstock.com/image-vector/circle-line-simple-design-logo-600nw-2174926871.jpg'
alt='Company Logo'
/>
</div>
<div class='content'>
<h1>Forgot Password</h1>
<p>Hello {{email}},</p>
<p>It seems like you forgot your password. Click the button below to reset it:</p>
<a href='{{link}}' class='btn'>Reset Password</a>
<p>If you didn't request a password reset, please ignore this email.</p>
</div>
<div class='footer'>
<p>&copy; 2024 Company Name. All rights reserved.</p>
<p><a href='https://example.com/unsubscribe'>Unsubscribe</a></p>
</div>
</div>
</body>
</html>
Loading

0 comments on commit b2bccb8

Please sign in to comment.