Files
Calbook/lib/email/mailer.ts

816 lines
24 KiB
TypeScript

import nodemailer from "nodemailer";
import { getSettings } from "@/lib/settings";
import { DEFAULT_SETTINGS, SETTING_KEYS } from "@/lib/constants";
import {
DEFAULT_TIMEZONE,
formatDateDE,
formatTimeDE,
resolveTimeZone
} from "@/lib/date";
import { renderTemplate } from "@/lib/email/template-engine";
import { renderStyledEmail } from "@/lib/email/style-renderer";
import { normalizeMeetingButtonTemplate } from "@/lib/email/shortcodes";
import { withRetry } from "@/lib/services/retry";
import { buildPublicUrl } from "@/lib/public-url";
import {
reportDeliveryFailure,
resolveDeliveryIssues
} from "@/lib/services/delivery-issues";
type StaffRecipient = {
name: string;
email: string;
};
type ReminderKind = "primary" | "secondary";
type SmtpConfigInput = {
host?: string;
port?: string | number;
user?: string;
pass?: string;
fromName?: string;
from?: string;
secure?: boolean;
};
function sanitizeHeaderValue(value: string) {
return value.replace(/[\r\n]+/g, " ").trim();
}
function extractFromAddress(value?: string) {
if (!value) return "";
const match = value.match(/<([^>]+)>/);
if (match?.[1]) return sanitizeHeaderValue(match[1]);
return sanitizeHeaderValue(value);
}
function extractFromName(value?: string) {
if (!value) return "";
const match = value.match(/^([^<]+)<[^>]+>$/);
if (!match?.[1]) return "";
return sanitizeHeaderValue(match[1].replace(/^"(.+)"$/, "$1"));
}
function isEmail(value: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
function uniqueStaff(staffList: StaffRecipient[]): StaffRecipient[] {
const byEmail = new Map<string, StaffRecipient>();
for (const staff of staffList) {
if (!byEmail.has(staff.email)) {
byEmail.set(staff.email, staff);
}
}
return Array.from(byEmail.values());
}
function isRetryableMailError(error: unknown) {
const maybeCode =
typeof error === "object" && error !== null && "code" in error
? String((error as { code?: unknown }).code ?? "")
: "";
const code = maybeCode.toUpperCase();
if (["EAUTH", "EENVELOPE", "EADDRESS"].includes(code)) {
return false;
}
const message =
error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
if (
message.includes("authentication") ||
message.includes("invalid login") ||
message.includes("bad credentials")
) {
return false;
}
return true;
}
async function sendMailWithRetry(
transporter: nodemailer.Transporter,
mail: nodemailer.SendMailOptions,
meta: {
operation: string;
target: string;
}
) {
try {
await withRetry(
async () => transporter.sendMail(mail),
{
attempts: 4,
baseDelayMs: 700,
maxDelayMs: 5_000,
shouldRetry: isRetryableMailError
}
);
await resolveDeliveryIssues({
channel: "SMTP",
operation: meta.operation,
target: meta.target
});
} catch (error) {
await reportDeliveryFailure({
channel: "SMTP",
operation: meta.operation,
target: meta.target,
error
});
throw error;
}
}
async function getTransportConfig(overrides?: SmtpConfigInput) {
const settings = await getSettings([
SETTING_KEYS.SMTP_HOST,
SETTING_KEYS.SMTP_PORT,
SETTING_KEYS.SMTP_USER,
SETTING_KEYS.SMTP_PASS,
SETTING_KEYS.SMTP_FROM_NAME,
SETTING_KEYS.SMTP_FROM
]);
const host =
overrides && "host" in overrides
? overrides.host?.trim()
: settings[SETTING_KEYS.SMTP_HOST] || process.env.SMTP_HOST;
const portValue =
overrides && "port" in overrides
? overrides.port
: settings[SETTING_KEYS.SMTP_PORT] || process.env.SMTP_PORT || 587;
const parsedPort = Number(portValue || 587);
const port = Number.isFinite(parsedPort) ? parsedPort : 587;
const secureFromEnv =
(process.env.SMTP_SECURE ?? "").toLowerCase() === "true" ||
process.env.SMTP_SECURE === "1";
const secure =
overrides && "secure" in overrides && typeof overrides.secure === "boolean"
? overrides.secure
: secureFromEnv || port === 465;
const smtpUser =
overrides && "user" in overrides
? overrides.user?.trim() || ""
: settings[SETTING_KEYS.SMTP_USER] || process.env.SMTP_USER || "";
const smtpPass =
overrides && "pass" in overrides
? overrides.pass ?? ""
: settings[SETTING_KEYS.SMTP_PASS] || process.env.SMTP_PASS || "";
const smtpUserAddress = isEmail(smtpUser) ? smtpUser : "";
const rawFromAddress =
overrides && "from" in overrides
? overrides.from?.trim() || ""
: settings[SETTING_KEYS.SMTP_FROM] || process.env.SMTP_FROM || "";
const configuredFromAddress = extractFromAddress(rawFromAddress);
const fromAddress =
smtpUserAddress ||
(isEmail(configuredFromAddress) ? configuredFromAddress : "") ||
"no-reply@calbook.local";
const fromNameRaw =
overrides && "fromName" in overrides
? overrides.fromName || extractFromName(rawFromAddress) || "CalBook"
: settings[SETTING_KEYS.SMTP_FROM_NAME] ||
process.env.SMTP_FROM_NAME ||
extractFromName(rawFromAddress) ||
"CalBook";
const fromName = sanitizeHeaderValue(fromNameRaw) || "CalBook";
return {
host,
port,
secure,
auth:
smtpUser
? {
user: smtpUser,
pass: smtpPass
}
: undefined,
from: {
name: fromName,
address: fromAddress
}
};
}
async function createTransporter(overrides?: SmtpConfigInput) {
const cfg = await getTransportConfig(overrides);
if (!cfg.host) return null;
return {
cfg,
transporter: nodemailer.createTransport(cfg)
};
}
export async function sendBookingEmails(params: {
appointment: {
customerEmail: string;
customerFirstName: string;
customerLastName: string;
customerPhone?: string | null;
notes?: string | null;
startAt: Date;
endAt: Date;
durationMinutes: number;
cancellationToken: string;
meetingUrl: string;
customerTimezone?: string;
};
staffList: StaffRecipient[];
companyName: string;
}) {
const settings = await getSettings([
SETTING_KEYS.EMAIL_SUBJECT_CUSTOMER_CONFIRM,
SETTING_KEYS.EMAIL_SUBJECT_STAFF_NOTIFY,
SETTING_KEYS.EMAIL_TEMPLATE_CUSTOMER_CONFIRM,
SETTING_KEYS.EMAIL_TEMPLATE_STAFF_NOTIFY,
SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID,
SETTING_KEYS.COMPANY_NAME
]);
const transport = await createTransporter();
if (!transport) return;
const { cfg, transporter } = transport;
const staffRecipients = uniqueStaff(params.staffList);
if (staffRecipients.length === 0) return;
const customerName = `${params.appointment.customerFirstName} ${params.appointment.customerLastName}`;
const customerTimezone = resolveTimeZone(params.appointment.customerTimezone);
const customerDate = formatDateDE(params.appointment.startAt, false, customerTimezone);
const customerTime = formatTimeDE(params.appointment.startAt, customerTimezone);
const staffDate = formatDateDE(params.appointment.startAt, false, DEFAULT_TIMEZONE);
const staffTime = formatTimeDE(params.appointment.startAt, DEFAULT_TIMEZONE);
const staffNames = staffRecipients.map((staff) => staff.name).join(", ");
const cancelUrl = buildPublicUrl(
`/stornieren?token=${encodeURIComponent(params.appointment.cancellationToken)}`
);
const rescheduleUrl = buildPublicUrl(
`/buchen?rescheduleToken=${encodeURIComponent(params.appointment.cancellationToken)}`
);
const dashboardUrl = buildPublicUrl("/admin/termine");
const customerBaseValues: Record<string, string> = {
customerName,
date: customerDate,
time: customerTime,
duration: String(params.appointment.durationMinutes),
staffNames,
companyName: params.companyName,
cancelUrl,
rescheduleUrl,
phone: params.appointment.customerPhone ?? "-",
email: params.appointment.customerEmail,
notes: params.appointment.notes ?? "-",
meetingUrl: params.appointment.meetingUrl,
dashboardUrl,
staffName: staffRecipients[0]?.name ?? "Person",
timezone: customerTimezone
};
const staffBaseValues: Record<string, string> = {
...customerBaseValues,
date: staffDate,
time: staffTime,
timezone: DEFAULT_TIMEZONE
};
const customerSubject = renderTemplate(
settings[SETTING_KEYS.EMAIL_SUBJECT_CUSTOMER_CONFIRM],
customerBaseValues
);
const customerTemplate = normalizeMeetingButtonTemplate(
settings[SETTING_KEYS.EMAIL_TEMPLATE_CUSTOMER_CONFIRM]
);
const customerBody = renderTemplate(
customerTemplate,
customerBaseValues
);
const styleId = settings[SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID];
const customerStyled = renderStyledEmail({
styleId,
subject: customerSubject,
companyName: params.companyName,
heading: customerSubject,
body: customerBody,
ctaLabel: "Meeting beitreten",
ctaUrl: params.appointment.meetingUrl,
secondaryCtaLabel: "Termin stornieren",
secondaryCtaUrl: cancelUrl,
footerNote: ""
});
await sendMailWithRetry(
transporter,
{
from: cfg.from,
to: params.appointment.customerEmail,
subject: customerSubject,
text: customerStyled.text,
html: customerStyled.html
},
{
operation: "booking-customer",
target: params.appointment.customerEmail
}
);
await Promise.all(
staffRecipients.map((staff) => {
const values = {
...staffBaseValues,
staffName: staff.name
};
const staffBody = renderTemplate(
normalizeMeetingButtonTemplate(settings[SETTING_KEYS.EMAIL_TEMPLATE_STAFF_NOTIFY]),
values
);
const staffSubject = renderTemplate(
settings[SETTING_KEYS.EMAIL_SUBJECT_STAFF_NOTIFY],
values
);
const staffStyled = renderStyledEmail({
styleId,
subject: staffSubject,
companyName: params.companyName,
heading: staffSubject,
body: staffBody,
ctaLabel: "Meeting beitreten",
ctaUrl: params.appointment.meetingUrl,
secondaryCtaLabel: "Termin stornieren",
secondaryCtaUrl: cancelUrl,
footerNote: ""
});
return sendMailWithRetry(
transporter,
{
from: cfg.from,
to: staff.email,
subject: staffSubject,
text: staffStyled.text,
html: staffStyled.html
},
{
operation: "booking-staff",
target: staff.email
}
);
})
);
}
export async function sendCancellationEmails(params: {
customerEmail: string;
customerName: string;
staffList: StaffRecipient[];
date: Date;
customerTimezone?: string;
companyName: string;
}) {
const settings = await getSettings([
SETTING_KEYS.EMAIL_SUBJECT_CANCELLATION_CUSTOMER,
SETTING_KEYS.EMAIL_SUBJECT_CANCELLATION_STAFF,
SETTING_KEYS.EMAIL_TEMPLATE_CANCELLATION,
SETTING_KEYS.EMAIL_TEMPLATE_CANCELLATION_CUSTOMER,
SETTING_KEYS.EMAIL_TEMPLATE_CANCELLATION_STAFF,
SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID
]);
const transport = await createTransporter();
if (!transport) return;
const { cfg, transporter } = transport;
const staffRecipients = uniqueStaff(params.staffList);
const customerTimezone = resolveTimeZone(params.customerTimezone);
const customerDate = formatDateDE(params.date, false, customerTimezone);
const customerTime = formatTimeDE(params.date, customerTimezone);
const staffDate = formatDateDE(params.date, false, DEFAULT_TIMEZONE);
const staffTime = formatTimeDE(params.date, DEFAULT_TIMEZONE);
const staffNames = staffRecipients.map((staff) => staff.name).join(", ");
const customerValues: Record<string, string> = {
customerName: params.customerName,
date: customerDate,
time: customerTime,
companyName: params.companyName,
staffNames,
staffName: staffRecipients[0]?.name ?? "Person",
timezone: customerTimezone
};
const staffValues: Record<string, string> = {
...customerValues,
date: staffDate,
time: staffTime,
timezone: DEFAULT_TIMEZONE
};
const styleId = settings[SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID];
const cancellationBody = renderTemplate(
settings[SETTING_KEYS.EMAIL_TEMPLATE_CANCELLATION_CUSTOMER] ||
settings[SETTING_KEYS.EMAIL_TEMPLATE_CANCELLATION],
customerValues
);
const cancellationCustomerSubject = renderTemplate(
settings[SETTING_KEYS.EMAIL_SUBJECT_CANCELLATION_CUSTOMER],
customerValues
);
const cancellationCustomerStyled = renderStyledEmail({
styleId,
subject: cancellationCustomerSubject,
companyName: params.companyName,
heading: cancellationCustomerSubject,
body: cancellationBody,
footerNote: ""
});
await sendMailWithRetry(
transporter,
{
from: cfg.from,
to: params.customerEmail,
subject: cancellationCustomerSubject,
text: cancellationCustomerStyled.text,
html: cancellationCustomerStyled.html
},
{
operation: "cancel-customer",
target: params.customerEmail
}
);
await Promise.all(
staffRecipients.map((staff) => {
const values = { ...staffValues, staffName: staff.name };
const subject = renderTemplate(
settings[SETTING_KEYS.EMAIL_SUBJECT_CANCELLATION_STAFF],
values
);
const body = renderTemplate(
settings[SETTING_KEYS.EMAIL_TEMPLATE_CANCELLATION_STAFF] ||
settings[SETTING_KEYS.EMAIL_TEMPLATE_CANCELLATION],
values
);
const styled = renderStyledEmail({
styleId,
subject,
companyName: params.companyName,
heading: subject,
body,
footerNote: ""
});
return sendMailWithRetry(
transporter,
{
from: cfg.from,
to: staff.email,
subject,
text: styled.text,
html: styled.html
},
{
operation: "cancel-staff",
target: staff.email
}
);
})
);
}
export async function sendReminderEmails(params: {
customerEmail: string;
customerName: string;
customerPhone?: string | null;
notes?: string | null;
staffList: StaffRecipient[];
date: Date;
customerTimezone?: string;
durationMinutes: number;
cancellationToken: string;
meetingUrl: string;
companyName: string;
reminderKind: ReminderKind;
hoursBefore: number;
}) {
const settings = await getSettings([
SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID,
SETTING_KEYS.EMAIL_SUBJECT_REMINDER_CUSTOMER,
SETTING_KEYS.EMAIL_SUBJECT_REMINDER_STAFF,
SETTING_KEYS.EMAIL_SUBJECT_REMINDER_CUSTOMER_PRIMARY,
SETTING_KEYS.EMAIL_SUBJECT_REMINDER_CUSTOMER_SECONDARY,
SETTING_KEYS.EMAIL_SUBJECT_REMINDER_STAFF_PRIMARY,
SETTING_KEYS.EMAIL_SUBJECT_REMINDER_STAFF_SECONDARY,
SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_CUSTOMER,
SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_STAFF,
SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_CUSTOMER_PRIMARY,
SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_CUSTOMER_SECONDARY,
SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_STAFF_PRIMARY,
SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_STAFF_SECONDARY
]);
const transport = await createTransporter();
if (!transport) return;
const { cfg, transporter } = transport;
const staffRecipients = uniqueStaff(params.staffList);
if (staffRecipients.length === 0) return;
const customerTimezone = resolveTimeZone(params.customerTimezone);
const customerDate = formatDateDE(params.date, false, customerTimezone);
const customerTime = formatTimeDE(params.date, customerTimezone);
const staffDate = formatDateDE(params.date, false, DEFAULT_TIMEZONE);
const staffTime = formatTimeDE(params.date, DEFAULT_TIMEZONE);
const staffNames = staffRecipients.map((staff) => staff.name).join(", ");
const styleId = settings[SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID];
const reminderHours = Math.max(1, Math.floor(params.hoursBefore));
const reminderLabel =
reminderHours === 1 ? "in 1 Stunde" : `in ${reminderHours} Stunden`;
const cancelUrl = buildPublicUrl(
`/stornieren?token=${encodeURIComponent(params.cancellationToken)}`
);
const rescheduleUrl = buildPublicUrl(
`/buchen?rescheduleToken=${encodeURIComponent(params.cancellationToken)}`
);
const dashboardUrl = buildPublicUrl("/admin/termine");
const customerBaseValues: Record<string, string> = {
customerName: params.customerName,
date: customerDate,
time: customerTime,
duration: String(params.durationMinutes),
staffNames,
companyName: params.companyName,
cancelUrl,
rescheduleUrl,
phone: params.customerPhone?.trim() || "-",
email: params.customerEmail,
notes: params.notes?.trim() || "-",
meetingUrl: params.meetingUrl,
dashboardUrl,
staffName: staffRecipients[0]?.name ?? "Person",
timezone: customerTimezone,
reminderLabel,
hoursBefore: String(reminderHours),
reminderKind: params.reminderKind
};
const staffBaseValues: Record<string, string> = {
...customerBaseValues,
date: staffDate,
time: staffTime,
timezone: DEFAULT_TIMEZONE
};
const customerSubjectTemplate =
params.reminderKind === "secondary"
? settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_CUSTOMER_SECONDARY] ||
settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_CUSTOMER]
: settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_CUSTOMER_PRIMARY] ||
settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_CUSTOMER];
const customerBodyTemplate =
params.reminderKind === "secondary"
? settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_CUSTOMER_SECONDARY] ||
settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_CUSTOMER]
: settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_CUSTOMER_PRIMARY] ||
settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_CUSTOMER];
const subjectCustomer = renderTemplate(customerSubjectTemplate, customerBaseValues);
const customerBody = renderTemplate(
normalizeMeetingButtonTemplate(customerBodyTemplate),
customerBaseValues
);
const customerStyled = renderStyledEmail({
styleId,
subject: subjectCustomer,
companyName: params.companyName,
heading: subjectCustomer,
lead: `Dein Termin startet ${reminderLabel}.`,
body: customerBody,
ctaLabel: "Meeting beitreten",
ctaUrl: params.meetingUrl,
secondaryCtaLabel: "Termin stornieren",
secondaryCtaUrl: cancelUrl,
footerNote: ""
});
await sendMailWithRetry(
transporter,
{
from: cfg.from,
to: params.customerEmail,
subject: subjectCustomer,
text: customerStyled.text,
html: customerStyled.html
},
{
operation: `reminder-${params.reminderKind}-customer`,
target: params.customerEmail
}
);
await Promise.all(
staffRecipients.map((staff) => {
const values = {
...staffBaseValues,
staffName: staff.name
};
const staffSubjectTemplate =
params.reminderKind === "secondary"
? settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_STAFF_SECONDARY] ||
settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_STAFF]
: settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_STAFF_PRIMARY] ||
settings[SETTING_KEYS.EMAIL_SUBJECT_REMINDER_STAFF];
const staffBodyTemplate =
params.reminderKind === "secondary"
? settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_STAFF_SECONDARY] ||
settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_STAFF]
: settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_STAFF_PRIMARY] ||
settings[SETTING_KEYS.EMAIL_TEMPLATE_REMINDER_STAFF];
const subjectStaff = renderTemplate(staffSubjectTemplate, values);
const staffBody = renderTemplate(
normalizeMeetingButtonTemplate(staffBodyTemplate),
values
);
const staffStyled = renderStyledEmail({
styleId,
subject: subjectStaff,
companyName: params.companyName,
heading: subjectStaff,
lead: `Der Termin mit ${params.customerName} startet ${reminderLabel}.`,
body: staffBody,
ctaLabel: "Meeting beitreten",
ctaUrl: params.meetingUrl,
secondaryCtaLabel: "Termin stornieren",
secondaryCtaUrl: cancelUrl,
footerNote: ""
});
return sendMailWithRetry(
transporter,
{
from: cfg.from,
to: staff.email,
subject: subjectStaff,
text: staffStyled.text,
html: staffStyled.html
},
{
operation: `reminder-${params.reminderKind}-staff`,
target: staff.email
}
);
})
);
}
export async function sendInstantMeetingEmails(params: {
recipients: Array<{ email: string; name?: string }>;
meetingUrl: string;
scopeLabel: string;
initiatorName: string;
companyName: string;
customMessage?: string;
subjectOverride?: string;
}) {
const transport = await createTransporter();
if (!transport) {
return { ok: false as const, message: "SMTP ist nicht konfiguriert." };
}
const settings = await getSettings([
SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID,
SETTING_KEYS.INSTANT_MEETING_EMAIL_SUBJECT,
SETTING_KEYS.INSTANT_MEETING_EMAIL_TEMPLATE
]);
const { cfg, transporter } = transport;
const styleId = settings[SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID];
const subjectTemplate =
params.subjectOverride?.trim() ||
settings[SETTING_KEYS.INSTANT_MEETING_EMAIL_SUBJECT] ||
DEFAULT_SETTINGS[SETTING_KEYS.INSTANT_MEETING_EMAIL_SUBJECT];
const bodyTemplate =
settings[SETTING_KEYS.INSTANT_MEETING_EMAIL_TEMPLATE] ||
DEFAULT_SETTINGS[SETTING_KEYS.INSTANT_MEETING_EMAIL_TEMPLATE];
const recipients = uniqueStaff(
params.recipients
.map((recipient) => ({
name: recipient.name?.trim() || recipient.email,
email: recipient.email.trim().toLowerCase()
}))
.filter((recipient) => isEmail(recipient.email))
);
if (recipients.length === 0) {
return { ok: false as const, message: "Keine gültigen Empfänger gefunden." };
}
await Promise.all(
recipients.map(async (recipient) => {
const values: Record<string, string> = {
recipientName: recipient.name || recipient.email,
companyName: params.companyName,
meetingUrl: params.meetingUrl,
scopeLabel: params.scopeLabel,
initiatorName: params.initiatorName,
customMessage: params.customMessage?.trim() || ""
};
const subject = renderTemplate(subjectTemplate, values);
const body = renderTemplate(bodyTemplate, values);
const styled = renderStyledEmail({
styleId,
subject,
companyName: params.companyName,
heading: subject,
body,
ctaLabel: "Meeting beitreten",
ctaUrl: params.meetingUrl,
footerNote: ""
});
await sendMailWithRetry(
transporter,
{
from: cfg.from,
to: recipient.email,
subject,
text: styled.text,
html: styled.html
},
{
operation: "instant-meeting",
target: recipient.email
}
);
})
);
return { ok: true as const, sentCount: recipients.length };
}
export async function sendSmtpTestEmail(params: {
to: string;
companyName: string;
smtp?: SmtpConfigInput;
}) {
const settings = await getSettings([
SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID,
SETTING_KEYS.EMAIL_SUBJECT_SMTP_TEST,
SETTING_KEYS.EMAIL_TEMPLATE_SMTP_TEST
]);
const transport = await createTransporter(params.smtp);
if (!transport) {
return { ok: false as const, message: "SMTP ist nicht konfiguriert." };
}
const { cfg, transporter } = transport;
const styleId = settings[SETTING_KEYS.EMAIL_STYLE_TEMPLATE_ID];
const values: Record<string, string> = {
companyName: params.companyName,
recipientEmail: params.to,
email: params.to,
timestamp: new Date().toISOString()
};
const subject = renderTemplate(
settings[SETTING_KEYS.EMAIL_SUBJECT_SMTP_TEST],
values
);
const body = renderTemplate(settings[SETTING_KEYS.EMAIL_TEMPLATE_SMTP_TEST], values);
const styled = renderStyledEmail({
styleId,
subject,
companyName: params.companyName,
heading: subject,
body,
footerNote: ""
});
try {
await sendMailWithRetry(
transporter,
{
from: cfg.from,
to: params.to,
subject,
text: styled.text,
html: styled.html
},
{
operation: "smtp-test",
target: params.to
}
);
return { ok: true as const };
} catch (error) {
const message = error instanceof Error ? error.message : "SMTP-Test fehlgeschlagen";
return { ok: false as const, message };
}
}