update
This commit is contained in:
@@ -215,13 +215,30 @@ class NotificationProcessorService extends EventEmitter {
|
|||||||
// Use user-defined template first, then the GENERIC fallback
|
// Use user-defined template first, then the GENERIC fallback
|
||||||
switch (setting.channel_type) {
|
switch (setting.channel_type) {
|
||||||
case 'email':
|
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;
|
const emailConfig = setting.config as EmailConfig;
|
||||||
// Use user template OR generic fallback
|
// Subject is now fixed to the translated event name
|
||||||
const subjectTemplate = emailConfig.subjectTemplate || genericSubject;
|
if (emailConfig && typeof emailConfig === 'object') {
|
||||||
subject = this.interpolate(subjectTemplate, baseInterpolationData);
|
console.log(`[NotificationProcessor NEW DEBUG] emailConfig.subjectTemplate:`, emailConfig.subjectTemplate);
|
||||||
// For email body, assume user template is HTML if provided, otherwise use generic HTML fallback
|
} else {
|
||||||
// Note: EmailConfig type currently doesn't have bodyTemplate. Using generic fallback.
|
console.log(`[NotificationProcessor NEW DEBUG] emailConfig is not a valid object.`);
|
||||||
body = this.interpolate(genericEmailBody, baseInterpolationData);
|
}
|
||||||
|
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;
|
break;
|
||||||
|
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
|
|||||||
@@ -427,46 +427,73 @@ export class NotificationService {
|
|||||||
|
|
||||||
const eventDisplayName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
const eventDisplayName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
||||||
|
|
||||||
const defaultSubjectKey = `event.${payload.event}`;
|
// --- Subject ---
|
||||||
const defaultSubjectFallback = `Nexus Terminal Notification: {event}`;
|
// Subject is now always the translated event display name
|
||||||
const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName });
|
const subject = eventDisplayName;
|
||||||
|
console.log(`[_sendEmail] Using fixed subject for event ${payload.event}: ${subject}`);
|
||||||
|
|
||||||
const defaultSubjectTemplateKey = 'testNotification.subject';
|
// --- Body ---
|
||||||
const defaultSubjectTemplate = i18next.t(defaultSubjectTemplateKey, { lng: userLang, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName });
|
const formattedTimestampForEmail = formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz");
|
||||||
// Prepare data object for _renderTemplate (for subject)
|
const detailsString = typeof payload.details === 'string'
|
||||||
const templateDataEmailSubject: Record<string, string> = {
|
? payload.details
|
||||||
event: payload.event,
|
: JSON.stringify(payload.details || {}, null, 2);
|
||||||
eventDisplay: eventDisplayName, // Assuming subject doesn't need markdown
|
|
||||||
// NEW: Format timestamp using user's timezone
|
// Prepare data for template rendering or i18n interpolation
|
||||||
timestamp: formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz"), // Example format for email
|
const templateDataEmailBody: Record<string, string> = {
|
||||||
details: typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2),
|
event: payload.event, // Raw event name
|
||||||
// Add other relevant fields from i18nOptions if needed by subject template
|
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]) => {
|
...Object.entries(i18nOptions).reduce((acc, [key, value]) => {
|
||||||
if (key !== 'lng') { // Exclude 'lng' itself
|
if (key !== 'lng' && typeof value !== 'object') { // Exclude 'lng' and objects
|
||||||
acc[key] = String(value);
|
acc[key] = String(value);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, string>)
|
}, {} as Record<string, string>)
|
||||||
};
|
};
|
||||||
const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, templateDataEmailSubject, subjectText); // Use new signature (3 args)
|
console.log(`[_sendEmail] Prepared templateDataEmailBody for event ${payload.event}:`, templateDataEmailBody);
|
||||||
|
|
||||||
|
let body = '';
|
||||||
const bodyKey = `eventBody.${payload.event}`;
|
// Correctly construct the default body text using translated/formatted variables
|
||||||
const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2);
|
|
||||||
// NEW: Use formatted timestamp in default body text
|
|
||||||
const formattedTimestampForEmail = formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz");
|
|
||||||
const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${formattedTimestampForEmail}\nDetails:\n${detailsString}`;
|
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 = {
|
const mailOptions: Mail.Options = {
|
||||||
from: config.from,
|
from: config.from,
|
||||||
to: config.to,
|
to: config.to,
|
||||||
subject: subject,
|
subject: subject, // Use the fixed subject
|
||||||
text: body,
|
text: body, // Use the rendered/translated body
|
||||||
|
// Consider adding an html property if your templates/i18n generate HTML
|
||||||
|
// html: bodyHtml,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
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);
|
const info = await transporter.sendMail(mailOptions);
|
||||||
console.log(`[通知] 邮件成功发送至 ${config.to} (设置 ID: ${setting.id})。消息 ID: ${info.messageId}`);
|
console.log(`[通知] 邮件成功发送至 ${config.to} (设置 ID: ${setting.id})。消息 ID: ${info.messageId}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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 Mail type for transporter
|
||||||
|
import SMTPTransport from 'nodemailer/lib/smtp-transport'; // Import SMTPTransport for options type
|
||||||
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';
|
||||||
@@ -47,7 +48,7 @@ class EmailSenderService implements INotificationSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove explicit type annotation to let TypeScript infer the type
|
// Remove explicit type annotation to let TypeScript infer the type
|
||||||
const transporterOptions = {
|
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, // true for 465, false for other ports
|
||||||
@@ -56,8 +57,10 @@ class EmailSenderService implements INotificationSender {
|
|||||||
pass: finalSmtpPass,
|
pass: finalSmtpPass,
|
||||||
} : undefined, // Only include auth if user/pass are provided
|
} : undefined, // Only include auth if user/pass are provided
|
||||||
tls: {
|
tls: {
|
||||||
// Do not fail on invalid certs if secure is false or not explicitly required
|
// rejectUnauthorized should be within the tls object according to types
|
||||||
rejectUnauthorized: finalSmtpSecure // Stricter check based on finalSecure value
|
rejectUnauthorized: finalSmtpSecure,
|
||||||
|
// minVersion is also a valid TLS option
|
||||||
|
minVersion: 'TLSv1.2' // Explicitly require TLSv1.2 or higher for Gmail compatibility
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface WebhookConfig {
|
|||||||
|
|
||||||
export interface EmailConfig {
|
export interface EmailConfig {
|
||||||
to: string; // Comma-separated list of recipient emails
|
to: string; // Comma-separated list of recipient emails
|
||||||
subjectTemplate?: string; // Optional subject template
|
bodyTemplate?: string; // Optional body template (plain text)
|
||||||
// SMTP settings per channel
|
// SMTP settings per channel
|
||||||
smtpHost?: string;
|
smtpHost?: string;
|
||||||
smtpPort?: number;
|
smtpPort?: number;
|
||||||
|
|||||||
@@ -77,10 +77,10 @@
|
|||||||
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.emailToHelp') }}</small>
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.emailToHelp') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="email-subject" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.emailSubjectTemplate') }}</label>
|
<label for="email-body" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.emailBodyTemplate') }}</label> <!-- Changed key -->
|
||||||
<input type="text" id="email-subject" v-model="emailConfig.subjectTemplate" :placeholder="`${$t('settings.notifications.form.emailSubjectPlaceholder')} {event}`"
|
<textarea id="email-body" v-model="emailConfig.bodyTemplate" rows="3" :placeholder="`${$t('settings.notifications.form.emailBodyPlaceholder')} {event}, {timestamp}, {details}`"
|
||||||
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea> <!-- Changed to textarea and v-model -->
|
||||||
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.templateHelp') }}</small>
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.templateHelp') }} {event}, {timestamp}, {details}</small> <!-- Added available placeholders -->
|
||||||
</div>
|
</div>
|
||||||
<!-- SMTP Settings -->
|
<!-- SMTP Settings -->
|
||||||
<div>
|
<div>
|
||||||
@@ -219,7 +219,8 @@ import {
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
// Extend EmailConfig for SMTP fields
|
// Extend EmailConfig for SMTP fields
|
||||||
interface SmtpEmailConfig extends EmailConfig {
|
interface SmtpEmailConfig extends Omit<EmailConfig, 'subjectTemplate'> { // Omit subjectTemplate from base
|
||||||
|
bodyTemplate?: string; // Add bodyTemplate
|
||||||
smtpHost?: string;
|
smtpHost?: string;
|
||||||
smtpPort?: number;
|
smtpPort?: number;
|
||||||
smtpSecure?: boolean;
|
smtpSecure?: boolean;
|
||||||
@@ -297,7 +298,7 @@ const formData = reactive(getDefaultFormData());
|
|||||||
const webhookConfig = ref<WebhookConfig>({ url: '', method: 'POST', headers: {}, bodyTemplate: '' });
|
const webhookConfig = ref<WebhookConfig>({ url: '', method: 'POST', headers: {}, bodyTemplate: '' });
|
||||||
const emailConfig = ref<SmtpEmailConfig>({ // Use extended type
|
const emailConfig = ref<SmtpEmailConfig>({ // Use extended type
|
||||||
to: '',
|
to: '',
|
||||||
subjectTemplate: '',
|
bodyTemplate: '', // Changed from subjectTemplate
|
||||||
smtpHost: '',
|
smtpHost: '',
|
||||||
smtpPort: 587, // Default port
|
smtpPort: 587, // Default port
|
||||||
smtpSecure: true, // Default to true (TLS)
|
smtpSecure: true, // Default to true (TLS)
|
||||||
@@ -322,7 +323,7 @@ watch(() => props.initialData, (newData) => {
|
|||||||
const savedConfig = newData.config as SmtpEmailConfig;
|
const savedConfig = newData.config as SmtpEmailConfig;
|
||||||
emailConfig.value = {
|
emailConfig.value = {
|
||||||
to: savedConfig.to || '',
|
to: savedConfig.to || '',
|
||||||
subjectTemplate: savedConfig.subjectTemplate || '',
|
bodyTemplate: savedConfig.bodyTemplate || '', // Changed from subjectTemplate
|
||||||
smtpHost: savedConfig.smtpHost || '',
|
smtpHost: savedConfig.smtpHost || '',
|
||||||
smtpPort: savedConfig.smtpPort || 587,
|
smtpPort: savedConfig.smtpPort || 587,
|
||||||
smtpSecure: savedConfig.smtpSecure === undefined ? true : savedConfig.smtpSecure, // Default true if undefined
|
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: '' };
|
webhookConfig.value = { url: '', method: 'POST', headers: {}, bodyTemplate: '' };
|
||||||
// Reset email config with defaults
|
// Reset email config with defaults
|
||||||
emailConfig.value = {
|
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: '' };
|
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' };
|
||||||
webhookHeadersString.value = '{}';
|
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
|
if (newType !== oldType && !isEditing.value) { // Only reset if not editing or type changes during add mode
|
||||||
webhookConfig.value = { url: '', method: 'POST', headers: {}, bodyTemplate: '' };
|
webhookConfig.value = { url: '', method: 'POST', headers: {}, bodyTemplate: '' };
|
||||||
emailConfig.value = {
|
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: '' };
|
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' };
|
||||||
webhookHeadersString.value = '{}';
|
webhookHeadersString.value = '{}';
|
||||||
|
|||||||
@@ -467,8 +467,8 @@
|
|||||||
"webhookBodyPlaceholder": "Default: JSON payload. Use",
|
"webhookBodyPlaceholder": "Default: JSON payload. Use",
|
||||||
"emailTo": "Recipient Email(s):",
|
"emailTo": "Recipient Email(s):",
|
||||||
"emailToHelp": "Comma-separated list.",
|
"emailToHelp": "Comma-separated list.",
|
||||||
"emailSubjectTemplate": "Subject Template (Optional)",
|
"emailBodyTemplate": "Body Template (Optional)",
|
||||||
"emailSubjectPlaceholder": "Default: Notification:",
|
"emailBodyPlaceholder": "Default: Event-based notification content. Use",
|
||||||
"smtpHost": "SMTP Host:",
|
"smtpHost": "SMTP Host:",
|
||||||
"smtpPort": "SMTP Port:",
|
"smtpPort": "SMTP Port:",
|
||||||
"smtpSecure": "Use TLS/SSL",
|
"smtpSecure": "Use TLS/SSL",
|
||||||
|
|||||||
@@ -467,8 +467,8 @@
|
|||||||
"webhookBodyPlaceholder": "デフォルト: JSON フォーマットのペイロード。利用可能:",
|
"webhookBodyPlaceholder": "デフォルト: JSON フォーマットのペイロード。利用可能:",
|
||||||
"emailTo": "宛先メールアドレス:",
|
"emailTo": "宛先メールアドレス:",
|
||||||
"emailToHelp": "複数のメールアドレスをカンマで区切ります。",
|
"emailToHelp": "複数のメールアドレスをカンマで区切ります。",
|
||||||
"emailSubjectTemplate": "メールの件名テンプレート (オプション)",
|
"emailBodyTemplate": "メール本文テンプレート (オプション)",
|
||||||
"emailSubjectPlaceholder": "デフォルト: 通知:",
|
"emailBodyPlaceholder": "デフォルト: イベントベースの通知内容。利用可能:",
|
||||||
"smtpHost": "SMTP ホスト:",
|
"smtpHost": "SMTP ホスト:",
|
||||||
"smtpPort": "SMTP ポート:",
|
"smtpPort": "SMTP ポート:",
|
||||||
"smtpSecure": "TLS/SSL を使用",
|
"smtpSecure": "TLS/SSL を使用",
|
||||||
|
|||||||
@@ -467,8 +467,8 @@
|
|||||||
"webhookBodyPlaceholder": "默认: JSON 格式负载。可使用",
|
"webhookBodyPlaceholder": "默认: JSON 格式负载。可使用",
|
||||||
"emailTo": "收件人邮箱:",
|
"emailTo": "收件人邮箱:",
|
||||||
"emailToHelp": "多个邮箱用逗号分隔。",
|
"emailToHelp": "多个邮箱用逗号分隔。",
|
||||||
"emailSubjectTemplate": "邮件主题模板 (可选)",
|
"emailBodyTemplate": "邮件内容模板 (可选)",
|
||||||
"emailSubjectPlaceholder": "默认: 通知:",
|
"emailBodyPlaceholder": "默认: 基于事件的通知内容。可使用",
|
||||||
"smtpHost": "SMTP 主机:",
|
"smtpHost": "SMTP 主机:",
|
||||||
"smtpPort": "SMTP 端口:",
|
"smtpPort": "SMTP 端口:",
|
||||||
"smtpSecure": "使用 TLS/SSL",
|
"smtpSecure": "使用 TLS/SSL",
|
||||||
|
|||||||
Reference in New Issue
Block a user