diff --git a/.env.example b/.env.example
index 45274f36057e014a4ab5448a568feffb2e5eae15..0a9d99796da68c294f59c9b4437917eb6d035fe2 100644
--- a/.env.example
+++ b/.env.example
@@ -2,3 +2,12 @@ DB_USERNAME="postgres"
 DB_PASSWORD="postgres"
 DB_NAME="postgres"
 BODY_SIZE_LIMIT=10485760 # 10MiB
+MAIL_HOST="domain.tld"
+MAIL_PORT=465
+MAIL_USER="noreply@domain.tld"
+MAIL_PASSWORD="secure-password"
+MAIL_FROM_ADDRESS="noreply@domain.tld"
+MAIL_FROM_NAME="Getränkekasse"
+MAIL_TLS=true
+MAIL_AUTH_METHOD="plain"
+MAIL_REPLY_TO="getraenke@domain.tld" # optional, sets the reply-to header, useful if you don't read the mailbox of MAIL_USER
diff --git a/docker-compose.yml.example b/docker-compose.yml.example
index dc70a894a2cb9bc15234d46e134e80517a5e1dc4..b80af6026e3a4c9acd6064db5e2d92d446c723a6 100644
--- a/docker-compose.yml.example
+++ b/docker-compose.yml.example
@@ -29,6 +29,15 @@ services:
     environment:
       BODY_SIZE_LIMIT: ${BODY_SIZE_LIMIT}
       DATABASE_URL: postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
+      MAIL_HOST: ${MAIL_HOST}
+      MAIL_PORT: ${MAIL_PORT}
+      MAIL_USER: ${MAIL_USER}
+      MAIL_PASSWORD: ${MAIL_PASSWORD}
+      MAIL_FROM_NAME: ${MAIL_FROM_NAME}
+      MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
+      MAIL_TLS: ${MAIL_TLS}
+      MAIL_AUTH_METHOD: ${MAIL_AUTH_METHOD}
+      MAIL_REPLY_TO: ${MAIL_REPLY_TO}
     volumes:
       - ./article-images:/app/article-images
 
diff --git a/package-lock.json b/package-lock.json
index 1dce469b35e98e52fb221191ddca416aef195a07..b9c1726e1a01bd63bd59af1ae46ba3a1857ddbfe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
 				"@sveltejs/adapter-node": "^1.2.1",
 				"cookie": "^0.5.0",
 				"nanoid": "^4.0.1",
+				"nodemailer": "^6.9.4",
 				"prisma": "^4.11.0"
 			},
 			"devDependencies": {
@@ -1666,6 +1667,14 @@
 			"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
 			"dev": true
 		},
+		"node_modules/nodemailer": {
+			"version": "6.9.4",
+			"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
+			"integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==",
+			"engines": {
+				"node": ">=6.0.0"
+			}
+		},
 		"node_modules/once": {
 			"version": "1.4.0",
 			"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3426,6 +3435,11 @@
 			"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
 			"dev": true
 		},
+		"nodemailer": {
+			"version": "6.9.4",
+			"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
+			"integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA=="
+		},
 		"once": {
 			"version": "1.4.0",
 			"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
diff --git a/package.json b/package.json
index a6cb806f9209143dcf3320555d65733dac4353cf..25ee4fb7c12e51f987b390c518db6c186ceb94b6 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
 		"@sveltejs/adapter-node": "^1.2.1",
 		"cookie": "^0.5.0",
 		"nanoid": "^4.0.1",
+		"nodemailer": "^6.9.4",
 		"prisma": "^4.11.0"
 	}
 }
diff --git a/src/lib/notifications/channelTypes.js b/src/lib/notifications/channelTypes.js
index ffbf120472a1eed547104364d6177124199144d9..766048e9c1aae7d3791e927407ae54b5e68d08f5 100644
--- a/src/lib/notifications/channelTypes.js
+++ b/src/lib/notifications/channelTypes.js
@@ -1,5 +1,5 @@
-import { handleWebhook } from "./webhookHandler";
-import { handleEmail } from "./emailHandler";
+import { handleWebhook } from "../server/notifications/webhookHandler";
+import { handleEmail } from "../server/notifications/emailHandler";
 
 /**
  * @type {{
diff --git a/src/lib/notifications/emailHandler.ts b/src/lib/notifications/emailHandler.ts
deleted file mode 100644
index e5e6fc7ce07c7643e2e77a12171ba2df9c2cead8..0000000000000000000000000000000000000000
--- a/src/lib/notifications/emailHandler.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { formatMessage, getMessageTitle } from "./formatter";
-import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types";
-
-export async function handleEmail<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
-	const emailTitle = getMessageTitle(type);
-	const emailBody = formatMessage(type, data);
-	return Promise.resolve({status: "success"}); // TODO actually send email
-};
diff --git a/src/lib/server/mail.js b/src/lib/server/mail.js
new file mode 100644
index 0000000000000000000000000000000000000000..a1af296bc4ccd2b376a276bdbf0c474296d8e6c8
--- /dev/null
+++ b/src/lib/server/mail.js
@@ -0,0 +1,34 @@
+import nodemailer from "nodemailer";
+
+const transporter = nodemailer.createTransport({
+	host: process.env.MAIL_HOST,
+	port: parseInt(process.env.MAIL_PORT),
+	auth: {
+		user: process.env.MAIL_USER,
+		pass: process.env.MAIL_PASSWORD
+	},
+	secure: process.env.MAIL_TLS === "true",
+	authMethod: process.env.MAIL_AUTH_METHOD
+});
+
+/**
+ * @param {Object} mailOptions
+ * @param {string} mailOptions.to The email address to send the email to
+ * @param {string} mailOptions.subject The subject of the email
+ * @param {string} mailOptions.text The content of the email
+ * @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};
+	return transporter.sendMail({
+		subject,
+		to,
+		from: {
+			address: process.env.MAIL_FROM_ADDRESS,
+			name: process.env.MAIL_FROM_NAME
+		},
+		replyTo: process.env.MAIL_REPLY_TO,
+		...content
+	});
+}
diff --git a/src/lib/server/notifications/emailHandler.ts b/src/lib/server/notifications/emailHandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b06fc3c68e2e69551b831050e9af1c37be6d1ff3
--- /dev/null
+++ b/src/lib/server/notifications/emailHandler.ts
@@ -0,0 +1,16 @@
+import { sendMail } from "../mail";
+import { formatMessage, 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)
+	}).then(res=>{
+		if(res.accepted.length > 0) return { status: "success" };
+		else if(res.pending.length > 0) throw "Email is pending";
+		else if(res.rejected.length > 0) throw "Email was rejected";
+		else throw "Unknown error";
+	});
+};
diff --git a/src/lib/notifications/formatter.ts b/src/lib/server/notifications/formatter.ts
similarity index 98%
rename from src/lib/notifications/formatter.ts
rename to src/lib/server/notifications/formatter.ts
index 1bb7a2d78e221005849bc8955629cfa392a34b94..f8a7425aa2a881cfffdb1cb386cf016e3df63e2a 100644
--- a/src/lib/notifications/formatter.ts
+++ b/src/lib/server/notifications/formatter.ts
@@ -1,4 +1,4 @@
-import type { NotificationData, NotificationType, PossibleNotificationType } from "./types";
+import type { NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types";
 
 export function getMessageTitle<T extends PossibleNotificationType>(type: NotificationType<T>): string {
 	switch(type.name){
diff --git a/src/lib/notifications/handler.ts b/src/lib/server/notifications/handler.ts
similarity index 92%
rename from src/lib/notifications/handler.ts
rename to src/lib/server/notifications/handler.ts
index 3ee3f4c986366e0abbe2905e0106e2f194d186bc..0ca34a4828657d3b136c98a2e4e4e54d0109a5c9 100644
--- a/src/lib/notifications/handler.ts
+++ b/src/lib/server/notifications/handler.ts
@@ -1,6 +1,6 @@
 import { db } from "$lib/server/database";
-import { ChannelType } from "./channelTypes";
-import type { HandlerReturnType, NotificationChannel, NotificationData, NotificationType, PossibleNotificationType } from "./types";
+import { ChannelType } from "../../notifications/channelTypes";
+import type { HandlerReturnType, NotificationChannel, NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types";
 
 export async function sendNotification<T extends PossibleNotificationType>(user: { id: number, notificationChannels: NotificationChannel[] | undefined } | number, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType[]>{
 	let notificationChannels: NotificationChannel[];
diff --git a/src/lib/notifications/webhookHandler.ts b/src/lib/server/notifications/webhookHandler.ts
similarity index 90%
rename from src/lib/notifications/webhookHandler.ts
rename to src/lib/server/notifications/webhookHandler.ts
index 9a5704adb50e338230db8ac6004f6850c622b973..30b234e339fd4325aba98cc3e05a8f7261954334 100644
--- a/src/lib/notifications/webhookHandler.ts
+++ b/src/lib/server/notifications/webhookHandler.ts
@@ -1,5 +1,5 @@
 import { formatMessage } from "./formatter";
-import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types";
+import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types";
 
 export async function handleWebhook<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
 	const body = JSON.stringify(Object.assign({}, data, {type: type.name, content: formatMessage(type, data)}));
diff --git a/src/routes/admin/api/users.js b/src/routes/admin/api/users.js
index d8e27f6e2e19b6106406383d8ca90bed96198a07..fdd1a723784a5af19327ea831ed0ea4ecacc700e 100644
--- a/src/routes/admin/api/users.js
+++ b/src/routes/admin/api/users.js
@@ -22,12 +22,12 @@ export async function getUsers(){
 }
 
 export async function getUser(id, includeCards=false, includeNotificationChannels=false){
-	let include = includeCards || includeNotificationChannels ? {} : undefined;
-	if(includeCards) include.cards = true;
-	if(includeNotificationChannels) include.notificationChannels = true;
 	return db.user.findUnique({
 		where: { id },
-		include
+		include: {
+			notificationChannels: includeNotificationChannels,
+			cards: includeCards
+		}
 	});
 }
 
diff --git a/src/routes/admin/user/[id]/+page.server.js b/src/routes/admin/user/[id]/+page.server.js
index c7fb2b4d025f7b7465c56d42e0160bfff13f49fe..654a367ab723e65dbdae77c58b1828871129ca54 100644
--- a/src/routes/admin/user/[id]/+page.server.js
+++ b/src/routes/admin/user/[id]/+page.server.js
@@ -7,7 +7,11 @@ import { createNotificationChannel, updateNotificationChannel } from "../../api/
 export const load = async ({ params }) => {
 	const { id } = params;
 	const user = await getUser(parseInt(id), true, true);
-	return { user };
+	const channelTypes = {};
+	for(const [name, {key}] of Object.entries(ChannelType)){
+		channelTypes[name] = key;
+	}
+	return { user, channelTypes };
 }
 
 export const actions = {
diff --git a/src/routes/admin/user/[id]/+page.svelte b/src/routes/admin/user/[id]/+page.svelte
index 869bd2c38e2ba50816d147e839eeeb6dfde3baf8..aecaf5a93bd308ab3555e31e2108487c356df952 100644
--- a/src/routes/admin/user/[id]/+page.svelte
+++ b/src/routes/admin/user/[id]/+page.svelte
@@ -1,7 +1,7 @@
 <script>
-import CardList from "./CardList.svelte";
-import { enhance } from "$app/forms";
-import { addMessage, MessageType } from "$lib/messages";
+	import CardList from "./CardList.svelte";
+	import { enhance } from "$app/forms";
+	import { addMessage, MessageType } from "$lib/messages";
 	import NotificationChannelList from "./NotificationChannelList.svelte";
 
 	export let data;
@@ -40,4 +40,4 @@ import { addMessage, MessageType } from "$lib/messages";
 </form>
 
 <h2>Benachrichtigungen</h2>
-<NotificationChannelList bind:notificationChannels />
+<NotificationChannelList bind:notificationChannels channelTypes={data.channelTypes} />
diff --git a/src/routes/admin/user/[id]/NotificationChannelItem.svelte b/src/routes/admin/user/[id]/NotificationChannelItem.svelte
index 6b9dc20fec9629870dcdf613fbdc070d0afd8f69..04fdd9fb3c25ed77149bac51edd6d9e1d8579df1 100644
--- a/src/routes/admin/user/[id]/NotificationChannelItem.svelte
+++ b/src/routes/admin/user/[id]/NotificationChannelItem.svelte
@@ -1,16 +1,15 @@
 <script>
-	import { ChannelType } from "$lib/notifications/channelTypes";
 	import { NotificationType } from "$lib/notifications/notificationTypes";
 	import BitfieldSelector from "../../../../components/BitfieldSelector.svelte";
 
-	export let id, channelType, notificationTypes, recipient, edit, deleteChannel;
+	export let id, channelType, notificationTypes, recipient, edit, deleteChannel, channelTypes;
 </script>
 
 {#if edit}
 <tr>
 	<td>
 		<select name="channelType" form="notif-form{id}" required>
-			{#each Object.entries(ChannelType) as [name, {key}]}
+			{#each Object.entries(channelTypes) as [name, key]}
 				<option value={key} selected={key==channelType}>{name}</option>
 			{/each}
 		</select>
@@ -21,7 +20,7 @@
 </tr>
 {:else}
 <tr>
-	<td>{Object.entries(ChannelType).find(([,type])=>type.key==channelType)?.[0]}</td>
+	<td>{Object.entries(channelTypes).find(([,type])=>type==channelType)?.[0]}</td>
 	<td><span title={Object.values(NotificationType).filter(t=>t.key&notificationTypes).map(t=>t.name).join(", ")}>{notificationTypes}</span></td>
 	<td>{recipient}</td>
 	<td><button type="button" on:click={() => edit = true}>Bearbeiten</button><button on:click={deleteChannel}>Löschen</button></td>
diff --git a/src/routes/admin/user/[id]/NotificationChannelList.svelte b/src/routes/admin/user/[id]/NotificationChannelList.svelte
index cce8af454d0cab5eab7a554bed375a78eac4d3f3..be346c13d504bb6fa88c540d3f0980c29bb002fe 100644
--- a/src/routes/admin/user/[id]/NotificationChannelList.svelte
+++ b/src/routes/admin/user/[id]/NotificationChannelList.svelte
@@ -1,7 +1,6 @@
 <script>
 	import { enhance } from "$app/forms";
 	import { MessageType, addMessage } from "$lib/messages";
-	import { ChannelType } from "$lib/notifications/channelTypes";
 	import { NotificationType } from "$lib/notifications/notificationTypes";
 	import BitfieldSelector from "../../../../components/BitfieldSelector.svelte";
 	import NotificationChannelItem from "./NotificationChannelItem.svelte";
@@ -9,6 +8,8 @@
 	/** @type {import("$lib/notifications/types").NotificationChannel[]} */
 	export let notificationChannels;
 	let edit = notificationChannels.map(() => false);
+	/** @type {Object<string, number>} */
+	export let channelTypes;
 	
 	function deleteChannel(id){
 		fetch("/admin/api/notificationchannel", {
@@ -41,12 +42,12 @@
 		<th></th>
 	</tr>
 	{#each notificationChannels as channel, i}
-		<NotificationChannelItem {...channel} bind:edit={edit[i]} deleteChannel={()=>deleteChannel(channel.id)} />
+		<NotificationChannelItem {...channel} bind:edit={edit[i]} deleteChannel={()=>deleteChannel(channel.id)} {channelTypes} />
 	{/each}
 	<tr>
 		<td>
 			<select name="channelType" form="notif-form-new" required>
-				{#each Object.entries(ChannelType) as [name, {key}]}
+				{#each Object.entries(channelTypes) as [name, key]}
 					<option value={key}>{name}</option>
 				{/each}
 			</select>
diff --git a/src/routes/api/users.js b/src/routes/api/users.js
index 21cc3a02171c4146fb9fe7b2058247086c34058c..5b0f3765f88e50a55bef7cf0a8cf27792018a317 100644
--- a/src/routes/api/users.js
+++ b/src/routes/api/users.js
@@ -6,6 +6,6 @@ export async function getUser(card) {
 	return Object.assign(usercard.user, {card: usercard.card, perms: usercard.perms});
 }
 
-export async function getUserById(id, includeCards=false) {
-	return db.user.findUnique({where: {id}, include: {cards: includeCards}});
+export async function getUserById(id, includeCards=false, includeNotificationChannels=false) {
+	return db.user.findUnique({where: {id}, include: {cards: includeCards, notificationChannels: includeNotificationChannels}});
 }