UNPKG

@daitanjs/communication

Version:

An email and SMS library with background job processing.

359 lines (354 loc) 13.1 kB
// 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