@daitanjs/communication
Version:
An email and SMS library with background job processing.
380 lines (374 loc) • 15.2 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/communication/src/index.js
var src_exports = {};
__export(src_exports, {
composeMessageFromTemplate: () => composeMessageFromTemplate,
createMessageTemplate: () => createMessageTemplate,
replacePlaceholders: () => import_utilities2.replacePlaceholders,
sendMail: () => sendMail,
sendSMS: () => sendSMS,
sendTemplatedEmail: () => sendTemplatedEmail,
sendWhatsapp: () => sendWhatsapp
});
module.exports = __toCommonJS(src_exports);
var import_development4 = require("@daitanjs/development");
var import_utilities2 = require("@daitanjs/utilities");
// src/communication/src/email/nodemailer.js
var import_development = require("@daitanjs/development");
var import_config = require("@daitanjs/config");
var import_queues = require("@daitanjs/queues");
var import_error = require("@daitanjs/error");
var emailLogger = (0, import_development.getLogger)("daitan-comm-email-queuer");
var EMAIL_QUEUE_NAME = "mail-queue";
var SEND_EMAIL_JOB_NAME = "send-email-via-nodemailer";
var sendMail = async ({ message, config = {} }) => {
const configManager = (0, import_config.getConfigManager)();
const callId = `mail-queue-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
emailLogger.info(
`[${callId}] sendMail: Queuing email. Subject: "${String(
message?.subject
).substring(0, 50)}..."`,
{ to: message?.to }
);
if (!message || typeof message !== "object") {
throw new import_error.DaitanInvalidInputError(
"`message` object must be a non-null object."
);
}
const missingFields = [];
if (!message.to || Array.isArray(message.to) && message.to.length === 0 || typeof message.to === "string" && !message.to.trim())
missingFields.push("to");
if (!message.subject || !String(message.subject).trim())
missingFields.push("subject");
if (!message.html || !String(message.html).trim()) missingFields.push("html");
if (missingFields.length > 0) {
throw new import_error.DaitanInvalidInputError(
`Email message missing required field(s): ${missingFields.join(", ")}.`,
{ missingFields }
);
}
const finalSmtpConfig = {
host: config.host || configManager.get("SMTP_HOST"),
port: Number(config.port ?? configManager.get("SMTP_PORT", 587)),
auth: {
user: config.auth?.user || configManager.get("SMTP_USER"),
pass: config.auth?.pass || configManager.get("SMTP_PASS")
},
tls: {
rejectUnauthorized: config.tlsRejectUnauthorized ?? (0, import_development.getOptionalEnvVariable)("MAIL_TLS_REJECT_UNAUTHORIZED", "false", {
type: "boolean"
}) === true
}
};
finalSmtpConfig.secure = config.secure !== void 0 ? config.secure : finalSmtpConfig.port === 465;
let senderName = message.name || config.fromName || configManager.get("SMTP_FROM_NAME") || "DaitanJS App";
let fromEmailAddress = message.from || config.fromAddress || configManager.get("SMTP_FROM_ADDRESS") || finalSmtpConfig.auth.user;
const fromRegex = /^(.*?)<([^>]+)>$/;
const fromMatch = typeof message.from === "string" ? message.from.match(fromRegex) : null;
if (fromMatch) {
senderName = fromMatch[1].trim() || senderName;
fromEmailAddress = fromMatch[2].trim();
} else if (typeof message.from === "string") {
fromEmailAddress = message.from.trim();
}
const finalFromHeader = senderName ? `"${senderName.replace(/"/g, '\\"')}" <${fromEmailAddress}>` : fromEmailAddress;
let finalToRecipients = Array.isArray(message.to) ? message.to : [message.to];
const recipientOverride = configManager.get("MAIL_RECIPIENT_OVERRIDE");
if (configManager.get("NODE_ENV") === "development" && recipientOverride) {
emailLogger.warn(
`[${callId}] DEV MODE: Email recipients overridden to "${recipientOverride}".`
);
finalToRecipients = [recipientOverride.trim()];
message.cc = [];
message.bcc = [];
}
const mailOptions = {
from: finalFromHeader,
to: finalToRecipients.join(", "),
subject: message.subject,
html: message.html,
...message.text && { text: message.text },
...message.cc && Array.isArray(message.cc) && message.cc.length > 0 && { cc: message.cc.join(", ") },
...message.bcc && Array.isArray(message.bcc) && message.bcc.length > 0 && { bcc: message.bcc.join(", ") },
...message.attachments && { attachments: message.attachments },
...message.replyTo && { replyTo: message.replyTo },
...message.headers && { headers: message.headers }
};
const jobData = { mailOptions, smtpConfig: finalSmtpConfig, callId };
emailLogger.debug(
`[${callId}] Preparing to add email job to queue "${EMAIL_QUEUE_NAME}".`
);
try {
const job = await (0, import_queues.addJob)(EMAIL_QUEUE_NAME, SEND_EMAIL_JOB_NAME, jobData, {
attempts: 3,
backoff: { type: "exponential", delay: 5e3 }
});
emailLogger.info(
`[${callId}] Email job successfully added to queue "${EMAIL_QUEUE_NAME}" with Job ID: ${job.id}.`
);
return job;
} catch (queueError) {
throw new import_error.DaitanOperationError(
`Failed to queue email for sending: ${queueError.message}`,
{ to: message.to },
queueError
);
}
};
// src/communication/src/email/templatedMailer.js
var import_development2 = require("@daitanjs/development");
var import_html = require("@daitanjs/html");
var import_error2 = require("@daitanjs/error");
var templatedMailerLogger = (0, import_development2.getLogger)("daitan-templated-mailer");
var generateWelcomeEmailBody = (data) => {
const heading = (0, import_html.createHeading)({ text: `Welcome, ${data.name}!`, level: 1 });
const p1 = (0, import_html.createParagraph)({
text: `We're thrilled to have you on board. We're confident that our platform will help you achieve your goals.`
});
const p2 = (0, import_html.createParagraph)({
text: `To get started, please click the button below to activate your account:`
});
const button = (0, import_html.createButton)({
text: "Activate Your Account",
href: data.activationLink
});
return `${heading}${p1}${p2}<br>${button}`;
};
var generatePasswordResetEmailBody = (data) => {
const heading = (0, import_html.createHeading)({ text: `Reset Your Password`, level: 1 });
const p1 = (0, import_html.createParagraph)({
text: `Hi ${data.name}, a password reset was requested for your account.`
});
const p2 = (0, import_html.createParagraph)({
text: `If you did not request this, you can safely ignore this email. Otherwise, click the button below to set a new password:`
});
const button = (0, import_html.createButton)({ text: "Reset Password", href: data.resetLink });
const p3 = (0, import_html.createParagraph)({
text: `This link is valid for 1 hour.`,
fontSize: 12,
color: "#888888"
});
return `${heading}${p1}${p2}<br>${button}<br>${p3}`;
};
var generateNotificationEmailBody = (data) => {
let content = (0, import_html.createHeading)({
text: `Notification for ${data.name}`,
level: 2
});
if (data.alertType) {
content += (0, import_html.createAlert)({
message: data.notificationMessage,
type: data.alertType
});
} else {
content += (0, import_html.createParagraph)({ text: data.notificationMessage });
}
if (data.actionLink && data.actionText) {
content += `<br>${(0, import_html.createButton)({
text: data.actionText,
href: data.actionLink
})}`;
}
return content;
};
var templates = {
welcome: generateWelcomeEmailBody,
passwordReset: generatePasswordResetEmailBody,
notification: generateNotificationEmailBody
};
var sendTemplatedEmail = async ({
to,
subject,
templateName,
templateData,
mailerConfig = {}
}) => {
const callId = `templated-email-${templateName}-${Date.now().toString(36)}`;
templatedMailerLogger.info(`[${callId}] Preparing to send templated email.`, {
to,
templateName
});
if (!to || !subject || !templateName || !templateData) {
throw new import_error2.DaitanInvalidInputError(
"Missing required parameters: `to`, `subject`, `templateName`, and `templateData` are all required."
);
}
const templateGenerator = templates[templateName];
if (!templateGenerator) {
throw new import_error2.DaitanConfigurationError(
`Email template "${templateName}" not found. Available templates: ${Object.keys(
templates
).join(", ")}`
);
}
const bodyContent = templateGenerator(templateData);
const header = (0, import_html.createEmailHeader)({ title: subject });
const footer = (0, import_html.createEmailFooter)({
companyName: templateData.companyName || "DaitanJS Platform",
address: "123 Innovation Drive, Tech City, 12345"
});
const fullHtmlBody = `<div style="padding: 20px;">${header}${bodyContent}${footer}</div>`;
const finalHtml = (0, import_html.createEmailWrapper)({
bodyContent: fullHtmlBody,
config: {
title: subject,
previewText: typeof bodyContent === "string" ? bodyContent.substring(0, 100) : "Notification"
}
});
return sendMail({
message: { to, subject, html: finalHtml },
config: mailerConfig
});
};
// src/communication/src/sms/twilio.js
var import_twilio = __toESM(require("twilio"), 1);
var import_development3 = require("@daitanjs/development");
var import_config2 = require("@daitanjs/config");
var import_utilities = require("@daitanjs/utilities");
var import_error3 = require("@daitanjs/error");
var smsLogger = (0, import_development3.getLogger)("daitan-comm-twilio");
var twilioClientInstance = null;
var getTwilioClient = () => {
if (twilioClientInstance) {
return twilioClientInstance;
}
const configManager = (0, import_config2.getConfigManager)();
const accountSid = configManager.get("TWILIO_ACCOUNTSID");
const authToken = configManager.get("TWILIO_AUTHTOKEN");
if (!accountSid || !authToken) {
throw new import_error3.DaitanConfigurationError(
"Twilio Account SID and Auth Token must be configured in environment variables (TWILIO_ACCOUNTSID, TWILIO_AUTHTOKEN)."
);
}
twilioClientInstance = (0, import_twilio.default)(accountSid, authToken);
smsLogger.info("Twilio client initialized successfully.");
return twilioClientInstance;
};
async function sendSMS({ recipient, messageBody, from }) {
if (!recipient || !/^\+?[1-9]\d{1,14}$/.test(recipient)) {
throw new import_error3.DaitanInvalidInputError(
"Invalid recipient phone number. Must be in E.164 format."
);
}
if (!messageBody || typeof messageBody !== "string" || !messageBody.trim()) {
throw new import_error3.DaitanInvalidInputError("Message body cannot be empty.");
}
const client = getTwilioClient();
const fromNumber = from || (0, import_config2.getConfigManager)().get("TWILIO_SENDER");
if (!fromNumber) {
throw new import_error3.DaitanConfigurationError(
"Twilio sender number (TWILIO_SENDER) is not configured."
);
}
try {
const message = await client.messages.create({
to: recipient,
from: fromNumber,
body: messageBody
});
smsLogger.info(
`SMS sent successfully to ${recipient}. SID: ${message.sid}`
);
return message.sid;
} catch (error) {
smsLogger.error(`Failed to send SMS to ${recipient}: ${error.message}`);
throw new import_error3.DaitanApiError(
`Twilio API error: ${error.message}`,
"Twilio",
error.status,
{ apiErrorCode: error.code },
error
);
}
}
async function sendWhatsapp({ recipient, messageBody, from }) {
if (!recipient || !/^\+?[1-9]\d{1,14}$/.test(recipient)) {
throw new import_error3.DaitanInvalidInputError(
"Invalid recipient phone number. Must be in E.164 format."
);
}
if (!messageBody || typeof messageBody !== "string" || !messageBody.trim()) {
throw new import_error3.DaitanInvalidInputError("Message body cannot be empty.");
}
const client = getTwilioClient();
const fromNumber = from || (0, import_config2.getConfigManager)().get("TWILIO_WHATSAPP_SENDER");
if (!fromNumber) {
throw new import_error3.DaitanConfigurationError(
"Twilio WhatsApp sender ID (TWILIO_WHATSAPP_SENDER) is not configured."
);
}
try {
const message = await client.messages.create({
to: `whatsapp:${recipient}`,
from: fromNumber.startsWith("whatsapp:") ? fromNumber : `whatsapp:${fromNumber}`,
body: messageBody
});
smsLogger.info(
`WhatsApp message sent successfully to ${recipient}. SID: ${message.sid}`
);
return message.sid;
} catch (error) {
smsLogger.error(
`Failed to send WhatsApp message to ${recipient}: ${error.message}`
);
throw new import_error3.DaitanApiError(
`Twilio WhatsApp API error: ${error.message}`,
"Twilio",
error.status,
{ apiErrorCode: error.code },
error
);
}
}
var createMessageTemplate = (templateString) => {
return (placeholders) => (0, import_utilities.replacePlaceholders)({ templateString, placeholders });
};
var composeMessageFromTemplate = (templateFn, placeholders) => {
if (typeof templateFn !== "function") {
throw new import_error3.DaitanInvalidInputError(
"templateFn must be a function created with createMessageTemplate."
);
}
return templateFn(placeholders);
};
// src/communication/src/index.js
var communicationIndexLogger = (0, import_development4.getLogger)("daitan-communication-index");
communicationIndexLogger.debug("Exporting DaitanJS Communication modules...");
communicationIndexLogger.info("DaitanJS Communication module exports ready.");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
composeMessageFromTemplate,
createMessageTemplate,
replacePlaceholders,
sendMail,
sendSMS,
sendTemplatedEmail,
sendWhatsapp
});
//# sourceMappingURL=index.cjs.map