@b2y/email-service 1.0.0
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/EmailService.js +20 -0
- package/README.md +18 -0
- package/config.js +9 -0
- package/index.js +23 -0
- package/logger.js +81 -0
- package/package.json +43 -0
- package/providers/BaseProvider.js +10 -0
- package/providers/NodemailerProvider.js +40 -0
- package/providers/SendgridProvider.js +33 -0
- package/server.js +15 -0
- package/templates/resetPassword.html +7 -0
- package/templates/welcome.html +6 -0
- package/utils/TemplateEngine.js +86 -0
package/EmailService.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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/README.md
ADDED
|
@@ -0,0 +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
|
package/config.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const BaseProvider = require('./providers/BaseProvider');
|
|
2
|
+
const NodemailerProvider = require('./providers/NodemailerProvider');
|
|
3
|
+
const SendGridProvider = require('./providers/SendGridProvider');
|
|
4
|
+
const TemplateEngine = require('./utils/TemplateEngine');
|
|
5
|
+
|
|
6
|
+
// Create a singleton instance of TemplateEngine for convenience
|
|
7
|
+
const templateEngine = new TemplateEngine();
|
|
8
|
+
|
|
9
|
+
const EmailService = require('./EmailService');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
// Providers
|
|
13
|
+
BaseProvider,
|
|
14
|
+
NodemailerProvider,
|
|
15
|
+
SendGridProvider,
|
|
16
|
+
|
|
17
|
+
// Template utilities
|
|
18
|
+
TemplateEngine, // Export the class itself
|
|
19
|
+
templateEngine, // Export a singleton instance
|
|
20
|
+
|
|
21
|
+
// Service
|
|
22
|
+
EmailService
|
|
23
|
+
};
|
package/logger.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@b2y/email-service",
|
|
3
|
+
"version": "1.0.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/NodemailerProvider.js",
|
|
10
|
+
"./providers/sendgrid": "./providers/SendGridProvider.js",
|
|
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
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@sendgrid/mail": "^8.1.5",
|
|
38
|
+
"dotenv": "^16.5.0",
|
|
39
|
+
"handlebars": "^4.7.8",
|
|
40
|
+
"log4js": "^6.9.1",
|
|
41
|
+
"nodemailer": "^6.10.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const sgMail = require('@sendgrid/mail');
|
|
2
|
+
const BaseProvider = require('./BaseProvider');
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const logger = require('../logger');
|
|
5
|
+
|
|
6
|
+
class SendGridProvider extends BaseProvider {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
sgMail.setApiKey(config.sendGridApiKey);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async sendEmail({ to, subject, html }) {
|
|
13
|
+
if (!to || !subject || !html) {
|
|
14
|
+
logger.error('Missing required email fields: to, subject, or html');
|
|
15
|
+
throw new Error('Missing required email fields: to, subject, or html');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return await sgMail.send({
|
|
20
|
+
to,
|
|
21
|
+
from: config.emailUser,
|
|
22
|
+
subject,
|
|
23
|
+
html,
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error(`SendGrid failed to send email: ${error.message}`);
|
|
27
|
+
throw new Error(`SendGrid failed to send email: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = SendGridProvider;
|
package/server.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const handlebars = require('handlebars');
|
|
4
|
+
const logger = require('../logger');
|
|
5
|
+
|
|
6
|
+
class TemplateEngine {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
// Default templates directory in the module
|
|
9
|
+
this.defaultTemplateDir = path.join(__dirname, '..', 'templates');
|
|
10
|
+
|
|
11
|
+
// Custom templates directory provided by the user
|
|
12
|
+
this.customTemplateDir = options.customTemplateDir || null;
|
|
13
|
+
|
|
14
|
+
// Array to store additional template directories
|
|
15
|
+
this.additionalTemplateDirs = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Add a new template directory to search for templates
|
|
19
|
+
addTemplateDirectory(dirPath, prepend = false) {
|
|
20
|
+
if (!fs.existsSync(dirPath)) {
|
|
21
|
+
logger.warn(`Template directory does not exist: ${dirPath}`);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (prepend) {
|
|
26
|
+
this.additionalTemplateDirs.unshift(dirPath);
|
|
27
|
+
} else {
|
|
28
|
+
this.additionalTemplateDirs.push(dirPath);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
logger.info(`Added template directory: ${dirPath}`);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Reset all additional template directories
|
|
36
|
+
resetTemplateDirectories() {
|
|
37
|
+
this.additionalTemplateDirs = [];
|
|
38
|
+
logger.info('Reset additional template directories');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
compileTemplate(templateName, variables) {
|
|
42
|
+
let templateContent;
|
|
43
|
+
let templatePath;
|
|
44
|
+
|
|
45
|
+
// First try to find the template in the additional directories
|
|
46
|
+
for (const dir of this.additionalTemplateDirs) {
|
|
47
|
+
templatePath = path.join(dir, `${templateName}.html`);
|
|
48
|
+
if (fs.existsSync(templatePath)) {
|
|
49
|
+
templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
50
|
+
logger.info(`Using template from additional directory: ${templatePath}`);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Then try the custom directory if no template found yet
|
|
56
|
+
if (!templateContent && this.customTemplateDir) {
|
|
57
|
+
templatePath = path.join(this.customTemplateDir, `${templateName}.html`);
|
|
58
|
+
if (fs.existsSync(templatePath)) {
|
|
59
|
+
templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
60
|
+
logger.info(`Using custom template: ${templatePath}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Finally try the default directory if still no template found
|
|
65
|
+
if (!templateContent) {
|
|
66
|
+
templatePath = path.join(this.defaultTemplateDir, `${templateName}.html`);
|
|
67
|
+
if (fs.existsSync(templatePath)) {
|
|
68
|
+
templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
69
|
+
logger.info(`Using default template: ${templatePath}`);
|
|
70
|
+
} else {
|
|
71
|
+
logger.error(`Template "${templateName}" not found in any configured template directory`);
|
|
72
|
+
throw new Error(`Template "${templateName}" not found`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const template = handlebars.compile(templateContent);
|
|
78
|
+
return template(variables);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.error(`Error compiling template "${templateName}": ${err.message}`);
|
|
81
|
+
throw new Error(`Error compiling template "${templateName}": ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = TemplateEngine;
|