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,