Skip to content

Commit 2dc1fbe

Browse files
feat: added support for the SMTP email provider. (#1535)
* feat/support smtp email provider Signed-off-by: pranalidhanavade <pranali.dhanavade@ayanworks.com> * refactor: error handling for email provider Signed-off-by: pranalidhanavade <pranali.dhanavade@ayanworks.com> * fix: sonarlint issues Signed-off-by: pranalidhanavade <pranali.dhanavade@ayanworks.com> * fix: HTML injection vulnerability issue Signed-off-by: pranalidhanavade <pranali.dhanavade@ayanworks.com> --------- Signed-off-by: pranalidhanavade <pranali.dhanavade@ayanworks.com>
1 parent c1c9356 commit 2dc1fbe

7 files changed

Lines changed: 120 additions & 17 deletions

File tree

apps/organization/templates/organization-invitation.template.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { escapeHtml } from '@credebl/common/common.utils';
2+
13
export class OrganizationInviteTemplate {
24
public sendInviteEmailTemplate(
35
email: string,
@@ -17,7 +19,9 @@ export class OrganizationInviteTemplate {
1719
: `After successful registration, you can log in to the platform and click on “Accept Organization Invitation” on your dashboard.`;
1820

1921
const Button = isUserExist ? `Accept Organization Invitation` : `Register on ${process.env.PLATFORM_NAME}`;
20-
22+
const safeEmail = escapeHtml(email);
23+
const safeOrgName = escapeHtml(orgName);
24+
const safeFirstName = escapeHtml(firstName);
2125
return `<!DOCTYPE html>
2226
<html lang="en">
2327
@@ -36,13 +40,13 @@ export class OrganizationInviteTemplate {
3640
<div style="font-family: Montserrat; font-style: normal; font-weight: 500;
3741
font-size: 15px; line-height: 24px;color: #00000;">
3842
<p style="margin-top:0px">
39-
Hello ${email},
43+
Hello ${safeEmail},
4044
</p>
4145
<p>
42-
${firstName} has invited you to join “${orgName}” as a member.
46+
${safeFirstName} has invited you to join “${safeOrgName}” as a member.
4347
4448
</p><ul>
45-
<li><strong>Organization:</strong> ${orgName}</li>
49+
<li><strong>Organization:</strong> ${safeOrgName}</li>
4650
<li><strong>Role:</strong> Member</li>
4751
</ul>
4852
<p>

apps/user/src/main.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
2+
3+
import { CommonConstants } from '@credebl/common/common.constant';
14
import { HttpExceptionFilter } from 'libs/http-exception.filter';
25
import { Logger } from '@nestjs/common';
36
import { NestFactory } from '@nestjs/core';
7+
import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter';
48
import { UserModule } from './user.module';
5-
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
69
import { getNatsOptions } from '@credebl/common/nats.config';
7-
import { CommonConstants } from '@credebl/common/common.constant';
8-
import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter';
910

1011
const logger = new Logger();
1112

@@ -19,5 +20,24 @@ async function bootstrap(): Promise<void> {
1920

2021
await app.listen();
2122
logger.log('User Microservice is listening to NATS ');
23+
const supportedProviders = ['sendgrid', 'resend', 'smtp'] as const;
24+
type EmailProvider = (typeof supportedProviders)[number];
25+
const provider = process.env.EMAIL_PROVIDER?.toLowerCase();
26+
27+
if (!provider) {
28+
Logger.warn(
29+
`Email service is disabled because EMAIL_PROVIDER is not set. ` +
30+
`Configure EMAIL_PROVIDER (sendgrid, resend, or smtp) to enable sending emails.`
31+
);
32+
} else if (!supportedProviders.includes(provider as EmailProvider)) {
33+
Logger.warn(
34+
`Unknown EMAIL_PROVIDER value "${process.env.EMAIL_PROVIDER}". ` +
35+
`Supported providers are: sendgrid, resend, smtp. ` +
36+
`Email service will be disabled.`
37+
);
38+
} else {
39+
const Emailprovider = provider as EmailProvider;
40+
logger.log(`Email provider configured: ${Emailprovider}`);
41+
}
2242
}
2343
bootstrap();

libs/common/src/common.constant.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,9 @@ export enum CommonConstants {
305305
PLATFORM_ADMIN_EMAIL = 'platform.admin@yopmail.com',
306306
PLATFORM_ADMIN_ORG = 'Platform-admin',
307307
PLATFORM_ADMIN_ORG_ROLE = 'platform_admin',
308-
DEFAULT_EMAIL_PROVIDER = 'sendgrid',
308+
SENDGRID_EMAIL_PROVIDER = 'sendgrid',
309+
RESEND_EMAIL_PROVIDER = 'resend',
310+
SMTP_EMAIL_PROVIDER = 'smtp',
309311
USER_HOLDER_ROLE = 'holder',
310312

311313
//onBoarding Type

libs/common/src/common.utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,12 @@ export function shouldLoadOidcModules(): boolean {
145145
const hide = 'true' === raw.toLowerCase();
146146
return !hide;
147147
}
148+
149+
export const escapeHtml = (value: string): string =>
150+
value
151+
.replace(/&/g, '&amp;')
152+
.replace(/</g, '&lt;')
153+
.replace(/>/g, '&gt;')
154+
.replace(/"/g, '&quot;')
155+
.replace(/'/g, '&#39;')
156+
.replace(/\//g, '&#x2F;');

libs/common/src/email.service.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,38 @@ import { Injectable, Logger } from '@nestjs/common';
33
import { CommonConstants } from './common.constant';
44
import { EmailDto } from './dtos/email.dto';
55
import { sendWithResend } from './resend-helper-file';
6+
import { sendWithSMTP } from './smtp-helper-file';
67
import { sendWithSendGrid } from './send-grid-helper-file';
78

89
@Injectable()
910
export class EmailService {
1011
private readonly logger = new Logger(EmailService.name);
1112

1213
async sendEmail(emailDto: EmailDto): Promise<boolean> {
13-
const provider = process.env.EMAIL_PROVIDER?.toLowerCase() || CommonConstants.DEFAULT_EMAIL_PROVIDER;
14+
const provider = process.env.EMAIL_PROVIDER?.toLowerCase();
1415

1516
this.logger.debug(`Email Provider is: ${provider}`);
1617
let result: boolean;
1718

1819
try {
1920
switch (provider) {
20-
case 'sendgrid':
21+
case CommonConstants.SENDGRID_EMAIL_PROVIDER:
2122
result = await sendWithSendGrid(emailDto);
2223
break;
23-
case 'resend':
24+
case CommonConstants.RESEND_EMAIL_PROVIDER:
2425
result = await sendWithResend(emailDto);
2526
break;
27+
case CommonConstants.SMTP_EMAIL_PROVIDER:
28+
result = await sendWithSMTP(emailDto);
29+
break;
2630
default:
27-
this.logger.warn(`Unknown email provider: ${provider}, defaulting to SendGrid.`);
28-
result = await sendWithSendGrid(emailDto);
31+
this.logger.warn(`Unknown email provider: ${provider}`);
32+
return false;
2933
}
3034
} catch (error) {
3135
this.logger.error(`Failed to send email using ${provider}: ${error.message}`);
3236
throw error;
3337
}
34-
3538
return result;
3639
}
3740
}

libs/common/src/resend-helper-file.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import { Resend } from 'resend';
66

77
dotenv.config();
88

9+
const emailProvider = process.env.EMAIL_PROVIDER;
910
const apiKey = process.env.RESEND_API_KEY;
10-
if (!apiKey) {
11-
throw new Error('Missing RESEND_API_KEY in environment variables.');
11+
12+
let resend: Resend | null = null;
13+
14+
if ('resend' === emailProvider) {
15+
if (!apiKey) {
16+
throw new Error('Missing RESEND_API_KEY in environment variables.');
17+
}
18+
resend = new Resend(apiKey);
1219
}
13-
const resend = new Resend(apiKey);
1420

1521
export const sendWithResend = async (emailDto: EmailDto): Promise<boolean> => {
1622
try {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as dotenv from 'dotenv';
2+
import * as nodemailer from 'nodemailer';
3+
4+
import { EmailDto } from './dtos/email.dto';
5+
import { Logger } from '@nestjs/common';
6+
7+
dotenv.config();
8+
9+
const emailProvider = process.env.EMAIL_PROVIDER?.toLowerCase();
10+
11+
let transporter: nodemailer.Transporter | null = null;
12+
13+
if ('smtp' === emailProvider) {
14+
const { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS } = process.env;
15+
16+
if (!SMTP_HOST || !SMTP_PORT || !SMTP_USER || !SMTP_PASS) {
17+
throw new Error('Missing SMTP configuration. Required: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS');
18+
}
19+
20+
const port = Number(SMTP_PORT);
21+
22+
if (!Number.isInteger(port) || 0 >= port) {
23+
throw new Error(`Invalid SMTP_PORT value: "${SMTP_PORT}". Must be a valid number.`);
24+
}
25+
26+
transporter = nodemailer.createTransport({
27+
host: SMTP_HOST,
28+
port: Number(SMTP_PORT),
29+
secure: 465 === Number(SMTP_PORT),
30+
auth: {
31+
user: SMTP_USER,
32+
pass: SMTP_PASS
33+
},
34+
requireTLS: 587 === Number(SMTP_PORT)
35+
});
36+
}
37+
38+
export const sendWithSMTP = async (emailDto: EmailDto): Promise<boolean> => {
39+
if (!transporter) {
40+
Logger.error('SMTP email provider is not initialized');
41+
return false;
42+
}
43+
44+
try {
45+
await transporter.sendMail({
46+
from: emailDto.emailFrom,
47+
to: emailDto.emailTo,
48+
subject: emailDto.emailSubject,
49+
text: emailDto.emailText,
50+
html: emailDto.emailHtml,
51+
attachments: emailDto.emailAttachments
52+
});
53+
54+
return true;
55+
} catch (error) {
56+
Logger.error('Error while sending email with SMTP', error);
57+
return false;
58+
}
59+
};

0 commit comments

Comments
 (0)