This commit is contained in:
Baobhan Sith
2025-04-26 21:58:05 +08:00
parent aa9683a8b2
commit 40ac06bf43
3 changed files with 293 additions and 240 deletions
@@ -1,101 +1,110 @@
import nodemailer from 'nodemailer'; import nodemailer from "nodemailer";
import Mail from 'nodemailer/lib/mailer'; // Import Mail type for transporter import Mail from "nodemailer/lib/mailer";
import SMTPTransport from 'nodemailer/lib/smtp-transport'; // Import SMTPTransport for options type import SMTPTransport from "nodemailer/lib/smtp-transport";
import { INotificationSender } from '../notification.dispatcher.service'; import { INotificationSender } from "../notification.dispatcher.service";
import { ProcessedNotification } from '../notification.processor.service'; import { ProcessedNotification } from "../notification.processor.service";
import { EmailConfig } from '../../types/notification.types'; import { EmailConfig } from "../../types/notification.types";
import { settingsService } from '../settings.service'; // Import settingsService import { settingsService } from "../settings.service";
class EmailSenderService implements INotificationSender { class EmailSenderService implements INotificationSender {
async send(notification: ProcessedNotification): Promise<void> { async send(notification: ProcessedNotification): Promise<void> {
const config = notification.config as EmailConfig; const config = notification.config as EmailConfig;
const { to, subjectTemplate, smtpHost, smtpPort, smtpSecure, smtpUser, smtpPass, from } = config; const { to, smtpHost, smtpPort, smtpSecure, smtpUser, smtpPass, from } =
const subject = notification.subject || 'Notification'; // Use processed subject or default config;
const body = notification.body; // Use processed body (assuming HTML) const subject = notification.subject || "Notification";
const body = notification.body;
if (!to) { if (!to) {
console.error('[EmailSender] Missing recipient address (to) in configuration.'); console.error(
throw new Error('Email configuration is incomplete (missing recipient address).'); "[EmailSender] Missing recipient address (to) in configuration."
);
throw new Error(
"Email configuration is incomplete (missing recipient address)."
);
} }
try { try {
// Get global settings for fallback SMTP configuration using settingsService const globalSmtpHost = await settingsService.getSetting("smtpHost");
const globalSmtpHost = await settingsService.getSetting('smtpHost'); const globalSmtpPortStr = await settingsService.getSetting("smtpPort");
const globalSmtpPortStr = await settingsService.getSetting('smtpPort'); const globalSmtpSecureStr = await settingsService.getSetting(
const globalSmtpSecureStr = await settingsService.getSetting('smtpSecure'); "smtpSecure"
const globalSmtpUser = await settingsService.getSetting('smtpUser'); );
const globalSmtpPass = await settingsService.getSetting('smtpPass'); const globalSmtpUser = await settingsService.getSetting("smtpUser");
const globalSmtpFrom = await settingsService.getSetting('smtpFrom'); 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 finalSmtpHost = smtpHost || globalSmtpHost;
const finalSmtpPort = smtpPort ?? (globalSmtpPortStr ? parseInt(globalSmtpPortStr, 10) : 587); // Default port 587 const finalSmtpPort =
const finalSmtpSecure = smtpSecure ?? (globalSmtpSecureStr === 'true') ?? false; // Default secure false smtpPort ?? (globalSmtpPortStr ? parseInt(globalSmtpPortStr, 10) : 587);
const finalSmtpSecure =
smtpSecure ?? globalSmtpSecureStr === "true" ?? false;
const finalSmtpUser = smtpUser || globalSmtpUser; const finalSmtpUser = smtpUser || globalSmtpUser;
const finalSmtpPass = smtpPass || globalSmtpPass; const finalSmtpPass = smtpPass || globalSmtpPass;
const finalFrom = from || globalSmtpFrom || 'noreply@nexus-terminal.local'; // Default from const finalFrom =
from || globalSmtpFrom || "noreply@nexus-terminal.local";
if (!finalSmtpHost) { if (!finalSmtpHost) {
console.error('[EmailSender] SMTP host is not configured (neither channel-specific nor global).'); console.error(
throw new Error('SMTP host configuration is missing.'); "[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) { if (isNaN(finalSmtpPort) || finalSmtpPort <= 0) {
console.error(`[EmailSender] Invalid SMTP port configured: ${finalSmtpPort}. Using default 587.`); console.error(
// finalSmtpPort = 587; // Or throw error depending on strictness needed `[EmailSender] Invalid SMTP port configured: ${finalSmtpPort}. Using default 587.`
);
throw new Error(`Invalid SMTP port configured: ${finalSmtpPort}`); throw new Error(`Invalid SMTP port configured: ${finalSmtpPort}`);
} }
// Remove explicit type annotation to let TypeScript infer the type const transporterOptions: SMTPTransport.Options = {
const transporterOptions: SMTPTransport.Options = { // Use specific SMTPTransport options type
host: finalSmtpHost, host: finalSmtpHost,
port: finalSmtpPort, port: finalSmtpPort,
secure: finalSmtpSecure, // true for 465, false for other ports secure: finalSmtpSecure,
auth: (finalSmtpUser && finalSmtpPass) ? { auth:
finalSmtpUser && finalSmtpPass
? {
user: finalSmtpUser, user: finalSmtpUser,
pass: finalSmtpPass, 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
} }
: undefined,
tls: {
rejectUnauthorized: finalSmtpSecure,
minVersion: "TLSv1.2",
},
}; };
const transporter = nodemailer.createTransport(transporterOptions); 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 = { const mailOptions: Mail.Options = {
from: `"${finalFrom.split('@')[0]}" <${finalFrom}>`, // sender address format "Sender Name <sender@example.com>" from: `"${finalFrom.split("@")[0]}" <${finalFrom}>`,
to: to, // list of receivers (comma-separated) to: to,
subject: subject, // Subject line subject: subject,
// text: 'Plain text body', // Plain text body (optional, provide if HTML is not supported well)
html: body, // html body html: body,
}; };
console.log(`[EmailSender] Sending email notification to: ${to} with subject: "${subject}"`); console.log(
`[EmailSender] Sending email notification to: ${to} with subject: "${subject}"`
);
const info = await transporter.sendMail(mailOptions); const info = await transporter.sendMail(mailOptions);
console.log(`[EmailSender] Email sent successfully. Message ID: ${info.messageId}`); console.log(
`[EmailSender] Email sent successfully. Message ID: ${info.messageId}`
);
} catch (error: any) { } catch (error: any) {
console.error(`[EmailSender] Error sending email notification to ${to}:`, error); console.error(
// Provide more specific error message if possible `[EmailSender] Error sending email notification to ${to}:`,
throw new Error(`Failed to send email notification: ${error.message || error}`); error
);
throw new Error(
`Failed to send email notification: ${error.message || error}`
);
} }
} }
} }
// Create and export singleton instance
const emailSenderService = new EmailSenderService(); const emailSenderService = new EmailSenderService();
export default emailSenderService; export default emailSenderService;
@@ -1,53 +1,78 @@
import axios from 'axios'; import axios from "axios";
import { INotificationSender } from '../notification.dispatcher.service'; // Import the interface import { INotificationSender } from "../notification.dispatcher.service";
import { ProcessedNotification } from '../notification.processor.service'; import { ProcessedNotification } from "../notification.processor.service";
import { TelegramConfig } from '../../types/notification.types'; import { TelegramConfig } from "../../types/notification.types";
class TelegramSenderService implements INotificationSender { class TelegramSenderService implements INotificationSender {
async send(notification: ProcessedNotification): Promise<void> { async send(notification: ProcessedNotification): Promise<void> {
const config = notification.config as TelegramConfig; const config = notification.config as TelegramConfig;
const { botToken, chatId } = config; const { botToken, chatId } = config;
const messageBody = notification.body; const messageBody = notification.body;
if (!botToken || !chatId) { if (!botToken || !chatId) {
console.error('[TelegramSender] Missing botToken or chatId in configuration.'); console.error(
throw new Error('Telegram configuration is incomplete (missing botToken or chatId).'); "[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`; const apiUrl = `https://api.telegram.org/bot${botToken}/sendMessage`;
try { try {
console.log(`[TelegramSender] Sending notification to chat ID: ${chatId}`); console.log(
const response = await axios.post(apiUrl, { `[TelegramSender] Sending notification to chat ID: ${chatId}`
);
const response = await axios.post(
apiUrl,
{
chat_id: chatId, chat_id: chatId,
text: messageBody, text: messageBody,
parse_mode: 'Markdown', // Use standard Markdown parse_mode: "Markdown",
disable_web_page_preview: true // Optional: disable link previews disable_web_page_preview: true,
}, { },
timeout: 10000 // Set a timeout (e.g., 10 seconds) {
}); timeout: 10000,
}
);
if (response.data && response.data.ok) { if (response.data && response.data.ok) {
console.log(`[TelegramSender] Successfully sent notification to chat ID: ${chatId}`); console.log(
`[TelegramSender] Successfully sent notification to chat ID: ${chatId}`
);
} else { } else {
// Log Telegram's error description if available const errorDescription =
const errorDescription = response.data?.description || 'Unknown error from Telegram API'; response.data?.description || "Unknown error from Telegram API";
console.error(`[TelegramSender] Failed to send notification. Telegram API response: ${errorDescription}`, response.data); console.error(
`[TelegramSender] Failed to send notification. Telegram API response: ${errorDescription}`,
response.data
);
throw new Error(`Telegram API error: ${errorDescription}`); throw new Error(`Telegram API error: ${errorDescription}`);
} }
} catch (error: any) { } catch (error: any) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.error(`[TelegramSender] Axios error sending notification: ${error.message}`, error.response?.data); console.error(
throw new Error(`Failed to send Telegram notification (Axios Error): ${error.message}`); `[TelegramSender] Axios error sending notification: ${error.message}`,
error.response?.data
);
throw new Error(
`Failed to send Telegram notification (Axios Error): ${error.message}`
);
} else { } else {
console.error(`[TelegramSender] Unexpected error sending notification:`, error); console.error(
throw new Error(`Failed to send Telegram notification (Unexpected Error): ${error.message || 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(); const telegramSenderService = new TelegramSenderService();
export default telegramSenderService; export default telegramSenderService;
@@ -1,21 +1,19 @@
import axios, { Method } from 'axios'; import axios, { Method } from "axios";
import { INotificationSender } from '../notification.dispatcher.service'; import { INotificationSender } from "../notification.dispatcher.service";
import { ProcessedNotification } from '../notification.processor.service'; import { ProcessedNotification } from "../notification.processor.service";
import { WebhookConfig } from '../../types/notification.types'; import { WebhookConfig } from "../../types/notification.types";
class WebhookSenderService implements INotificationSender { class WebhookSenderService implements INotificationSender {
async send(notification: ProcessedNotification): Promise<void> { async send(notification: ProcessedNotification): Promise<void> {
const config = notification.config as WebhookConfig; const config = notification.config as WebhookConfig;
const { url, method = 'POST', headers = {} } = config; // Default method to POST const { url, method = "POST", headers = {} } = config;
const requestBody = notification.body; // Body is already processed by the processor const requestBody = notification.body;
if (!url) { if (!url) {
console.error('[WebhookSender] Missing webhook URL in configuration.'); console.error("[WebhookSender] Missing webhook URL in configuration.");
throw new Error('Webhook configuration is incomplete (missing URL).'); throw new Error("Webhook configuration is incomplete (missing URL).");
} }
// Validate URL format (basic check)
try { try {
new URL(url); new URL(url);
} catch (e) { } catch (e) {
@@ -23,85 +21,106 @@ class WebhookSenderService implements INotificationSender {
throw new Error(`Invalid webhook URL format: ${url}`); throw new Error(`Invalid webhook URL format: ${url}`);
} }
// Prepare headers
const finalHeaders: Record<string, string> = { const finalHeaders: Record<string, string> = {
'Content-Type': 'application/json', // Default Content-Type, can be overridden by config "Content-Type": "application/json",
...headers, // Merge custom headers from config ...headers,
}; };
// Determine HTTP method const requestMethod: Method = method.toUpperCase() as Method;
const requestMethod: Method = method.toUpperCase() as Method; // Ensure method is uppercase and valid Axios Method type const validMethods: Method[] = [
const validMethods: Method[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; "GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"OPTIONS",
];
if (!validMethods.includes(requestMethod)) { if (!validMethods.includes(requestMethod)) {
console.error(`[WebhookSender] Invalid HTTP method specified: ${method}. Defaulting to POST.`); console.error(
// requestMethod = 'POST'; // Or throw an error `[WebhookSender] Invalid HTTP method specified: ${method}. Defaulting to POST.`
);
throw new Error(`Invalid HTTP method specified: ${method}`); throw new Error(`Invalid HTTP method specified: ${method}`);
} }
try { try {
console.log(`[WebhookSender] Sending ${requestMethod} notification to webhook URL: ${url}`); console.log(
`[WebhookSender] Sending ${requestMethod} notification to webhook URL: ${url}`
);
// Prepare request data based on method
let requestData: any = undefined; let requestData: any = undefined;
let requestParams: any = undefined; let requestParams: any = undefined;
// For GET requests, data is usually sent as query params. if (["POST", "PUT", "PATCH"].includes(requestMethod)) {
// For POST/PUT/PATCH, data is sent in the body. if (
// We assume the processed `requestBody` is intended for the body. finalHeaders["Content-Type"]
// If the template was designed for GET params, this might need adjustment. ?.toLowerCase()
if (['POST', 'PUT', 'PATCH'].includes(requestMethod)) { .includes("application/json")
// Try to parse body as JSON if Content-Type suggests it, otherwise send as string ) {
if (finalHeaders['Content-Type']?.toLowerCase().includes('application/json')) {
try { try {
requestData = JSON.parse(requestBody); requestData = JSON.parse(requestBody);
} catch (parseError) { } 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)}...`); 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; requestData = requestBody;
} }
} else { } else {
requestData = requestBody; requestData = requestBody;
} }
} else if (requestMethod === 'GET') { } else if (requestMethod === "GET") {
// For GET, we might need to parse the body (if it's a query string) or handle differently. console.warn(
// For simplicity now, we won't automatically convert body to params for GET. `[WebhookSender] Sending data in body for GET request might not be standard. URL: ${url}`
// 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({ const response = await axios({
method: requestMethod, method: requestMethod,
url: url, url: url,
headers: finalHeaders, headers: finalHeaders,
data: requestData, data: requestData,
params: requestParams, params: requestParams,
timeout: 15000 // Set a timeout (e.g., 15 seconds) timeout: 15000,
}); });
// Check response status (e.g., 2xx indicates success)
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
console.log(`[WebhookSender] Successfully sent notification to webhook. Status: ${response.status}`); console.log(
`[WebhookSender] Successfully sent notification to webhook. Status: ${response.status}`
);
} else { } else {
console.warn(`[WebhookSender] Webhook endpoint responded with status: ${response.status}`, response.data); console.warn(
// Consider throwing an error for non-2xx responses depending on requirements `[WebhookSender] Webhook endpoint responded with status: ${response.status}`,
// throw new Error(`Webhook endpoint responded with status: ${response.status}`); response.data
);
} }
} catch (error: any) { } catch (error: any) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.error(`[WebhookSender] Axios error sending notification to ${url}: ${error.message}`, error.response?.status, error.response?.data); console.error(
throw new Error(`Failed to send webhook notification (Axios Error): ${error.message}`); `[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 { } else {
console.error(`[WebhookSender] Unexpected error sending notification to ${url}:`, error); console.error(
throw new Error(`Failed to send webhook notification (Unexpected Error): ${error.message || 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(); const webhookSenderService = new WebhookSenderService();
export default webhookSenderService; export default webhookSenderService;