diff --git a/packages/backend/src/services/notification.processor.service.ts b/packages/backend/src/services/notification.processor.service.ts index df81b2a..2de2de8 100644 --- a/packages/backend/src/services/notification.processor.service.ts +++ b/packages/backend/src/services/notification.processor.service.ts @@ -215,13 +215,30 @@ class NotificationProcessorService extends EventEmitter { // Use user-defined template first, then the GENERIC fallback switch (setting.channel_type) { case 'email': +console.log(`[NotificationProcessor NEW DEBUG] Raw setting.config:`, JSON.stringify(setting.config)); + + +console.log(`[NotificationProcessor NEW DEBUG] Parsed emailConfig:`, JSON.stringify(emailConfig)); +if (emailConfig && typeof emailConfig === 'object') { + console.log(`[NotificationProcessor NEW DEBUG] emailConfig.subjectTemplate:`, emailConfig.subjectTemplate); + } else { + console.log(`[NotificationProcessor NEW DEBUG] emailConfig is not a valid object.`); + } + const emailConfig = setting.config as EmailConfig; - // Use user template OR generic fallback - const subjectTemplate = emailConfig.subjectTemplate || genericSubject; - subject = this.interpolate(subjectTemplate, baseInterpolationData); - // For email body, assume user template is HTML if provided, otherwise use generic HTML fallback - // Note: EmailConfig type currently doesn't have bodyTemplate. Using generic fallback. - body = this.interpolate(genericEmailBody, baseInterpolationData); + // Subject is now fixed to the translated event name +if (emailConfig && typeof emailConfig === 'object') { + console.log(`[NotificationProcessor NEW DEBUG] emailConfig.subjectTemplate:`, emailConfig.subjectTemplate); + } else { + console.log(`[NotificationProcessor NEW DEBUG] emailConfig is not a valid object.`); + } + subject = translatedEvent; + + // Use user-defined template (from subjectTemplate field) for the BODY, or generic fallback + + + const bodyTemplate = emailConfig.subjectTemplate || genericEmailBody; + body = this.interpolate(bodyTemplate, baseInterpolationData); break; case 'webhook': diff --git a/packages/backend/src/services/notification.service.ts b/packages/backend/src/services/notification.service.ts index 8f2d651..7295154 100644 --- a/packages/backend/src/services/notification.service.ts +++ b/packages/backend/src/services/notification.service.ts @@ -427,46 +427,73 @@ export class NotificationService { const eventDisplayName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - const defaultSubjectKey = `event.${payload.event}`; - const defaultSubjectFallback = `Nexus Terminal Notification: {event}`; - const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); + // --- Subject --- + // Subject is now always the translated event display name + const subject = eventDisplayName; + console.log(`[_sendEmail] Using fixed subject for event ${payload.event}: ${subject}`); - const defaultSubjectTemplateKey = 'testNotification.subject'; - const defaultSubjectTemplate = i18next.t(defaultSubjectTemplateKey, { lng: userLang, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); - // Prepare data object for _renderTemplate (for subject) - const templateDataEmailSubject: Record = { - event: payload.event, - eventDisplay: eventDisplayName, // Assuming subject doesn't need markdown - // NEW: Format timestamp using user's timezone - timestamp: formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz"), // Example format for email - details: typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2), - // Add other relevant fields from i18nOptions if needed by subject template - ...Object.entries(i18nOptions).reduce((acc, [key, value]) => { - if (key !== 'lng') { // Exclude 'lng' itself - acc[key] = String(value); - } - return acc; - }, {} as Record) - }; - const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, templateDataEmailSubject, subjectText); // Use new signature (3 args) - - - const bodyKey = `eventBody.${payload.event}`; - const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2); - // NEW: Use formatted timestamp in default body text + // --- Body --- const formattedTimestampForEmail = formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz"); + const detailsString = typeof payload.details === 'string' + ? payload.details + : JSON.stringify(payload.details || {}, null, 2); + + // Prepare data for template rendering or i18n interpolation + const templateDataEmailBody: Record = { + event: payload.event, // Raw event name + eventDisplay: eventDisplayName, // Translated event name + timestamp: formattedTimestampForEmail, // Formatted timestamp + details: detailsString, // Stringified details + // Add other relevant fields from i18nOptions if needed by body template/i18n + ...Object.entries(i18nOptions).reduce((acc, [key, value]) => { + if (key !== 'lng' && typeof value !== 'object') { // Exclude 'lng' and objects + acc[key] = String(value); + } + return acc; + }, {} as Record) + }; + console.log(`[_sendEmail] Prepared templateDataEmailBody for event ${payload.event}:`, templateDataEmailBody); + + let body = ''; + // Correctly construct the default body text using translated/formatted variables const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${formattedTimestampForEmail}\nDetails:\n${detailsString}`; - // Pass formatted timestamp to i18n interpolation as well - const body = i18next.t(bodyKey, { ...i18nOptions, timestamp: formattedTimestampForEmail, defaultValue: defaultBodyText, eventDisplay: eventDisplayName }); + + // Check if the user provided a bodyTemplate in the config + if (config.bodyTemplate) { + // Use custom body template if provided + let templateToRender = config.bodyTemplate; + console.log(`[_sendEmail] Original custom body template for event ${payload.event}:`, templateToRender); + + // --- PRE-PROCESS TEMPLATE: Replace {event} with {eventDisplay} --- + // This ensures the translated event name is used even if the user's template uses {event} + templateToRender = templateToRender.replace(/\{event\}/g, '{eventDisplay}'); + console.log(`[_sendEmail] Pre-processed body template (replaced {event} with {eventDisplay}):`, templateToRender); + // ----------------------------------------------------------------- + + // Render the potentially modified template. + body = this._renderTemplate(templateToRender, templateDataEmailBody, defaultBodyText); + } else { + // No custom template, use the directly constructed default text + console.log(`[_sendEmail] No custom body template found. Using default constructed body text for event ${payload.event}`); + body = defaultBodyText; + // Removed lines related to unused bodyKey: + // const bodyKey = `eventBody.${payload.event}`; + // console.log(`[_sendEmail] No custom body template found. Using i18n body key '${bodyKey}' for event ${payload.event}`); + // body = i18next.t(bodyKey, { ... }); + } + console.log(`[_sendEmail] Final email body for event ${payload.event}:\n${body}`); + const mailOptions: Mail.Options = { from: config.from, to: config.to, - subject: subject, - text: body, + subject: subject, // Use the fixed subject + text: body, // Use the rendered/translated body + // Consider adding an html property if your templates/i18n generate HTML + // html: bodyHtml, }; try { - console.log(`[通知] 通过 ${config.smtpHost}:${config.smtpPort} 发送邮件至 ${config.to} (事件: ${payload.event})`); + console.log(`[通知] 通过 ${config.smtpHost}:${config.smtpPort} 发送邮件至 ${config.to} (事件: ${payload.event}, 主题: ${subject})`); const info = await transporter.sendMail(mailOptions); console.log(`[通知] 邮件成功发送至 ${config.to} (设置 ID: ${setting.id})。消息 ID: ${info.messageId}`); } catch (error: any) { diff --git a/packages/backend/src/services/senders/email.sender.service.ts b/packages/backend/src/services/senders/email.sender.service.ts index 9b4f64b..bb0dd8b 100644 --- a/packages/backend/src/services/senders/email.sender.service.ts +++ b/packages/backend/src/services/senders/email.sender.service.ts @@ -1,5 +1,6 @@ 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'; @@ -47,19 +48,21 @@ class EmailSenderService implements INotificationSender { } // Remove explicit type annotation to let TypeScript infer the type - const transporterOptions = { + 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: { - // Do not fail on invalid certs if secure is false or not explicitly required - rejectUnauthorized: finalSmtpSecure // Stricter check based on finalSecure value - } - }; + 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); diff --git a/packages/backend/src/types/notification.types.ts b/packages/backend/src/types/notification.types.ts index a0d17ed..86d869e 100644 --- a/packages/backend/src/types/notification.types.ts +++ b/packages/backend/src/types/notification.types.ts @@ -22,7 +22,7 @@ export interface WebhookConfig { export interface EmailConfig { to: string; // Comma-separated list of recipient emails - subjectTemplate?: string; // Optional subject template + bodyTemplate?: string; // Optional body template (plain text) // SMTP settings per channel smtpHost?: string; smtpPort?: number; diff --git a/packages/frontend/src/components/NotificationSettingForm.vue b/packages/frontend/src/components/NotificationSettingForm.vue index 8016651..e926596 100644 --- a/packages/frontend/src/components/NotificationSettingForm.vue +++ b/packages/frontend/src/components/NotificationSettingForm.vue @@ -77,10 +77,10 @@ {{ $t('settings.notifications.form.emailToHelp') }}
- - - {{ $t('settings.notifications.form.templateHelp') }} + + + {{ $t('settings.notifications.form.templateHelp') }} {event}, {timestamp}, {details}
@@ -219,7 +219,8 @@ import { import { useI18n } from 'vue-i18n'; // Extend EmailConfig for SMTP fields -interface SmtpEmailConfig extends EmailConfig { +interface SmtpEmailConfig extends Omit { // Omit subjectTemplate from base + bodyTemplate?: string; // Add bodyTemplate smtpHost?: string; smtpPort?: number; smtpSecure?: boolean; @@ -297,7 +298,7 @@ const formData = reactive(getDefaultFormData()); const webhookConfig = ref({ url: '', method: 'POST', headers: {}, bodyTemplate: '' }); const emailConfig = ref({ // Use extended type to: '', - subjectTemplate: '', + bodyTemplate: '', // Changed from subjectTemplate smtpHost: '', smtpPort: 587, // Default port smtpSecure: true, // Default to true (TLS) @@ -322,7 +323,7 @@ watch(() => props.initialData, (newData) => { const savedConfig = newData.config as SmtpEmailConfig; emailConfig.value = { to: savedConfig.to || '', - subjectTemplate: savedConfig.subjectTemplate || '', + bodyTemplate: savedConfig.bodyTemplate || '', // Changed from subjectTemplate smtpHost: savedConfig.smtpHost || '', smtpPort: savedConfig.smtpPort || 587, smtpSecure: savedConfig.smtpSecure === undefined ? true : savedConfig.smtpSecure, // Default true if undefined @@ -339,7 +340,7 @@ watch(() => props.initialData, (newData) => { webhookConfig.value = { url: '', method: 'POST', headers: {}, bodyTemplate: '' }; // Reset email config with defaults emailConfig.value = { - to: '', subjectTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' + to: '', bodyTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' // Changed from subjectTemplate }; telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' }; webhookHeadersString.value = '{}'; @@ -357,7 +358,7 @@ watch(() => formData.channel_type, (newType, oldType) => { if (newType !== oldType && !isEditing.value) { // Only reset if not editing or type changes during add mode webhookConfig.value = { url: '', method: 'POST', headers: {}, bodyTemplate: '' }; emailConfig.value = { - to: '', subjectTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' + to: '', bodyTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' // Changed from subjectTemplate }; telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' }; webhookHeadersString.value = '{}'; diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 90d0ff4..d8ac07c 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -467,8 +467,8 @@ "webhookBodyPlaceholder": "Default: JSON payload. Use", "emailTo": "Recipient Email(s):", "emailToHelp": "Comma-separated list.", - "emailSubjectTemplate": "Subject Template (Optional)", - "emailSubjectPlaceholder": "Default: Notification:", + "emailBodyTemplate": "Body Template (Optional)", + "emailBodyPlaceholder": "Default: Event-based notification content. Use", "smtpHost": "SMTP Host:", "smtpPort": "SMTP Port:", "smtpSecure": "Use TLS/SSL", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index b3254d6..fda215e 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -467,8 +467,8 @@ "webhookBodyPlaceholder": "デフォルト: JSON フォーマットのペイロード。利用可能:", "emailTo": "宛先メールアドレス:", "emailToHelp": "複数のメールアドレスをカンマで区切ります。", - "emailSubjectTemplate": "メールの件名テンプレート (オプション)", - "emailSubjectPlaceholder": "デフォルト: 通知:", + "emailBodyTemplate": "メール本文テンプレート (オプション)", + "emailBodyPlaceholder": "デフォルト: イベントベースの通知内容。利用可能:", "smtpHost": "SMTP ホスト:", "smtpPort": "SMTP ポート:", "smtpSecure": "TLS/SSL を使用", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 756bd59..f9608d2 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -467,8 +467,8 @@ "webhookBodyPlaceholder": "默认: JSON 格式负载。可使用", "emailTo": "收件人邮箱:", "emailToHelp": "多个邮箱用逗号分隔。", - "emailSubjectTemplate": "邮件主题模板 (可选)", - "emailSubjectPlaceholder": "默认: 通知:", + "emailBodyTemplate": "邮件内容模板 (可选)", + "emailBodyPlaceholder": "默认: 基于事件的通知内容。可使用", "smtpHost": "SMTP 主机:", "smtpPort": "SMTP 端口:", "smtpSecure": "使用 TLS/SSL",