@b2y/email-service 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Config.js +41 -0
- package/Logger.js +6 -0
- package/README.md +18 -18
- package/index.js +78 -23
- package/package.json +49 -43
- package/providers/AmazonSESProvider.js +77 -0
- package/providers/BaseProvider.js +74 -10
- package/providers/NodeMailerProvider.js +53 -0
- package/providers/PostmarkProvider.js +62 -0
- package/providers/SendgridProvider.js +60 -33
- package/utils/TemplateEngine.js +99 -86
- package/EmailService.js +0 -20
- package/config.js +0 -9
- package/logger.js +0 -81
- package/providers/NodemailerProvider.js +0 -40
- package/server.js +0 -15
- package/templates/resetPassword.html +0 -7
- package/templates/welcome.html +0 -6
package/Config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// config.js
|
|
2
|
+
require('dotenv').config();
|
|
3
|
+
const Config = {
|
|
4
|
+
// Email Provider Configuration
|
|
5
|
+
emailProvider: process.env.EMAIL_PROVIDER,
|
|
6
|
+
|
|
7
|
+
// Nodemailer Configuration
|
|
8
|
+
emailService: process.env.EMAIL_SERVICE,
|
|
9
|
+
emailHost: process.env.EMAIL_HOST,
|
|
10
|
+
smtpPort: process.env.SMTP_PORT,
|
|
11
|
+
emailUser: process.env.EMAIL_USERNAME,
|
|
12
|
+
emailPass: process.env.EMAIL_PASSWORD,
|
|
13
|
+
fromEmail: process.env.FROM_EMAIL,
|
|
14
|
+
|
|
15
|
+
// SendGrid Configuration
|
|
16
|
+
sendGridApiKey: process.env.SENDGRID_API_KEY,
|
|
17
|
+
|
|
18
|
+
// Postmark Configuration
|
|
19
|
+
postmarkApiKey: process.env.POSTMARK_API_KEY,
|
|
20
|
+
postmarkFromEmail: process.env.POSTMARK_FROM_EMAIL,
|
|
21
|
+
|
|
22
|
+
// Amazon SES Configuration
|
|
23
|
+
awsRegion: process.env.AWS_REGION,
|
|
24
|
+
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
25
|
+
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
26
|
+
awsFromEmail: process.env.AWS_FROM_EMAIL,
|
|
27
|
+
|
|
28
|
+
defaultLanguage: process.env.DEFAULT_LANGUAGE,
|
|
29
|
+
|
|
30
|
+
// Database Configuration (if using database for templates)
|
|
31
|
+
dbHost: process.env.DB_HOST,
|
|
32
|
+
dbPort: process.env.DB_PORT,
|
|
33
|
+
dbName: process.env.DB_DATABASE,
|
|
34
|
+
dbUser: process.env.DB_USER,
|
|
35
|
+
dbPass: process.env.DB_PASSWORD,
|
|
36
|
+
dbschema:process.env.DB_SCHEMA,
|
|
37
|
+
dbssl:process.env.DBSSL,
|
|
38
|
+
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = Config;
|
package/Logger.js
ADDED
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# email-service
|
|
2
|
-
|
|
3
|
-
A reusable, flexible Node.js email service supporting multiple providers like **Nodemailer (Gmail)** and **SendGrid**, with support for **dynamic HTML templates** using Handlebars.
|
|
4
|
-
|
|
5
|
-
# Features
|
|
6
|
-
|
|
7
|
-
- Common interface for multiple email providers
|
|
8
|
-
- Supports Gmail via Nodemailer and SendGrid (easy to extend more)
|
|
9
|
-
- Handlebars-based HTML templates with variables
|
|
10
|
-
- Easily switch between providers without changing your code
|
|
11
|
-
- Dynamic template loading from the filesystem
|
|
12
|
-
- Configurable via environment variables or direct parameters
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
# Installation
|
|
17
|
-
|
|
18
|
-
npm install @ourgitname/email-service
|
|
1
|
+
# email-service
|
|
2
|
+
|
|
3
|
+
A reusable, flexible Node.js email service supporting multiple providers like **Nodemailer (Gmail)** and **SendGrid**, with support for **dynamic HTML templates** using Handlebars.
|
|
4
|
+
|
|
5
|
+
# Features
|
|
6
|
+
|
|
7
|
+
- Common interface for multiple email providers
|
|
8
|
+
- Supports Gmail via Nodemailer and SendGrid (easy to extend more)
|
|
9
|
+
- Handlebars-based HTML templates with variables
|
|
10
|
+
- Easily switch between providers without changing your code
|
|
11
|
+
- Dynamic template loading from the filesystem
|
|
12
|
+
- Configurable via environment variables or direct parameters
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Installation
|
|
17
|
+
|
|
18
|
+
npm install @ourgitname/email-service
|
package/index.js
CHANGED
|
@@ -1,23 +1,78 @@
|
|
|
1
|
-
const BaseProvider = require('./providers/BaseProvider');
|
|
2
|
-
const
|
|
3
|
-
const SendgridProvider = require('./providers/SendgridProvider');
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
1
|
+
const BaseProvider = require('./providers/BaseProvider');
|
|
2
|
+
const NodeMailerProvider = require('./providers/NodeMailerProvider');
|
|
3
|
+
const SendgridProvider = require('./providers/SendgridProvider');
|
|
4
|
+
const PostmarkProvider = require('./providers/PostmarkProvider');
|
|
5
|
+
const AmazonSesProvider = require('./providers/AmazonSESProvider');
|
|
6
|
+
const TemplateEngine = require('./utils/TemplateEngine');
|
|
7
|
+
const EmailService = require('./service/EmailService');
|
|
8
|
+
const Config = require('./Config');
|
|
9
|
+
const logger = require('./Logger');
|
|
10
|
+
const { sequelize, EmailTemplate } = require('./model/Connect');
|
|
11
|
+
const { EmailProvider } = require('./enum/EmailProvider');
|
|
12
|
+
const StatusMessage = require('./constants/StatusMessageConstants');
|
|
13
|
+
// Initialize database connection
|
|
14
|
+
async function initializeDatabase() {
|
|
15
|
+
try {
|
|
16
|
+
await sequelize.authenticate();
|
|
17
|
+
logger.info(StatusMessage.DB_CONNECTION_SUCCESS);
|
|
18
|
+
return { EmailTemplate };
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logger.error(StatusMessage.DB_CONNECTION_FAILED, error);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Factory function to create email provider
|
|
26
|
+
function createEmailProvider() {
|
|
27
|
+
const provider = (Config.emailProvider || '').toLowerCase().trim();
|
|
28
|
+
|
|
29
|
+
switch (provider) {
|
|
30
|
+
case EmailProvider.NODE_MAILER:
|
|
31
|
+
return new NodeMailerProvider();
|
|
32
|
+
case EmailProvider.SENDGRID:
|
|
33
|
+
return new SendgridProvider();
|
|
34
|
+
case EmailProvider.POSTMARK:
|
|
35
|
+
return new PostmarkProvider();
|
|
36
|
+
case EmailProvider.AMAZON_SES:
|
|
37
|
+
return new AmazonSesProvider();
|
|
38
|
+
default:
|
|
39
|
+
logger.warn(StatusMessage.PROVIDER_DEFAULT_FALLBACK);
|
|
40
|
+
return new NodeMailerProvider();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Factory function to create email service
|
|
45
|
+
async function createEmailService(options = {}) {
|
|
46
|
+
try {
|
|
47
|
+
const provider = options.provider || createEmailProvider();
|
|
48
|
+
const dbClient = options.dbClient || await initializeDatabase();
|
|
49
|
+
|
|
50
|
+
return new EmailService(provider, dbClient);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logger.error(`Failed to create email service: ${error.message}`);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create a singleton instance
|
|
58
|
+
let defaultEmailService;
|
|
59
|
+
async function getEmailService() {
|
|
60
|
+
if (!defaultEmailService) {
|
|
61
|
+
defaultEmailService = await createEmailService();
|
|
62
|
+
}
|
|
63
|
+
return defaultEmailService;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
BaseProvider,
|
|
68
|
+
NodeMailerProvider,
|
|
69
|
+
SendgridProvider,
|
|
70
|
+
PostmarkProvider,
|
|
71
|
+
AmazonSesProvider,
|
|
72
|
+
EmailService,
|
|
73
|
+
TemplateEngine,
|
|
74
|
+
createEmailProvider,
|
|
75
|
+
createEmailService,
|
|
76
|
+
getEmailService,
|
|
77
|
+
Config
|
|
78
|
+
};
|
package/package.json
CHANGED
|
@@ -1,43 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@b2y/email-service",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"main": "index.js",
|
|
5
|
-
"exports": {
|
|
6
|
-
".": "./index.js",
|
|
7
|
-
"./providers": "./providers/index.js",
|
|
8
|
-
"./providers/base": "./providers/BaseProvider.js",
|
|
9
|
-
"./providers/nodemailer": "./providers/
|
|
10
|
-
"./providers/sendgrid": "./providers/
|
|
11
|
-
"./utils/templateEngine": "./utils/TemplateEngine.js",
|
|
12
|
-
"./EmailService": "./EmailService.js"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"providers/",
|
|
16
|
-
"templates/",
|
|
17
|
-
"utils/",
|
|
18
|
-
"*.js"
|
|
19
|
-
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
22
|
-
"start": "node test.js"
|
|
23
|
-
},
|
|
24
|
-
"author": "",
|
|
25
|
-
"license": "ISC",
|
|
26
|
-
"description": "A flexible email service with support for multiple providers and custom templates",
|
|
27
|
-
"keywords": [
|
|
28
|
-
"email",
|
|
29
|
-
"templates",
|
|
30
|
-
"nodemailer",
|
|
31
|
-
"sendgrid"
|
|
32
|
-
],
|
|
33
|
-
"publishConfig": {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"@
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@b2y/email-service",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./index.js",
|
|
7
|
+
"./providers": "./providers/index.js",
|
|
8
|
+
"./providers/base": "./providers/BaseProvider.js",
|
|
9
|
+
"./providers/nodemailer": "./providers/NodeMailerProvider.js",
|
|
10
|
+
"./providers/sendgrid": "./providers/SendGridProvider.js",
|
|
11
|
+
"./utils/templateEngine": "./utils/TemplateEngine.js",
|
|
12
|
+
"./EmailService": "./service/EmailService.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"providers/",
|
|
16
|
+
"templates/",
|
|
17
|
+
"utils/",
|
|
18
|
+
"*.js"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
22
|
+
"start": "node test.js"
|
|
23
|
+
},
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"description": "A flexible email service with support for multiple providers and custom templates",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"email",
|
|
29
|
+
"templates",
|
|
30
|
+
"nodemailer",
|
|
31
|
+
"sendgrid"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@aws-sdk/client-ses": "^3.957.0",
|
|
38
|
+
"@sendgrid/mail": "^8.1.5",
|
|
39
|
+
"b2y-logger": "^1.1.0",
|
|
40
|
+
"dotenv": "^16.5.0",
|
|
41
|
+
"handlebars": "^4.7.8",
|
|
42
|
+
"log4js": "^6.9.1",
|
|
43
|
+
"nodemailer": "^6.10.0",
|
|
44
|
+
"pg": "^8.16.3",
|
|
45
|
+
"pg-hstore": "^2.3.4",
|
|
46
|
+
"postmark": "^4.0.5",
|
|
47
|
+
"sequelize": "^6.37.7"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// providers/AmazonSES.js
|
|
2
|
+
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
|
|
3
|
+
const BaseProvider = require('./BaseProvider');
|
|
4
|
+
const Config = require('../Config');
|
|
5
|
+
const logger = require('../Logger');
|
|
6
|
+
const StatusMessage = require('../constants/StatusMessageConstants');
|
|
7
|
+
const { EmailProvider } = require('../enum/EmailProvider');
|
|
8
|
+
|
|
9
|
+
class AmazonSESProvider extends BaseProvider {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.client = new SESClient({
|
|
13
|
+
region: Config.awsRegion,
|
|
14
|
+
credentials: {
|
|
15
|
+
accessKeyId: Config.awsAccessKeyId,
|
|
16
|
+
secretAccessKey: Config.awsSecretAccessKey
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async sendEmail(options) {
|
|
22
|
+
try {
|
|
23
|
+
this.validateEmailOptions(options);
|
|
24
|
+
|
|
25
|
+
const toAddresses = this.formatRecipients(options.to);
|
|
26
|
+
const ccAddresses = options.cc ? this.formatRecipients(options.cc) : [];
|
|
27
|
+
const bccAddresses = options.bcc ? this.formatRecipients(options.bcc) : [];
|
|
28
|
+
|
|
29
|
+
const params = {
|
|
30
|
+
Source: options.from || Config.awsFromEmail,
|
|
31
|
+
Destination: {
|
|
32
|
+
ToAddresses: toAddresses,
|
|
33
|
+
...(ccAddresses.length > 0 && { CcAddresses: ccAddresses }),
|
|
34
|
+
...(bccAddresses.length > 0 && { BccAddresses: bccAddresses })
|
|
35
|
+
},
|
|
36
|
+
Message: {
|
|
37
|
+
Subject: {
|
|
38
|
+
Data: options.subject,
|
|
39
|
+
Charset: 'UTF-8'
|
|
40
|
+
},
|
|
41
|
+
Body: {
|
|
42
|
+
Html: {
|
|
43
|
+
Data: options.html,
|
|
44
|
+
Charset: 'UTF-8'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Add attachments if present (SES requires using SendRawEmail for attachments)
|
|
51
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
52
|
+
logger.warn('SES provider: Attachments require SendRawEmail. Consider implementing raw email support.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const command = new SendEmailCommand(params);
|
|
56
|
+
const result = await this.client.send(command);
|
|
57
|
+
|
|
58
|
+
logger.info('Email sent successfully via Amazon SES', {
|
|
59
|
+
messageId: result.MessageId,
|
|
60
|
+
recipients: { to: options.to, cc: options.cc, bcc: options.bcc },
|
|
61
|
+
tenantId: options.tenantId
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
messageId: result.MessageId,
|
|
67
|
+
provider: EmailProvider.SES,
|
|
68
|
+
tenantId: options.tenantId
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error(`Amazon SES failed to send email: ${error.message}`);
|
|
72
|
+
throw new Error(`${StatusMessage.AMAZON_SES_SEND_ERROR}: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = AmazonSESProvider;
|
|
@@ -1,10 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// providers/BaseProvider.js
|
|
2
|
+
const logger = require('../Logger');
|
|
3
|
+
const StatusMessage = require('../constants/StatusMessageConstants');
|
|
4
|
+
class BaseProvider {
|
|
5
|
+
constructor() {
|
|
6
|
+
if (new.target === BaseProvider) {
|
|
7
|
+
throw new Error(StatusMessage.CANNOT_INSTANTIATE_BASE);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async sendEmail(options) {
|
|
12
|
+
logger.error('sendEmail method not implemented in BaseProvider');
|
|
13
|
+
throw new Error(StatusMessage.PROVIDER_NOT_IMPLEMENTED);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
validateEmailOptions(options) {
|
|
17
|
+
const { to, subject, html, from } = options;
|
|
18
|
+
|
|
19
|
+
if (!to || (Array.isArray(to) && to.length === 0)) {
|
|
20
|
+
throw new Error(StatusMessage.RECIPIENTS_REQUIRED);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!subject) {
|
|
24
|
+
throw new Error(StatusMessage.SUBJECT_REQUIRED);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!html) {
|
|
28
|
+
throw new Error(StatusMessage.HTML_CONTENT_REQUIRED);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
formatRecipients(recipients) {
|
|
35
|
+
if (!recipients) return [];
|
|
36
|
+
|
|
37
|
+
if (typeof recipients === 'string') {
|
|
38
|
+
return recipients.split(',').map(r => r.trim());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(recipients)) {
|
|
42
|
+
return recipients;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
buildMailOptions(options) {
|
|
49
|
+
const { to, cc, bcc, subject, html, from, attachments } = options;
|
|
50
|
+
|
|
51
|
+
const mailOptions = {
|
|
52
|
+
from,
|
|
53
|
+
to: this.formatRecipients(to),
|
|
54
|
+
subject,
|
|
55
|
+
html,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (cc && cc.length > 0) {
|
|
59
|
+
mailOptions.cc = this.formatRecipients(cc);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (bcc && bcc.length > 0) {
|
|
63
|
+
mailOptions.bcc = this.formatRecipients(bcc);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (attachments && attachments.length > 0) {
|
|
67
|
+
mailOptions.attachments = attachments;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return mailOptions;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = BaseProvider;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// providers/NodeMailerProvider.js
|
|
2
|
+
const nodemailer = require('nodemailer');
|
|
3
|
+
const BaseProvider = require('./BaseProvider');
|
|
4
|
+
const Config = require('../Config');
|
|
5
|
+
const logger = require('../Logger');
|
|
6
|
+
const StatusMessage = require('../constants/StatusMessageConstants');
|
|
7
|
+
const { EmailProvider } = require('../enum/EmailProvider');
|
|
8
|
+
|
|
9
|
+
class NodeMailerProvider extends BaseProvider {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.transporter = nodemailer.createTransport({
|
|
13
|
+
service: Config.emailService,
|
|
14
|
+
host: Config.emailHost,
|
|
15
|
+
port: Config.smtpPort,
|
|
16
|
+
secure: Config.smtpPort,
|
|
17
|
+
auth: {
|
|
18
|
+
user: Config.emailUser,
|
|
19
|
+
pass: Config.emailPass,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async sendEmail(options) {
|
|
25
|
+
try {
|
|
26
|
+
this.validateEmailOptions(options);
|
|
27
|
+
const mailOptions = this.buildMailOptions({
|
|
28
|
+
...options,
|
|
29
|
+
from: options.from || Config.emailUser
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await this.transporter.sendMail(mailOptions);
|
|
33
|
+
|
|
34
|
+
logger.info('Email sent successfully via Nodemailer', {
|
|
35
|
+
messageId: result.messageId,
|
|
36
|
+
recipients: { to: options.to, cc: options.cc, bcc: options.bcc },
|
|
37
|
+
tenantId: options.tenantId
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
messageId: result.messageId,
|
|
43
|
+
provider: EmailProvider.NODE_MAILER,
|
|
44
|
+
tenantId: options.tenantId
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error(`Nodemailer failed to send email: ${error.message}`);
|
|
48
|
+
throw new Error(`${StatusMessage.NODEMAILER_SEND_ERROR}: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = NodeMailerProvider;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// providers/PostmarkProvider.js
|
|
2
|
+
const postmark = require('postmark');
|
|
3
|
+
const BaseProvider = require('./BaseProvider');
|
|
4
|
+
const Config = require('../Config');
|
|
5
|
+
const logger = require('../Logger');
|
|
6
|
+
const StatusMessage = require('../constants/StatusMessageConstants');
|
|
7
|
+
const { EmailProvider } = require('../enum/EmailProvider');
|
|
8
|
+
|
|
9
|
+
class PostmarkProvider extends BaseProvider {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.client = new postmark.Client(Config.postmarkApiKey);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async sendEmail(options) {
|
|
16
|
+
try {
|
|
17
|
+
this.validateEmailOptions(options);
|
|
18
|
+
const mailOptions = {
|
|
19
|
+
From: options.from || Config.postmarkFromEmail,
|
|
20
|
+
To: this.formatRecipients(options.to).join(','),
|
|
21
|
+
Subject: options.subject,
|
|
22
|
+
HtmlBody: options.html,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (options.cc && options.cc.length > 0) {
|
|
26
|
+
mailOptions.Cc = this.formatRecipients(options.cc).join(',');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.bcc && options.bcc.length > 0) {
|
|
30
|
+
mailOptions.Bcc = this.formatRecipients(options.bcc).join(',');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
34
|
+
mailOptions.Attachments = options.attachments.map(att => ({
|
|
35
|
+
Name: att.filename,
|
|
36
|
+
Content: att.content,
|
|
37
|
+
ContentType: att.contentType
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await this.client.sendEmail(mailOptions);
|
|
42
|
+
|
|
43
|
+
logger.info('Email sent successfully via Postmark', {
|
|
44
|
+
messageId: result.MessageID,
|
|
45
|
+
recipients: { to: options.to, cc: options.cc, bcc: options.bcc },
|
|
46
|
+
tenantId: options.tenantId
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
messageId: result.MessageID,
|
|
52
|
+
provider: EmailProvider.POSTMARK,
|
|
53
|
+
tenantId: options.tenantId
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(`Postmark failed to send email: ${error.message}`);
|
|
57
|
+
throw new Error(`${StatusMessage.POSTMARK_SEND_ERROR}: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = PostmarkProvider;
|
|
@@ -1,33 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
// providers/SendgridProvider.js
|
|
2
|
+
const sgMail = require('@sendgrid/mail');
|
|
3
|
+
const BaseProvider = require('./BaseProvider');
|
|
4
|
+
const Config = require('../Config');
|
|
5
|
+
const logger = require('../Logger');
|
|
6
|
+
const { EmailProvider } = require('../enum/EmailProvider');
|
|
7
|
+
|
|
8
|
+
class SendgridProvider extends BaseProvider {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
sgMail.setApiKey(Config.sendGridApiKey);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async sendEmail({ tenantId, to, cc, bcc, subject, html, from, attachments }) {
|
|
15
|
+
try {
|
|
16
|
+
this.validateEmailOptions({ to, subject, html, from });
|
|
17
|
+
|
|
18
|
+
const mailOptions = {
|
|
19
|
+
to: this.formatRecipients(to),
|
|
20
|
+
from: Config.fromEmail,
|
|
21
|
+
subject,
|
|
22
|
+
html,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (cc && cc.length > 0) {
|
|
26
|
+
mailOptions.cc = this.formatRecipients(cc);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (bcc && bcc.length > 0) {
|
|
30
|
+
mailOptions.bcc = this.formatRecipients(bcc);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (attachments && attachments.length > 0) {
|
|
34
|
+
mailOptions.attachments = attachments;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await sgMail.send(mailOptions);
|
|
38
|
+
|
|
39
|
+
logger.info('Email sent successfully via SendGrid', {
|
|
40
|
+
messageId: result[0].headers['x-message-id'],
|
|
41
|
+
recipients: { to, cc, bcc },
|
|
42
|
+
tenantId
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
messageId: result[0].headers['x-message-id'],
|
|
48
|
+
provider: EmailProvider.SENDGRID,
|
|
49
|
+
tenantId
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error(`SendGrid failed to send email : ${error.message}`);
|
|
54
|
+
throw new Error(`${StatusMessage.SENDGRID_SEND_ERROR}: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = SendgridProvider;
|
|
60
|
+
|
package/utils/TemplateEngine.js
CHANGED
|
@@ -1,86 +1,99 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
const handlebars = require('handlebars');
|
|
2
|
+
const logger = require('../Logger');
|
|
3
|
+
|
|
4
|
+
class TemplateEngine {
|
|
5
|
+
constructor(dbClient) {
|
|
6
|
+
if (!dbClient) throw new Error(StatusMessage.DB_CLIENT_REQUIRED);
|
|
7
|
+
this.EmailTemplate = dbClient.EmailTemplate;
|
|
8
|
+
this.registerHelpers();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
registerHelpers() {
|
|
12
|
+
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
13
|
+
handlebars.registerHelper('ne', (a, b) => a !== b);
|
|
14
|
+
handlebars.registerHelper('gt', (a, b) => a > b);
|
|
15
|
+
handlebars.registerHelper('lt', (a, b) => a < b);
|
|
16
|
+
handlebars.registerHelper('uppercase', (str) => str ? str.toUpperCase() : '');
|
|
17
|
+
handlebars.registerHelper('lowercase', (str) => str ? str.toLowerCase() : '');
|
|
18
|
+
handlebars.registerHelper('formatDate', (date) => {
|
|
19
|
+
if (!date) return '';
|
|
20
|
+
return new Date(date).toLocaleDateString();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getTemplateFromDB(templateName, tenantId, langCode) {
|
|
25
|
+
try {
|
|
26
|
+
logger.info(`Fetching template: ${templateName} in language: ${langCode}`);
|
|
27
|
+
const template = await this.EmailTemplate.findOne({
|
|
28
|
+
where: {
|
|
29
|
+
TemplateName: templateName,
|
|
30
|
+
TenantId: tenantId,
|
|
31
|
+
LangCode: langCode
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!template) {
|
|
36
|
+
logger.warn(`Template "${templateName}" not found in language ${langCode}`);
|
|
37
|
+
const defaultTemplate = await this.EmailTemplate.findOne({
|
|
38
|
+
where: {
|
|
39
|
+
TemplateName: templateName,
|
|
40
|
+
TenantId: tenantId,
|
|
41
|
+
LangCode: langCode
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!defaultTemplate) {
|
|
46
|
+
throw new Error(`Template "${templateName}" not found in any language`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
subject: defaultTemplate.Subject,
|
|
51
|
+
body: defaultTemplate.Body
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
subject: template.Subject,
|
|
57
|
+
body: template.Body
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error(`Error fetching template from DB: ${error.message}`);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async compileTemplate(templateName, variables = {}, tenantId, langCode) {
|
|
66
|
+
try {
|
|
67
|
+
logger.info(`Compiling template: ${templateName} in language: ${langCode}`);
|
|
68
|
+
|
|
69
|
+
const templateData = await this.getTemplateFromDB(templateName, tenantId, langCode);
|
|
70
|
+
|
|
71
|
+
const subjectTemplate = handlebars.compile(templateData.subject);
|
|
72
|
+
const bodyTemplate = handlebars.compile(templateData.body);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
subject: subjectTemplate(variables),
|
|
76
|
+
body: bodyTemplate(variables)
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error(`Error compiling template "${templateName}": ${error.message}`);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
replacePlaceholders(content, placeholders = {}) {
|
|
85
|
+
if (!content || typeof content !== 'string') {
|
|
86
|
+
return content;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const template = handlebars.compile(content);
|
|
91
|
+
return template(placeholders);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
logger.error(`Error replacing placeholders: ${err.message}`);
|
|
94
|
+
return content;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = TemplateEngine;
|
package/EmailService.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
const TemplateEngine = require('./utils/TemplateEngine');
|
|
2
|
-
|
|
3
|
-
class EmailService {
|
|
4
|
-
constructor(options = {}) {
|
|
5
|
-
if (!options.provider) throw new Error('Email provider is required');
|
|
6
|
-
this.provider = options.provider;
|
|
7
|
-
|
|
8
|
-
// Create the template engine with custom directory if provided
|
|
9
|
-
this.templateEngine = new TemplateEngine({
|
|
10
|
-
customTemplateDir: options.customTemplateDir
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async sendTemplateEmail(templateName, to, subject, variables = {}) {
|
|
15
|
-
const html = this.templateEngine.compileTemplate(templateName, variables);
|
|
16
|
-
return this.provider.sendEmail({ to, subject, html });
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
module.exports = EmailService;
|
package/config.js
DELETED
package/logger.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
const log4js = require('log4js');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// Get log directory from environment variable or use default
|
|
7
|
-
const LOG_DIR = process.env.LOG_DIR || path.join(process.cwd(), '..', 'logs');
|
|
8
|
-
|
|
9
|
-
// Ensure the log directory exists
|
|
10
|
-
if (!fs.existsSync(LOG_DIR)) {
|
|
11
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
log4js.configure({
|
|
15
|
-
appenders: {
|
|
16
|
-
console: { type: 'console' },
|
|
17
|
-
combined: {
|
|
18
|
-
type: 'file',
|
|
19
|
-
filename: path.join(LOG_DIR, 'EmailTemplate.log'),
|
|
20
|
-
maxLogSize: 5 * 1024 * 1024, // 5MB
|
|
21
|
-
backups: 100,
|
|
22
|
-
compress: true,
|
|
23
|
-
keepFileExt: true
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
categories: { default: { appenders: ['console', 'combined'], level: 'debug' } }
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Function to get the caller file name
|
|
30
|
-
const getCallerFile = () => {
|
|
31
|
-
const originalFunc = Error.prepareStackTrace;
|
|
32
|
-
let callerFile;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const err = new Error();
|
|
36
|
-
Error.prepareStackTrace = (_, stack) => stack;
|
|
37
|
-
const stack = err.stack;
|
|
38
|
-
|
|
39
|
-
for (let i = 0; i < stack.length; i++) {
|
|
40
|
-
const fileName = stack[i].getFileName();
|
|
41
|
-
if (fileName && !fileName.includes('logger.js') && !fileName.includes('node_modules')) {
|
|
42
|
-
callerFile = path.basename(fileName);
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
callerFile = 'unknown';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
Error.prepareStackTrace = originalFunc;
|
|
51
|
-
return callerFile || 'unknown';
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Custom logger function to attach file name dynamically
|
|
55
|
-
const log = (level, message, data) => {
|
|
56
|
-
const fileName = getCallerFile();
|
|
57
|
-
log4js.getLogger(fileName)[level](message, data);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
module.exports = {
|
|
61
|
-
debug: (message, data) => log('debug', message, data),
|
|
62
|
-
info: (message, data) => log('info', message, data),
|
|
63
|
-
warn: (message, data) => log('warn', message, data),
|
|
64
|
-
error: (message, errorOrData) => {
|
|
65
|
-
let enhancedData = errorOrData;
|
|
66
|
-
|
|
67
|
-
if (errorOrData instanceof Error) {
|
|
68
|
-
const { file, line, column } = getErrorDetails(errorOrData);
|
|
69
|
-
enhancedData = {
|
|
70
|
-
error: errorOrData.message,
|
|
71
|
-
stack: errorOrData.stack,
|
|
72
|
-
location: `${file}:${line}:${column}`
|
|
73
|
-
};
|
|
74
|
-
} else if (errorOrData && errorOrData.error instanceof Error) {
|
|
75
|
-
const { file, line, column } = getErrorDetails(errorOrData.error);
|
|
76
|
-
enhancedData = { ...errorOrData, location: `${file}:${line}:${column}` };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
log('error', message, enhancedData);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
const nodemailer = require('nodemailer');
|
|
2
|
-
const BaseProvider = require('./BaseProvider');
|
|
3
|
-
const config = require('../config');
|
|
4
|
-
const logger = require('../logger');
|
|
5
|
-
|
|
6
|
-
class NodemailerProvider extends BaseProvider {
|
|
7
|
-
constructor() {
|
|
8
|
-
super();
|
|
9
|
-
this.transporter = nodemailer.createTransport({
|
|
10
|
-
service: config.emailService,
|
|
11
|
-
auth: {
|
|
12
|
-
user: config.emailUser,
|
|
13
|
-
pass: config.emailPass,
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
async sendEmail({ to, subject, html }) {
|
|
20
|
-
if (!to || !subject || !html) {
|
|
21
|
-
logger.error('Missing required email fields: to, subject, or html');
|
|
22
|
-
throw new Error('Missing required email fields: to, subject, or html');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
return await this.transporter.sendMail({
|
|
27
|
-
from: config.emailUser,
|
|
28
|
-
to,
|
|
29
|
-
subject,
|
|
30
|
-
html,
|
|
31
|
-
});
|
|
32
|
-
} catch (error) {
|
|
33
|
-
logger.error(`Nodemailer failed to send email: ${error.message}`);
|
|
34
|
-
throw new Error(`Nodemailer failed to send email: ${error.message}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
module.exports = NodemailerProvider;
|
package/server.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
const { compileTemplate } = require('./utils/TemplateEngine');
|
|
2
|
-
|
|
3
|
-
class EmailService {
|
|
4
|
-
constructor(provider) {
|
|
5
|
-
if (!provider) throw new Error('Email provider is required');
|
|
6
|
-
this.provider = provider;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async sendTemplateEmail(templateName, to, subject, variables = {}) {
|
|
10
|
-
const html = compileTemplate(templateName, variables);
|
|
11
|
-
return this.provider.sendEmail({ to, subject, html });
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
module.exports = EmailService;
|