UNPKG

@daitanjs/communication

Version:

An email and SMS library with background job processing.

380 lines (374 loc) 15.2 kB
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