@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.
@@ -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
@@ -0,0 +1,9 @@
1
+ // config.js
2
+ require('dotenv').config();
3
+
4
+ module.exports = {
5
+ emailService: process.env.EMAIL_SERVICE,
6
+ emailUser: process.env.EMAIL_USER,
7
+ emailPass: process.env.EMAIL_PASS,
8
+ sendGridApiKey: process.env.SENDGRID_API_KEY,
9
+ };
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,10 @@
1
+ const logger = require('../logger');
2
+
3
+ class BaseProvider {
4
+ async sendEmail(options) {
5
+ logger.error('sendEmail method not implemented in BaseProvider');
6
+ throw new Error('sendEmail method not implemented');
7
+ }
8
+ }
9
+
10
+ module.exports = BaseProvider;
@@ -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,7 @@
1
+ <p>Dear User,</p>
2
+ <p>Your OTP for password reset is: <strong>{{otp}}</strong></p>
3
+ <p>This OTP is valid for 10 minutes.</p>
4
+ <p>If you did not request this, please ignore this email.</p>
5
+ <br>
6
+ <p>Best Regards,</p>
7
+ <p>TestCompany</p>
@@ -0,0 +1,6 @@
1
+ <html>
2
+ <body>
3
+ <h1>Welcome, {{name}}!</h1>
4
+ <p>Thanks for joining {{company}}.</p>
5
+ </body>
6
+ </html>
@@ -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;