diff --git a/package-lock.json b/package-lock.json index 3c981f468839577a0e358ffbb33d47c09e97ef8f..9451e552d96256243b07532d1b35c7bc1adf33b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "feiertagejs": "^1.4.0", "flowbite-svelte": "^0.46.15", "flowbite-svelte-icons": "^2.0.0-next.18", + "handlebars": "^4.7.8", "ical-generator": "^8.0.1", "intl": "^1.2.5", "leaflet": "^1.9.4", @@ -4401,6 +4402,27 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -5613,6 +5635,12 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -7244,6 +7272,15 @@ "node": ">=18" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8124,6 +8161,19 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -8451,6 +8501,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 56721b0702074f10887df936d22232577effe0b4..2d28b5f60e8aba8f56f953c63a438f0440d190df 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "feiertagejs": "^1.4.0", "flowbite-svelte": "^0.46.15", "flowbite-svelte-icons": "^2.0.0-next.18", + "handlebars": "^4.7.8", "ical-generator": "^8.0.1", "intl": "^1.2.5", "leaflet": "^1.9.4", diff --git a/src/lib/components/MailInput.svelte b/src/lib/components/MailInput.svelte index 0f36d24f9bdba21ecadd25d108e36f947c3a522b..974e1097d925c8811bdfd044b2e76e25bda839f0 100644 --- a/src/lib/components/MailInput.svelte +++ b/src/lib/components/MailInput.svelte @@ -2,7 +2,7 @@ import Degree from "$lib/degrees"; import Gender from "$lib/genders"; import { locale, locales } from "$lib/i18n/i18n"; - import { variables, conditions, renderTemplate, parseTemplate, compileTemplate, formatters, subjectFormatter, type PartialConfig, type TemplateType } from "$lib/mail"; + import { variables, conditions, compileTemplate, type PartialConfig, type TemplateType } from "$lib/mail"; import type { StudyProgram } from "$lib/server/database/entities/StudyProgram.entity"; import type { Tutor } from "$lib/server/database/entities/Tutor.entity"; import type { TutorTraining } from "$lib/server/database/entities/TutorTraining.entity"; @@ -60,18 +60,18 @@ password: "password123", }); - let [mailHtml, compilationError]: [string, string|null] = $derived.by(()=>{ + let [mailSubject, mailHtml, compilationError]: [string, string, Error|null] = $derived.by(()=>{ try { - return [renderTemplate(compileTemplate(type, parseTemplate(type, text), exampleTutor, config)), null] as const; - } catch(ex) { - return ["", (ex as Error).message] as const; - } - }); - let [compiledSubject, subjectError]: [string, string|null] = $derived.by(()=>{ - try { - return [compileTemplate(type, parseTemplate(type, subject), exampleTutor, config, subjectFormatter), null] as const; - } catch(ex) { - return ["", (ex as Error).message] as const; + const mailTemplate = compileTemplate({type: "tutor", text, subject, replyTo: "-"}); + const data = {...exampleTutor, ...config}; + return [ + mailTemplate.subject(data), + mailTemplate.html(data), + null, + ]; + } catch (error) { + console.error("Error compiling mail template:", error); + return ["", "", error] as [string, string, Error|null]; } }); </script> @@ -106,9 +106,6 @@ </Label> {/each} </div> -{#if subjectError} -<Helper color="red">{subjectError}</Helper> -{/if} {#each locales as locale} <Label class="mb-1"> Text ({locale}) @@ -116,15 +113,17 @@ </Label> {/each} {#if compilationError} -<Helper color="red">{compilationError}</Helper> +<Helper color="red"> + <pre>{compilationError.message.replace("<br>", "")}</pre> +</Helper> {/if} <H2>Vorschau</H2> -<Span>{compiledSubject}</Span> +<Span>{mailSubject}</Span> <iframe srcdoc={mailHtml} title="Email Preview" class="bg-gray-200 w-full mb-4" onload={function(){this.style.height=Math.ceil(this.contentWindow.document.documentElement.getBoundingClientRect().height)+"px";}}></iframe> -<Button color="green" onclick={onsubmit} type={buttonType} class="mb-3 min-w-40" disabled={buttonDisabled}>{buttonText}</Button> +<Button color="green" onclick={onsubmit} type={buttonType} class="mb-3 min-w-40" disabled={buttonDisabled || !!compilationError}>{buttonText}</Button> <Accordion> <AccordionItem> @@ -175,70 +174,14 @@ </AccordionItem> <AccordionItem> <Span slot="header">Variablen</Span> - <List tag="ul" position="outside" class="ml-4"> - {#each variables[type] as variable} - <Li><code>{"{{"}{variable.name}{"}}"}</code> - {variable.description}</Li> - {/each} - </List> - </AccordionItem> - <AccordionItem> - <Span slot="header">Formatierer</Span> - <P class="mb-2">Formatierer können mit Argumenten erstellt werden. Dafür nutzt man <code>formatierer:argument</code>, wobei das Argument gültiges JSON sein muss. Es muss darauf geachtet werden, dass sich kein <code>{"}}"}</code> bildet.</P> - <List tag="ul" position="outside" class="ml-4"> - {#each Object.entries(formatters) as [name, {description}]} - <Li><code>{name}</code> - {description}</Li> - {/each} - </List> - </AccordionItem> - <AccordionItem> - <Span slot="header">Bedingungen</Span> - <List tag="ul" position="outside" class="ml-4"> - {#each conditions[type] as condition} - <Li> - <code>{"{{if "}{condition.name}{#if condition.arguments}{" ...args"}{/if}{"}}"}</code> - {condition.description} - {#if condition.arguments} - <br />Argumente: {condition.arguments} - {/if} - </Li> - {/each} - </List> + <P> + TODO: hier eine Liste der verfügbaren Variablen einfügen, die in den Templates verwendet werden können. + </P> </AccordionItem> <AccordionItem> <Span slot="header">Syntax</Span> <P> - In den E-Mail-Eingaben können Variablen und bedingte Anweisungen verwenden werden, um den Inhalt dynamisch anzupassen. Die folgenden Regeln gelten: - <List tag="ul" position="outside" class="ml-4 mt-2"> - <Li> - <b>Variablen</b> - <List tag="ul" position="outside" class="ml-6"> - <Li>Variablen werden durch doppelte geschweifte Klammern gekennzeichnet: <code>{"{{"}variablenname{"}}"}</code>.</Li> - </List> - </Li> - <Li> - <b>Formatierung</b> - <List tag="ul" position="outside" class="ml-6"> - <Li>Werte von Variablen können formatiert werden, indem die Formatierer getrennt durch <code>|</code> nach dem Variablennamen gelistet werden, z.B. <code>{"{{"}variablenname|formatierer1|formatierer2{"}}"}</code>. Die Formatierer werden in der angegebenen Reihenfolge angewandt.</Li> - <Li>Unterstützt ein Formatierer Konstruktorenargumente, so können diese mit <code>:</code> getrennt hinter dem Namen angegeben werden. Das Argument muss gültiges JSON sein und kein frühzeitiges {"}}"} bilden. Beispiel: <code>{"{{"}argument|date:{"{"}"year":"2-digit"{"} "}{"}}"}</code>, wobei das Leerzeichen am Ende wichtig ist, weil sich sonst {"}}}"} bilden würde.</Li> - </List> - </Li> - <Li> - <b>Bedingte Anweisungen</b> - <List tag="ul" position="outside" class="ml-6"> - <Li>Bedingter Text kann mit <code>{"{{"}if bedingung{"}}"}</code> angezeigt werden.</Li> - <Li>Argumente für Bedingungen werden durch Leerzeichen getrennt nach der Bedingung geschrieben: <code>{"{{"}if bedingung arg1 arg2{"}}"}</code>.</Li> - <Li>Ende eines if-Blocks wird mit <code>{"{{/if}}"}</code> markiert.</Li> - <Li>Alternativer Text kann mit <code>{"{{"}else{"}}"}</code> definiert werden, um den Fall zu behandeln, dass die Bedingung falsch ist.</Li> - <Li>Zusätzliche Bedingungen innerhalb eines if-Blocks können mit <code>{"{{"}else if bedingung arg1 arg2{"}}"}</code> geprüft werden. Der Inhalt wird angezeigt, wenn die vorherige Bedingung falsch ist, aber die Bedingung des <code>else if</code> selbst wahr ist.</Li> - <Li>Negation einer Bedingung kann mit <code>{"{{"}if not bedingung arg1 arg2{"}}"}</code> durchgeführt werden.</Li> - </List> - </Li> - <Li> - <b>Markdown-Unterstützung</b> - <List tag="ul" position="outside" class="ml-6"> - <Li>Das Textfeld unterstützt Markdown zur Formatierung des Textes. Einen Leitfaden dazu ist <A href="https://www.markdownguide.org/basic-syntax/" target="_blank" rel="noopener">hier</A> zu finden.</Li> - </List> - </Li> - </List> + TODO: auf handlebars verweisen und hier Kurzzusammenfassung geben </P> </AccordionItem> </Accordion> diff --git a/src/lib/mail.ts b/src/lib/mail.ts index e7459db5223a491ca4fa28ffa68ff37f6018087f..22b24e9da8aae59aaa867dd6e184787de936126b 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -6,6 +6,7 @@ import markdownit from "markdown-it"; import type { Localized } from "$lib/utils"; import type { Config } from "$lib/server/config"; import type { DateTimeFormatOptions } from "intl"; +import Handlebars from "handlebars"; const md = markdownit({ breaks: true, @@ -22,61 +23,6 @@ export type MailTemplate = { type: TemplateType; }; -export const formatters: Record<string, { description: string, format: (l: Locale, arg: unknown, constructorArg: unknown|null)=>string }> = { - date: { - description: "Formatiert ein Datum, Kontruktorargumente sind die Optionen für Intl.DateTimeFormat (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options)", - format: (locale, arg, constructorArg)=>new Intl.DateTimeFormat(locale, constructorArg as DateTimeFormatOptions ?? undefined).format(arg as Date), - }, - dateLong: { - description: "Formatiert ein Datum, z.B. 01. Januar 1970", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).format(arg as Date), - }, - dateShort: { - description: "Formatiert ein Datum, z.B. 1. Januar", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { month: "long", day: "numeric" }).format(arg as Date), - }, - dateWeekday: { - description: "Gibt den Wochentag eines Datums zurück, z.B. Montag", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { weekday: "long" }).format(arg as Date), - }, - dateWeekdayShort: { - description: "Gibt den Wochentag eines Datums zurück, z.B. Mo", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { weekday: "short" }).format(arg as Date), - }, - dateRange: { - description: "Formatiert einen Datumsbereich, Konsruktorargumente sind die Optionen für Intl.DateTimeFormat (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options)", - format: (locale, arg, constructorArg)=>new Intl.DateTimeFormat(locale, constructorArg as DateTimeFormatOptions ?? undefined).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]), - }, - dateRangeLong: { - description: "Formatiert einen Datumsbereich, z.B. 1. - 2. Januar 1970", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]), - }, - dateRangeShort: { - description: "Formatiert einen Datumsbereich, z.B. 1. - 2. Januar", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { month: "long", day: "numeric" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]), - }, - time: { - description: "Formatiert eine Uhrzeit, z.B. 12:34", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit" }).format(arg as Date), - }, - timeRange: { - description: "Formatiert einen Zeitbereich, z.B. 12:34 - 13:45 Uhr", - format: (locale, arg)=>new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]), - }, - lowercase: { - description: "Konvertiert den Text in Kleinbuchstaben", - format: (_, arg)=>String(arg).toLowerCase(), - }, - uppercase: { - description: "Konvertiert den Text in Großbuchstaben", - format: (_, arg)=>String(arg).toUpperCase(), - }, - titlecase: { - description: "Konvertiert den ersten Buchstaben jedes Wortes in Großbuchstaben", - format: (_, arg)=>String(arg).replace(/\b\w/g, l=>l.toUpperCase()), - }, -}; - export const types = ["tutor"] as const; export type TemplateType = typeof types[number]; @@ -289,174 +235,16 @@ export const conditions: Record<TemplateType, Condition[]> = { ], }; -export const bodyFormatter = ({ de, en }: Localized) => `-------- english version below --------\n\n${de}\n\n-------- english version --------\n\n${en}`; -export const subjectFormatter = ({ de, en }: Localized) => [de, en].filter(s=>s).join(" / "); - -export function compileTemplate(type: TemplateType, parts: Localized<Part[]>, tutor: Tutor, config: PartialConfig, formatter: (l: Localized)=>string = bodyFormatter): string { - const result = {} as Localized<string>; - for(const locale of locales){ - result[locale] = parts[locale].map(p=>compilePart(type, p, tutor, locale, config)).join(""); - } - return formatter(result); -} - -function compilePart(type: TemplateType, part: Part, tutor: Tutor, locale: Locale, config: PartialConfig): string { - switch(part.type){ - case "text": - return part.text; - case "variable": - return String(part.replacement(tutor, locale, config)); - case "condition": - if(part.condition(tutor, part.args)){ - return part.then.map(p=>compilePart(type, p, tutor, locale, config)).join(""); - }else{ - return part.else.map(p=>compilePart(type, p, tutor, locale, config)).join(""); - } - } -} - -type Token = {type: "string"; value: string} | {type: "template", value: string}; -function tokenize(input: string): Token[] { - const tokens: Token[] = []; - let currentContent = ""; - let inTemplate = false; - for(let i = 0; i < input.length; i++){ - const c = input[i]; - if(!inTemplate){ - if(c === "{" && input[i+1] === "{"){ - if(currentContent){ - tokens.push({type: "string", value: currentContent}); - currentContent = ""; - } - inTemplate = true; - i++; - }else{ - currentContent += c; - } - }else{ - if(c === "}" && input[i+1] === "}"){ - if(!inTemplate) throw new Error(`Unexpected }} at ${i}`); - tokens.push({type: "template", value: currentContent}); - currentContent = ""; - inTemplate = false; - i++; - }else{ - currentContent += c; - } - } - } - if(currentContent){ - if(inTemplate){ - throw new Error("Unexpected end of template"); - }else{ - tokens.push({type: "string", value: currentContent}); - } - } - return tokens; -} - -type Part = {type: "text", text: string} | {type: "variable", replacement: Variable["replacement"]} | {type: "condition", condition: Condition["condition"], args: string[], then: Part[], else: Part[]}; -export function parseTemplate(type: TemplateType, template: Localized): Localized<Part[]>; -export function parseTemplate(type: TemplateType, template: string): Part[]; -export function parseTemplate(type: TemplateType, template: Localized|string){ - if(typeof template === "object"){ - const result = {} as Localized<Part[]>; - for(const locale of locales){ - result[locale] = parseTemplate(type, template[locale]); - } - return result; - } - const parts: Part[] = []; - const tokens = tokenize(template); - while(tokens.length){ - const token = tokens.shift()!; - if(token.type === "string"){ - parts.push({type: "text", text: token.value}); - }else if(token.type === "template"){ - const [name, ...args] = token.value.split(" "); - if(name === "if" && args.length > 0){ - const conditionName = args.shift(); - const condition = conditions[type].find(c=>c.name === conditionName); - if(!condition) throw new Error(`Unknown condition ${conditionName}`); - const [then, els] = consumeCondition(type, tokens); - parts.push({type: "condition", condition: condition.condition, args, then, else: els}); - }else{ - const [variableName, ...formatterNames] = name.split("|"); - const variable = variables[type].find(v=>v.name === variableName); - if(!variable) throw new Error(`Unknown variable ${name}`); - const fs = formatterNames.map(f=>{ - const [formatterName, ...formatterArgs] = f.split(":"); - const formatter = formatters[formatterName]; - if(!formatter) throw new Error(`Unknown formatter ${formatterName}`); - const arg = formatterArgs.join(":"); - return { format: formatter.format, arg: arg ? JSON.parse(arg) : null }; - }); - const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{ - return fs.reduce((v, f)=>f.format(l, v, f.arg), variable.replacement(t, l, c)); - }; - parts.push({type: "variable", replacement}); - } - } - } - return parts; -} - -function consumeCondition(type: TemplateType, tokens: Token[]): [Part[], Part[]] { - const thenParts: Part[] = []; - const elseParts: Part[] = []; - let inElse = false; - while(tokens.length){ - const token = tokens.shift()!; - if(token.type === "string"){ - if(inElse) elseParts.push({type: "text", text: token.value}); - else thenParts.push({type: "text", text: token.value}); - }else if(token.type === "template"){ - const [name, ...args] = token.value.split(" "); - if(name === "if" && args.length > 0){ - const conditionName = args.shift(); - const condition = conditions[type].find(c=>c.name === conditionName); - if(!condition) throw new Error(`Unknown condition ${conditionName}`); - const [then, els] = consumeCondition(type, tokens); - const part: Part = {type: "condition", condition: condition.condition, args, then, else: els}; - if(inElse) elseParts.push(part); - else thenParts.push(part); - }else if(name === "else"){ - if(inElse) throw new Error("Multiple else blocks"); - if(args.length > 0) { - if(args[0] !== "if") throw new Error(`Unexpected argument "${args[0]}" to else`); - const [, name, ...args2] = args; - const condition = conditions[type].find(c=>c.name === name); - if(!condition) throw new Error(`Unknown condition ${name}`); - const [then, els] = consumeCondition(type, tokens); - const part: Part = {type: "condition", condition: condition.condition, args: args2, then, else: els}; - elseParts.push(part); - return [thenParts, elseParts]; - } - inElse = true; - }else if(name === "/if"){ - return [thenParts, elseParts]; - }else{ - const [variableName, ...formatterNames] = name.split("|"); - const variable = variables[type].find(v=>v.name === variableName); - if(!variable) throw new Error(`Unknown variable ${name}`); - const fs = formatterNames.map(f=>{ - const [formatterName, ...formatterArgs] = f.split(":"); - const formatter = formatters[formatterName]; - if(!formatter) throw new Error(`Unknown formatter ${formatterName}`); - const arg = formatterArgs.join(":"); - return { format: formatter.format, arg: arg ? JSON.parse(arg) : null }; - }); - const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{ - return fs.reduce((v, f)=>f.format(l, v, f.arg), variable.replacement(t, l, c)); - }; - if(inElse) elseParts.push({type: "variable", replacement}); - else thenParts.push({type: "variable", replacement}); - } - } - } - throw new Error("Unexpected end of template"); -} +const bodyFormatter = ({ de, en }: Localized) => `-------- english version below --------\n\n${de}\n\n-------- english version --------\n\n${en}`; +const subjectFormatter = ({ de, en }: Localized) => [de, en].filter(s=>s).join(" / "); -export function renderTemplate(compiledMarkdown: string): string { - return md.render(compiledMarkdown); +export function compileTemplate(template: MailTemplate){ + const subjectTemplate = Handlebars.compile(subjectFormatter(template.subject)); + const textTemplate = Handlebars.compile(bodyFormatter(template.text)); + const htmlTemplate = Handlebars.compile(md.render(bodyFormatter(template.text))); + return { + subject: subjectTemplate, + text: textTemplate, + html: htmlTemplate, + }; } diff --git a/src/lib/server/mail.ts b/src/lib/server/mail.ts index e612fde74fb6ed2b2c1562c54f5165a930e676c3..2e811a1e49005058054e93452008a43aeed4bab9 100644 --- a/src/lib/server/mail.ts +++ b/src/lib/server/mail.ts @@ -2,7 +2,7 @@ import { env } from "$env/dynamic/private"; import nodemailer from "nodemailer"; import { Tutor } from "$lib/server/database/entities/Tutor.entity"; import type Mail from "nodemailer/lib/mailer"; -import { compileTemplate, parseTemplate, renderTemplate, subjectFormatter, type MailTemplate } from "$lib/mail"; +import { compileTemplate, type MailTemplate } from "$lib/mail"; import { Config } from "$lib/server/config"; import ical from "ical-generator"; import type SMTPTransport from "nodemailer/lib/smtp-transport"; @@ -119,18 +119,17 @@ export function sendMail({ template, tutors, attachments = [], from, icalEvent } icalEvent?: Mail.IcalAttachment, }): Promise<SendMailResult> { const replyTo = template.replyTo === "-" ? undefined : template.replyTo; - const textParts = parseTemplate(template.type, template.subject); - const subjectParts = parseTemplate(template.type, template.subject); + const mailTemplate = compileTemplate(template); const config = Config.get(); return Promise.all(tutors.map(tutor => { - const compiled = compileTemplate(template.type, textParts, tutor, {...config, domain: env.DOMAIN||""}); - const rendered = renderTemplate(compiled); - const subject = compileTemplate(template.type, subjectParts, tutor, {...config, domain: env.DOMAIN||""}, subjectFormatter); + const subject = mailTemplate.subject({...tutor, ...config, domain: env.DOMAIN||""}); + const text = mailTemplate.text({...tutor, ...config, domain: env.DOMAIN||""}); + const html = mailTemplate.markdown({...tutor, ...config, domain: env.DOMAIN||""}); return transporter.sendMail({ to: tutor.email, from: from || env.MAIL_FROM, - text: compiled, - html: rendered, + text, + html, subject, attachments, replyTo,