@daitanjs/communication
Version:
An email and SMS library with background job processing.
359 lines (354 loc) • 13.1 kB
JavaScript
// src/communication/src/index.js
import { getLogger as getLogger4 } from "@daitanjs/development";
import { replacePlaceholders as replacePlaceholders2 } from "@daitanjs/utilities";
// src/communication/src/email/nodemailer.js
import { getLogger, getOptionalEnvVariable } from "@daitanjs/development";
import { getConfigManager } from "@daitanjs/config";
import { addJob } from "@daitanjs/queues";
import {
DaitanConfigurationError,
DaitanOperationError,
DaitanInvalidInputError
} from "@daitanjs/error";
var emailLogger = 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 = 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 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 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 ?? 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 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 DaitanOperationError(
`Failed to queue email for sending: ${queueError.message}`,
{ to: message.to },
queueError
);
}
};
// src/communication/src/email/templatedMailer.js
import { getLogger as getLogger2 } from "@daitanjs/development";
import {
createEmailWrapper,
createEmailHeader,
createEmailFooter,
createHeading,
createParagraph,
createButton,
createAlert
} from "@daitanjs/html";
import {
DaitanConfigurationError as DaitanConfigurationError2,
DaitanInvalidInputError as DaitanInvalidInputError2
} from "@daitanjs/error";
var templatedMailerLogger = getLogger2("daitan-templated-mailer");
var generateWelcomeEmailBody = (data) => {
const heading = createHeading({ text: `Welcome, ${data.name}!`, level: 1 });
const p1 = createParagraph({
text: `We're thrilled to have you on board. We're confident that our platform will help you achieve your goals.`
});
const p2 = createParagraph({
text: `To get started, please click the button below to activate your account:`
});
const button = createButton({
text: "Activate Your Account",
href: data.activationLink
});
return `${heading}${p1}${p2}<br>${button}`;
};
var generatePasswordResetEmailBody = (data) => {
const heading = createHeading({ text: `Reset Your Password`, level: 1 });
const p1 = createParagraph({
text: `Hi ${data.name}, a password reset was requested for your account.`
});
const p2 = 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 = createButton({ text: "Reset Password", href: data.resetLink });
const p3 = 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 = createHeading({
text: `Notification for ${data.name}`,
level: 2
});
if (data.alertType) {
content += createAlert({
message: data.notificationMessage,
type: data.alertType
});
} else {
content += createParagraph({ text: data.notificationMessage });
}
if (data.actionLink && data.actionText) {
content += `<br>${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 DaitanInvalidInputError2(
"Missing required parameters: `to`, `subject`, `templateName`, and `templateData` are all required."
);
}
const templateGenerator = templates[templateName];
if (!templateGenerator) {
throw new DaitanConfigurationError2(
`Email template "${templateName}" not found. Available templates: ${Object.keys(
templates
).join(", ")}`
);
}
const bodyContent = templateGenerator(templateData);
const header = createEmailHeader({ title: subject });
const footer = 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 = 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
import twilio from "twilio";
import { getLogger as getLogger3 } from "@daitanjs/development";
import { getConfigManager as getConfigManager2 } from "@daitanjs/config";
import { replacePlaceholders } from "@daitanjs/utilities";
import {
DaitanConfigurationError as DaitanConfigurationError3,
DaitanApiError,
DaitanInvalidInputError as DaitanInvalidInputError3
} from "@daitanjs/error";
var smsLogger = getLogger3("daitan-comm-twilio");
var twilioClientInstance = null;
var getTwilioClient = () => {
if (twilioClientInstance) {
return twilioClientInstance;
}
const configManager = getConfigManager2();
const accountSid = configManager.get("TWILIO_ACCOUNTSID");
const authToken = configManager.get("TWILIO_AUTHTOKEN");
if (!accountSid || !authToken) {
throw new DaitanConfigurationError3(
"Twilio Account SID and Auth Token must be configured in environment variables (TWILIO_ACCOUNTSID, TWILIO_AUTHTOKEN)."
);
}
twilioClientInstance = twilio(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 DaitanInvalidInputError3(
"Invalid recipient phone number. Must be in E.164 format."
);
}
if (!messageBody || typeof messageBody !== "string" || !messageBody.trim()) {
throw new DaitanInvalidInputError3("Message body cannot be empty.");
}
const client = getTwilioClient();
const fromNumber = from || getConfigManager2().get("TWILIO_SENDER");
if (!fromNumber) {
throw new DaitanConfigurationError3(
"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 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 DaitanInvalidInputError3(
"Invalid recipient phone number. Must be in E.164 format."
);
}
if (!messageBody || typeof messageBody !== "string" || !messageBody.trim()) {
throw new DaitanInvalidInputError3("Message body cannot be empty.");
}
const client = getTwilioClient();
const fromNumber = from || getConfigManager2().get("TWILIO_WHATSAPP_SENDER");
if (!fromNumber) {
throw new DaitanConfigurationError3(
"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 DaitanApiError(
`Twilio WhatsApp API error: ${error.message}`,
"Twilio",
error.status,
{ apiErrorCode: error.code },
error
);
}
}
var createMessageTemplate = (templateString) => {
return (placeholders) => replacePlaceholders({ templateString, placeholders });
};
var composeMessageFromTemplate = (templateFn, placeholders) => {
if (typeof templateFn !== "function") {
throw new DaitanInvalidInputError3(
"templateFn must be a function created with createMessageTemplate."
);
}
return templateFn(placeholders);
};
// src/communication/src/index.js
var communicationIndexLogger = getLogger4("daitan-communication-index");
communicationIndexLogger.debug("Exporting DaitanJS Communication modules...");
communicationIndexLogger.info("DaitanJS Communication module exports ready.");
export {
composeMessageFromTemplate,
createMessageTemplate,
replacePlaceholders2 as replacePlaceholders,
sendMail,
sendSMS,
sendTemplatedEmail,
sendWhatsapp
};
//# sourceMappingURL=index.js.map