diff --git a/packages/backend/src/services/senders/email.sender.service.ts b/packages/backend/src/services/senders/email.sender.service.ts index bb0dd8b..b57ffcb 100644 --- a/packages/backend/src/services/senders/email.sender.service.ts +++ b/packages/backend/src/services/senders/email.sender.service.ts @@ -1,101 +1,110 @@ -import nodemailer from 'nodemailer'; -import Mail from 'nodemailer/lib/mailer'; // Import Mail type for transporter -import SMTPTransport from 'nodemailer/lib/smtp-transport'; // Import SMTPTransport for options type -import { INotificationSender } from '../notification.dispatcher.service'; -import { ProcessedNotification } from '../notification.processor.service'; -import { EmailConfig } from '../../types/notification.types'; -import { settingsService } from '../settings.service'; // Import settingsService +import nodemailer from "nodemailer"; +import Mail from "nodemailer/lib/mailer"; +import SMTPTransport from "nodemailer/lib/smtp-transport"; +import { INotificationSender } from "../notification.dispatcher.service"; +import { ProcessedNotification } from "../notification.processor.service"; +import { EmailConfig } from "../../types/notification.types"; +import { settingsService } from "../settings.service"; class EmailSenderService implements INotificationSender { + async send(notification: ProcessedNotification): Promise { + const config = notification.config as EmailConfig; + const { to, smtpHost, smtpPort, smtpSecure, smtpUser, smtpPass, from } = + config; + const subject = notification.subject || "Notification"; + const body = notification.body; - async send(notification: ProcessedNotification): Promise { - const config = notification.config as EmailConfig; - const { to, subjectTemplate, smtpHost, smtpPort, smtpSecure, smtpUser, smtpPass, from } = config; - const subject = notification.subject || 'Notification'; // Use processed subject or default - const body = notification.body; // Use processed body (assuming HTML) - - if (!to) { - console.error('[EmailSender] Missing recipient address (to) in configuration.'); - throw new Error('Email configuration is incomplete (missing recipient address).'); - } - - try { - // Get global settings for fallback SMTP configuration using settingsService - const globalSmtpHost = await settingsService.getSetting('smtpHost'); - const globalSmtpPortStr = await settingsService.getSetting('smtpPort'); - const globalSmtpSecureStr = await settingsService.getSetting('smtpSecure'); - const globalSmtpUser = await settingsService.getSetting('smtpUser'); - const globalSmtpPass = await settingsService.getSetting('smtpPass'); - const globalSmtpFrom = await settingsService.getSetting('smtpFrom'); - - // Determine SMTP settings: prioritize channel-specific, then global, then defaults - const finalSmtpHost = smtpHost || globalSmtpHost; - const finalSmtpPort = smtpPort ?? (globalSmtpPortStr ? parseInt(globalSmtpPortStr, 10) : 587); // Default port 587 - const finalSmtpSecure = smtpSecure ?? (globalSmtpSecureStr === 'true') ?? false; // Default secure false - const finalSmtpUser = smtpUser || globalSmtpUser; - const finalSmtpPass = smtpPass || globalSmtpPass; - const finalFrom = from || globalSmtpFrom || 'noreply@nexus-terminal.local'; // Default from - - if (!finalSmtpHost) { - console.error('[EmailSender] SMTP host is not configured (neither channel-specific nor global).'); - throw new Error('SMTP host configuration is missing.'); - } - // Basic validation for port - if (isNaN(finalSmtpPort) || finalSmtpPort <= 0) { - console.error(`[EmailSender] Invalid SMTP port configured: ${finalSmtpPort}. Using default 587.`); - // finalSmtpPort = 587; // Or throw error depending on strictness needed - throw new Error(`Invalid SMTP port configured: ${finalSmtpPort}`); - } - - // Remove explicit type annotation to let TypeScript infer the type - const transporterOptions: SMTPTransport.Options = { // Use specific SMTPTransport options type - host: finalSmtpHost, - port: finalSmtpPort, - secure: finalSmtpSecure, // true for 465, false for other ports - auth: (finalSmtpUser && finalSmtpPass) ? { - user: finalSmtpUser, - pass: finalSmtpPass, - } : undefined, // Only include auth if user/pass are provided - tls: { - // rejectUnauthorized should be within the tls object according to types - rejectUnauthorized: finalSmtpSecure, - // minVersion is also a valid TLS option - minVersion: 'TLSv1.2' // Explicitly require TLSv1.2 or higher for Gmail compatibility - } - }; - - const transporter = nodemailer.createTransport(transporterOptions); - - // Verify connection configuration (optional but recommended) - // try { - // await transporter.verify(); - // console.log('[EmailSender] SMTP configuration verified successfully.'); - // } catch (verifyError) { - // console.error('[EmailSender] SMTP configuration verification failed:', verifyError); - // throw new Error(`SMTP verification failed: ${verifyError.message}`); - // } - - - const mailOptions: Mail.Options = { - from: `"${finalFrom.split('@')[0]}" <${finalFrom}>`, // sender address format "Sender Name " - to: to, // list of receivers (comma-separated) - subject: subject, // Subject line - // text: 'Plain text body', // Plain text body (optional, provide if HTML is not supported well) - html: body, // html body - }; - - console.log(`[EmailSender] Sending email notification to: ${to} with subject: "${subject}"`); - const info = await transporter.sendMail(mailOptions); - console.log(`[EmailSender] Email sent successfully. Message ID: ${info.messageId}`); - - } catch (error: any) { - console.error(`[EmailSender] Error sending email notification to ${to}:`, error); - // Provide more specific error message if possible - throw new Error(`Failed to send email notification: ${error.message || error}`); - } + if (!to) { + console.error( + "[EmailSender] Missing recipient address (to) in configuration." + ); + throw new Error( + "Email configuration is incomplete (missing recipient address)." + ); } + + try { + const globalSmtpHost = await settingsService.getSetting("smtpHost"); + const globalSmtpPortStr = await settingsService.getSetting("smtpPort"); + const globalSmtpSecureStr = await settingsService.getSetting( + "smtpSecure" + ); + const globalSmtpUser = await settingsService.getSetting("smtpUser"); + const globalSmtpPass = await settingsService.getSetting("smtpPass"); + const globalSmtpFrom = await settingsService.getSetting("smtpFrom"); + + const finalSmtpHost = smtpHost || globalSmtpHost; + const finalSmtpPort = + smtpPort ?? (globalSmtpPortStr ? parseInt(globalSmtpPortStr, 10) : 587); + const finalSmtpSecure = + smtpSecure ?? globalSmtpSecureStr === "true" ?? false; + const finalSmtpUser = smtpUser || globalSmtpUser; + const finalSmtpPass = smtpPass || globalSmtpPass; + const finalFrom = + from || globalSmtpFrom || "noreply@nexus-terminal.local"; + + if (!finalSmtpHost) { + console.error( + "[EmailSender] SMTP host is not configured (neither channel-specific nor global)." + ); + throw new Error("SMTP host configuration is missing."); + } + + if (isNaN(finalSmtpPort) || finalSmtpPort <= 0) { + console.error( + `[EmailSender] Invalid SMTP port configured: ${finalSmtpPort}. Using default 587.` + ); + + throw new Error(`Invalid SMTP port configured: ${finalSmtpPort}`); + } + + const transporterOptions: SMTPTransport.Options = { + host: finalSmtpHost, + port: finalSmtpPort, + secure: finalSmtpSecure, + auth: + finalSmtpUser && finalSmtpPass + ? { + user: finalSmtpUser, + pass: finalSmtpPass, + } + : undefined, + tls: { + rejectUnauthorized: finalSmtpSecure, + + minVersion: "TLSv1.2", + }, + }; + + const transporter = nodemailer.createTransport(transporterOptions); + + const mailOptions: Mail.Options = { + from: `"${finalFrom.split("@")[0]}" <${finalFrom}>`, + to: to, + subject: subject, + + html: body, + }; + + console.log( + `[EmailSender] Sending email notification to: ${to} with subject: "${subject}"` + ); + const info = await transporter.sendMail(mailOptions); + console.log( + `[EmailSender] Email sent successfully. Message ID: ${info.messageId}` + ); + } catch (error: any) { + console.error( + `[EmailSender] Error sending email notification to ${to}:`, + error + ); + + throw new Error( + `Failed to send email notification: ${error.message || error}` + ); + } + } } -// Create and export singleton instance const emailSenderService = new EmailSenderService(); -export default emailSenderService; \ No newline at end of file +export default emailSenderService; diff --git a/packages/backend/src/services/senders/telegram.sender.service.ts b/packages/backend/src/services/senders/telegram.sender.service.ts index 5eab2d4..75b51b9 100644 --- a/packages/backend/src/services/senders/telegram.sender.service.ts +++ b/packages/backend/src/services/senders/telegram.sender.service.ts @@ -1,53 +1,78 @@ -import axios from 'axios'; -import { INotificationSender } from '../notification.dispatcher.service'; // Import the interface -import { ProcessedNotification } from '../notification.processor.service'; -import { TelegramConfig } from '../../types/notification.types'; +import axios from "axios"; +import { INotificationSender } from "../notification.dispatcher.service"; +import { ProcessedNotification } from "../notification.processor.service"; +import { TelegramConfig } from "../../types/notification.types"; class TelegramSenderService implements INotificationSender { + async send(notification: ProcessedNotification): Promise { + const config = notification.config as TelegramConfig; + const { botToken, chatId } = config; + const messageBody = notification.body; - async send(notification: ProcessedNotification): Promise { - const config = notification.config as TelegramConfig; - const { botToken, chatId } = config; - const messageBody = notification.body; - - if (!botToken || !chatId) { - console.error('[TelegramSender] Missing botToken or chatId in configuration.'); - throw new Error('Telegram configuration is incomplete (missing botToken or chatId).'); - } - - const apiUrl = `https://api.telegram.org/bot${botToken}/sendMessage`; - - try { - console.log(`[TelegramSender] Sending notification to chat ID: ${chatId}`); - const response = await axios.post(apiUrl, { - chat_id: chatId, - text: messageBody, - parse_mode: 'Markdown', // Use standard Markdown - disable_web_page_preview: true // Optional: disable link previews - }, { - timeout: 10000 // Set a timeout (e.g., 10 seconds) - }); - - if (response.data && response.data.ok) { - console.log(`[TelegramSender] Successfully sent notification to chat ID: ${chatId}`); - } else { - // Log Telegram's error description if available - const errorDescription = response.data?.description || 'Unknown error from Telegram API'; - console.error(`[TelegramSender] Failed to send notification. Telegram API response: ${errorDescription}`, response.data); - throw new Error(`Telegram API error: ${errorDescription}`); - } - } catch (error: any) { - if (axios.isAxiosError(error)) { - console.error(`[TelegramSender] Axios error sending notification: ${error.message}`, error.response?.data); - throw new Error(`Failed to send Telegram notification (Axios Error): ${error.message}`); - } else { - console.error(`[TelegramSender] Unexpected error sending notification:`, error); - throw new Error(`Failed to send Telegram notification (Unexpected Error): ${error.message || error}`); - } - } + if (!botToken || !chatId) { + console.error( + "[TelegramSender] Missing botToken or chatId in configuration." + ); + throw new Error( + "Telegram configuration is incomplete (missing botToken or chatId)." + ); } + + const apiUrl = `https://api.telegram.org/bot${botToken}/sendMessage`; + + try { + console.log( + `[TelegramSender] Sending notification to chat ID: ${chatId}` + ); + const response = await axios.post( + apiUrl, + { + chat_id: chatId, + text: messageBody, + parse_mode: "Markdown", + disable_web_page_preview: true, + }, + { + timeout: 10000, + } + ); + + if (response.data && response.data.ok) { + console.log( + `[TelegramSender] Successfully sent notification to chat ID: ${chatId}` + ); + } else { + const errorDescription = + response.data?.description || "Unknown error from Telegram API"; + console.error( + `[TelegramSender] Failed to send notification. Telegram API response: ${errorDescription}`, + response.data + ); + throw new Error(`Telegram API error: ${errorDescription}`); + } + } catch (error: any) { + if (axios.isAxiosError(error)) { + console.error( + `[TelegramSender] Axios error sending notification: ${error.message}`, + error.response?.data + ); + throw new Error( + `Failed to send Telegram notification (Axios Error): ${error.message}` + ); + } else { + console.error( + `[TelegramSender] Unexpected error sending notification:`, + error + ); + throw new Error( + `Failed to send Telegram notification (Unexpected Error): ${ + error.message || error + }` + ); + } + } + } } -// Create and export singleton instance const telegramSenderService = new TelegramSenderService(); -export default telegramSenderService; \ No newline at end of file +export default telegramSenderService; diff --git a/packages/backend/src/services/senders/webhook.sender.service.ts b/packages/backend/src/services/senders/webhook.sender.service.ts index e9be14e..ee5604b 100644 --- a/packages/backend/src/services/senders/webhook.sender.service.ts +++ b/packages/backend/src/services/senders/webhook.sender.service.ts @@ -1,107 +1,126 @@ -import axios, { Method } from 'axios'; -import { INotificationSender } from '../notification.dispatcher.service'; -import { ProcessedNotification } from '../notification.processor.service'; -import { WebhookConfig } from '../../types/notification.types'; +import axios, { Method } from "axios"; +import { INotificationSender } from "../notification.dispatcher.service"; +import { ProcessedNotification } from "../notification.processor.service"; +import { WebhookConfig } from "../../types/notification.types"; class WebhookSenderService implements INotificationSender { + async send(notification: ProcessedNotification): Promise { + const config = notification.config as WebhookConfig; + const { url, method = "POST", headers = {} } = config; + const requestBody = notification.body; - async send(notification: ProcessedNotification): Promise { - const config = notification.config as WebhookConfig; - const { url, method = 'POST', headers = {} } = config; // Default method to POST - const requestBody = notification.body; // Body is already processed by the processor - - if (!url) { - console.error('[WebhookSender] Missing webhook URL in configuration.'); - throw new Error('Webhook configuration is incomplete (missing URL).'); - } - - // Validate URL format (basic check) - try { - new URL(url); - } catch (e) { - console.error(`[WebhookSender] Invalid webhook URL format: ${url}`); - throw new Error(`Invalid webhook URL format: ${url}`); - } - - // Prepare headers - const finalHeaders: Record = { - 'Content-Type': 'application/json', // Default Content-Type, can be overridden by config - ...headers, // Merge custom headers from config - }; - - // Determine HTTP method - const requestMethod: Method = method.toUpperCase() as Method; // Ensure method is uppercase and valid Axios Method type - const validMethods: Method[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; - if (!validMethods.includes(requestMethod)) { - console.error(`[WebhookSender] Invalid HTTP method specified: ${method}. Defaulting to POST.`); - // requestMethod = 'POST'; // Or throw an error - throw new Error(`Invalid HTTP method specified: ${method}`); - } - - - try { - console.log(`[WebhookSender] Sending ${requestMethod} notification to webhook URL: ${url}`); - - // Prepare request data based on method - let requestData: any = undefined; - let requestParams: any = undefined; - - // For GET requests, data is usually sent as query params. - // For POST/PUT/PATCH, data is sent in the body. - // We assume the processed `requestBody` is intended for the body. - // If the template was designed for GET params, this might need adjustment. - if (['POST', 'PUT', 'PATCH'].includes(requestMethod)) { - // Try to parse body as JSON if Content-Type suggests it, otherwise send as string - if (finalHeaders['Content-Type']?.toLowerCase().includes('application/json')) { - try { - requestData = JSON.parse(requestBody); - } catch (parseError) { - console.warn(`[WebhookSender] Failed to parse request body as JSON for Content-Type application/json. Sending as raw string. Body: ${requestBody.substring(0,100)}...`); - requestData = requestBody; - } - } else { - requestData = requestBody; - } - } else if (requestMethod === 'GET') { - // For GET, we might need to parse the body (if it's a query string) or handle differently. - // For simplicity now, we won't automatically convert body to params for GET. - // User should configure GET webhooks appropriately (e.g., URL includes params). - console.warn(`[WebhookSender] Sending data in body for GET request might not be standard. URL: ${url}`); - // If requestBody is intended as query params, parsing logic would be needed here. - // requestParams = querystring.parse(requestBody); // Example - } - - - const response = await axios({ - method: requestMethod, - url: url, - headers: finalHeaders, - data: requestData, - params: requestParams, - timeout: 15000 // Set a timeout (e.g., 15 seconds) - }); - - // Check response status (e.g., 2xx indicates success) - if (response.status >= 200 && response.status < 300) { - console.log(`[WebhookSender] Successfully sent notification to webhook. Status: ${response.status}`); - } else { - console.warn(`[WebhookSender] Webhook endpoint responded with status: ${response.status}`, response.data); - // Consider throwing an error for non-2xx responses depending on requirements - // throw new Error(`Webhook endpoint responded with status: ${response.status}`); - } - - } catch (error: any) { - if (axios.isAxiosError(error)) { - console.error(`[WebhookSender] Axios error sending notification to ${url}: ${error.message}`, error.response?.status, error.response?.data); - throw new Error(`Failed to send webhook notification (Axios Error): ${error.message}`); - } else { - console.error(`[WebhookSender] Unexpected error sending notification to ${url}:`, error); - throw new Error(`Failed to send webhook notification (Unexpected Error): ${error.message || error}`); - } - } + if (!url) { + console.error("[WebhookSender] Missing webhook URL in configuration."); + throw new Error("Webhook configuration is incomplete (missing URL)."); } + + try { + new URL(url); + } catch (e) { + console.error(`[WebhookSender] Invalid webhook URL format: ${url}`); + throw new Error(`Invalid webhook URL format: ${url}`); + } + + const finalHeaders: Record = { + "Content-Type": "application/json", + ...headers, + }; + + const requestMethod: Method = method.toUpperCase() as Method; + const validMethods: Method[] = [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + ]; + if (!validMethods.includes(requestMethod)) { + console.error( + `[WebhookSender] Invalid HTTP method specified: ${method}. Defaulting to POST.` + ); + + throw new Error(`Invalid HTTP method specified: ${method}`); + } + + try { + console.log( + `[WebhookSender] Sending ${requestMethod} notification to webhook URL: ${url}` + ); + + let requestData: any = undefined; + let requestParams: any = undefined; + + if (["POST", "PUT", "PATCH"].includes(requestMethod)) { + if ( + finalHeaders["Content-Type"] + ?.toLowerCase() + .includes("application/json") + ) { + try { + requestData = JSON.parse(requestBody); + } catch (parseError) { + console.warn( + `[WebhookSender] Failed to parse request body as JSON for Content-Type application/json. Sending as raw string. Body: ${requestBody.substring( + 0, + 100 + )}...` + ); + requestData = requestBody; + } + } else { + requestData = requestBody; + } + } else if (requestMethod === "GET") { + console.warn( + `[WebhookSender] Sending data in body for GET request might not be standard. URL: ${url}` + ); + } + + const response = await axios({ + method: requestMethod, + url: url, + headers: finalHeaders, + data: requestData, + params: requestParams, + timeout: 15000, + }); + + if (response.status >= 200 && response.status < 300) { + console.log( + `[WebhookSender] Successfully sent notification to webhook. Status: ${response.status}` + ); + } else { + console.warn( + `[WebhookSender] Webhook endpoint responded with status: ${response.status}`, + response.data + ); + } + } catch (error: any) { + if (axios.isAxiosError(error)) { + console.error( + `[WebhookSender] Axios error sending notification to ${url}: ${error.message}`, + error.response?.status, + error.response?.data + ); + throw new Error( + `Failed to send webhook notification (Axios Error): ${error.message}` + ); + } else { + console.error( + `[WebhookSender] Unexpected error sending notification to ${url}:`, + error + ); + throw new Error( + `Failed to send webhook notification (Unexpected Error): ${ + error.message || error + }` + ); + } + } + } } -// Create and export singleton instance const webhookSenderService = new WebhookSenderService(); -export default webhookSenderService; \ No newline at end of file +export default webhookSenderService;