diff --git a/src/lib/server/mail.js b/src/lib/server/mail.ts similarity index 71% rename from src/lib/server/mail.js rename to src/lib/server/mail.ts index 716043d30a10baaaa6b381755c6d68a4fd4ab684..109a116b4def709354b86db6154f20b7d4a2c650 100644 --- a/src/lib/server/mail.js +++ b/src/lib/server/mail.ts @@ -1,4 +1,5 @@ import nodemailer from "nodemailer"; +import type { Address } from "nodemailer/lib/mailer"; const transporter = nodemailer.createTransport({ host: process.env.MAIL_HOST, @@ -19,17 +20,29 @@ const transporter = nodemailer.createTransport({ * @param {boolean} [mailOptions.html=false] `true` if `text` is HTML, `false` if `text` is plain text * @returns */ -export async function sendMail({to, subject, text, html=false}) { - const content = html ? {html: text} : {text}; + +type MailOptions = { + to?: Address | string | Array<Address | string>, + cc?: Address | string | Array<Address | string>, + bcc?: Address | string | Array<Address | string>, + subject: string, + text: string, + html?: string +} + +export async function sendMail({to, cc, bcc, subject, text, html}: MailOptions) { const subjectPrefix = process.env.MAIL_SUBJECT_PREFIX ?? ""; return transporter.sendMail({ subject: subjectPrefix + subject, to, + cc, + bcc, from: { address: process.env.MAIL_FROM_ADDRESS, name: process.env.MAIL_FROM_NAME }, replyTo: process.env.MAIL_REPLY_TO, - ...content + text, + html }); } diff --git a/src/lib/server/notifications/emailHandler.ts b/src/lib/server/notifications/emailHandler.ts index b06fc3c68e2e69551b831050e9af1c37be6d1ff3..2b69aa65b8dc23edc0a453e050e4d32815fe2c62 100644 --- a/src/lib/server/notifications/emailHandler.ts +++ b/src/lib/server/notifications/emailHandler.ts @@ -1,12 +1,13 @@ import { sendMail } from "../mail"; -import { formatMessage, getMessageTitle } from "./formatter"; +import { formatMessage, formatMessageToHtml, getMessageTitle } from "./formatter"; import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types"; export async function handleEmail<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> { return sendMail({ to: recipient, subject: getMessageTitle(type), - text: formatMessage(type, data) + text: formatMessage(type, data), + html: formatMessageToHtml(type, data) }).then(res=>{ if(res.accepted.length > 0) return { status: "success" }; else if(res.pending.length > 0) throw "Email is pending"; diff --git a/src/lib/server/notifications/formatter.ts b/src/lib/server/notifications/formatter.ts index 6c117aafc45231b96b4f1b0e59920545de8e9b45..24fbfc974c245a956be24f33cac0d017649b2a7c 100644 --- a/src/lib/server/notifications/formatter.ts +++ b/src/lib/server/notifications/formatter.ts @@ -76,3 +76,62 @@ Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`; } } } + +export function formatMessageToHtml<T extends PossibleNotificationType>(type: NotificationType<T>, data: NotificationData<T>): string { + switch (type.name) { + case "buy": { + const { total, items, balanceBefore, balanceAfter } = data as NotificationData<"buy">; + const premiums = items.filter(item => item.premium && item.premium > 0).map(item => item.premium).reduce((a, b) => a + b, 0); + return `<p>Du hast folgende ${items.length} Artikel gekauft:</p> +<ul>${items.map(item => `<li>${escapeHtml(item.name)} (${escapeHtml(item.code)}) - ${(item.price / 100).toFixed(2)}€` + (item.premium && item.premium > 0 ? ` (+${(item.premium / 100).toFixed(2)}€)` : "") + `</li>`).join("")}</ul> +<p>Gesamt: ${(total / 100).toFixed(2)}€` + (premiums > 0 ? ` (davon ${(premiums / 100).toFixed(2)}€ Negativaufschlag)` : "") + `</p> +<p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>`; + } + case "refund": { + const { refund, item, balanceBefore, balanceAfter, timeBought } = data as NotificationData<"refund">; + const premiumMessage = refund.premium && refund.premium > 0 ? ` (+${(refund.premium / 100).toFixed(2)}€)` : ""; + return `<p>Dir wurden ${(refund.price / 100).toFixed(2)}€${premiumMessage} für ${escapeHtml(item.name)} (${escapeHtml(item.code)}) erstattet, gekauft am ${timeBought.toLocaleString()}.</p> +<p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>` + } + case "deposit": { + const { amount, balanceBefore, balanceAfter } = data as NotificationData<"deposit">; + return `<p>Du hast ${(amount / 100).toFixed(2)}€ eingezahlt.</p> +<p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>`; + } + case "withdraw": { + const { amount, balanceBefore, balanceAfter } = data as NotificationData<"withdraw">; + return `<p>Du hast ${(amount / 100).toFixed(2)}€ ausgezahlt.</p> +<p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>`; + } + case "use-voucher": { + const { voucher, balanceBefore, balanceAfter } = data as NotificationData<"use-voucher">; + return `<p>Du hast einen Gutschein im Wert von ${(voucher.value / 100).toFixed(2)}€ eingelöst.</p> +<p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>`; + } + case "send-transfer": { + const { amount, balanceBefore, balanceAfter, receiver } = data as NotificationData<"send-transfer">; + return `<p>Du hast ${(amount / 100).toFixed(2)}€ an ${escapeHtml(receiver.name)} überwiesen.</p> +<p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>`; + } + case "receive-transfer": { + const { amount, balanceBefore, balanceAfter, sender } = data as NotificationData<"receive-transfer">; + return `<p>Du hast ${(amount / 100).toFixed(2)}€ von ${escapeHtml(sender.name)} erhalten.</p> +<p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>`; + } + default: { + console.error(`Unknown notification type`, type); + return `Unknown notification type ${escapeHtml(JSON.stringify(type))}`; + } + } +} + +function escapeHtml(unsafe: string): string{ + const lookup = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + }; + return unsafe.replace(/[&<>"']/g, c => lookup[c]); +}