+{{/each}}
+```
+
+### Available Template Variables
+
+All templates have access to the following standard variables:
+
+#### Recipient Information
+
+- `{{recipient.email}}` - Email address
+- `{{recipient.name}}` - Display name (staff only, clients for privacy)
+- `{{recipient.language}}` - Preferred language (de/en)
+
+#### Tenant Branding
+
+- `{{tenant.longName}}` - Organization name
+- `{{tenant.logo}}` - Base64-encoded logo image
+- `{{tenant.primaryColor}}` - Primary brand color (hex)
+- `{{tenant.secondaryColor}}` - Secondary brand color (hex)
+- `{{tenant.backgroundColor}}` - Background color (hex)
+
+#### Template-Specific Variables
+
+Additional variables depend on the template type (see table above).
+
+## Multi-Language Support
+
+### Adding New Languages
+
+1. **Create Template Files**
+
+ ```bash
+ # Example for French (fr)
+ cp user-created.de.html user-created.fr.html
+ cp user-created.de.txt user-created.fr.txt
+ ```
+
+2. **Update Language Type**
+
+ ```typescript
+ // In src/lib/server/email/template-engine.ts
+ export type Language = "de" | "en" | "fr";
+ ```
+
+3. **Translate Content**
+ - Update all text content to the target language
+ - Maintain all template variables (`{{...}}`)
+ - Test with different tenant branding
+
+### Language Fallback
+
+The system automatically falls back to German (DE) if a template doesn't exist in the requested language:
+
+```
+Request: user-created.fr.html
+Not found → Fallback to: user-created.de.html
+```
+
+## Tenant Branding Configuration
+
+### Brand Colors
+
+Tenants can customize three color values that are automatically applied to email templates:
+
+```typescript
+interface TenantBranding {
+ primaryColor: string; // Main brand color (buttons, links, logos)
+ secondaryColor: string; // Accent color (success messages, highlights)
+ backgroundColor: string; // Email background color
+}
+```
+
+**Example Usage in Templates:**
+
+```css
+.logo {
+ color: {{tenant.primaryColor}};
+}
+
+.button {
+ background-color: {{tenant.primaryColor}};
+}
+
+.success {
+ background-color: {{tenant.secondaryColor}};
+}
+
+body {
+ background-color: {{tenant.backgroundColor}};
+}
+```
+
+### Logo Integration
+
+Tenant logos are stored as binary data in the database and automatically converted to base64 for email embedding:
+
+```html
+{{#if tenant.logo}}
+
+{{/if}}
+
{{tenant.longName}}
+```
+
+**Supported Logo Formats:**
+
+- PNG (recommended)
+- JPEG
+- GIF
+- WEBP
+
+**Recommendations:**
+
+- Maximum size: 200x100 pixels
+- Transparent backgrounds for PNG
+- Optimize file size for email delivery
+
+## SMTP Configuration
+
+### Environment Variables
+
+Configure email delivery through environment variables:
+
+```bash
+# Required SMTP settings
+SMTP_HOST=smtp.example.com
+SMTP_PORT=587
+SMTP_SECURE=false
+SMTP_USER=username@example.com
+SMTP_PASS=password
+
+# Optional sender information
+SMTP_FROM_NAME="Your Organization"
+SMTP_FROM_EMAIL=noreply@example.com
+```
+
+### Security Considerations
+
+- Use TLS/SSL for SMTP connections (`SMTP_SECURE=true` for port 465)
+- Store credentials securely (environment variables, secrets management)
+- Consider using app-specific passwords for G-Mail/Outlook
+- Implement proper SPF, DKIM, and D-MARC records
+
+## Privacy and Data Protection
+
+### End-to-End Encryption
+
+The email system is designed with privacy in mind:
+
+- **Client Emails**: Optional field, may be empty for privacy
+- **Staff Emails**: Required for administrative notifications
+- **No Personal Data**: Templates avoid exposing sensitive information
+- **Minimal Logging**: Only delivery status is logged, not content
+
+### Data Handling
+
+```typescript
+// Client email creation (privacy-focused)
+const clientRecipient = {
+ email: user.email || "", // May be empty
+ name: undefined, // Never stored for clients
+ language: user.language || "de"
+};
+
+// Staff email creation
+const staffRecipient = {
+ email: user.email, // Always required
+ name: user.name || undefined, // Optional display name
+ language: user.language || "de"
+};
+```
+
+## Testing and Development
+
+### Running Tests
+
+```bash
+# Run email system tests
+npm run test src/lib/server/email/__tests__/email-system.test.ts
+
+# Run all tests
+npm run test
+```
+
+### Email Testing in Development
+
+The system automatically uses test mode in development:
+
+- No actual emails are sent
+- SMTP configuration is mocked
+- Template rendering is fully tested
+- Console logging shows email content
+
+### Template Development Workflow
+
+1. **Create/Edit Templates**
+
+ ```bash
+ # Edit existing template
+ vim src/lib/server/email/templates/user-created.de.html
+ ```
+
+2. **Test Template Rendering**
+
+ ```typescript
+ // Add test case in email-system.test.ts
+ const result = await templateEngine.renderTemplate("user-created", {
+ recipient: { email: "test@example.com", name: "Test User" },
+ subject: "Test Subject",
+ language: "de",
+ tenant: mockTenant
+ });
+ ```
+
+3. **Validate Output**
+ - Check HTML rendering
+ - Verify variable substitution
+ - Test conditional logic
+ - Validate across languages
+
+## Troubleshooting
+
+### Common Issues
+
+**Template Not Found**
+
+```
+Error: Template file not found: template-name.de.html
+```
+
+- Verify file exists in `src/lib/server/email/templates/`
+- Check file naming convention
+- Ensure file permissions are correct
+
+**SMTP Connection Failed**
+
+```
+Error: SMTP configuration is incomplete
+```
+
+- Verify all required environment variables
+- Test SMTP credentials manually
+- Check network connectivity and firewall rules
+
+**Variable Not Substituted**
+
+```
+Email shows: Hello {{recipient.name}}
+```
+
+- Verify variable name spelling
+- Check data structure matches template expectations
+- Ensure tenant object is properly passed
+
+### Debug Mode
+
+Enable detailed logging for email debugging:
+
+```typescript
+// In template-engine.ts, add console.log statements
+console.log("Template data:", JSON.stringify(data, null, 2));
+console.log("Rendered HTML:", renderedHtml);
+```
+
+## Security Best Practices
+
+1. **Template Security**
+
+ - Sanitize any user-generated content
+ - Avoid exposing sensitive data in templates
+ - Use HTTPS for all links in emails
+
+2. **SMTP Security**
+
+ - Use encrypted connections (TLS/SSL)
+ - Rotate SMTP credentials regularly
+ - Monitor for unauthorized access
+
+3. **Data Protection**
+ - Respect user privacy preferences
+ - Implement proper data retention policies
+ - Log minimal information for debugging
+
+## Future Enhancements
+
+### Extension Points
+
+The email system is designed for extensibility:
+
+- **New Template Types**: Add to `EmailTemplateType` enum
+- **Custom Variables**: Extend `TemplateData` interface
+- **Advanced Logic**: Enhance template engine syntax
+- **External Services**: Integration with email service providers
+
+## Support and Maintenance
+
+### Regular Maintenance Tasks
+
+1. **Monitor Delivery Rates**
+
+ - Check SMTP logs for delivery failures
+ - Monitor bounce rates and spam complaints
+ - Update DNS records as needed
+
+2. **Template Updates**
+
+ - Review templates for brand consistency
+ - Update content for policy changes
+ - Test across email clients
+
+3. **Security Updates**
+ - Keep dependencies updated
+ - Review SMTP security settings
+ - Audit access logs
+
+### Getting Help
+
+For technical support or questions about the email system:
+
+1. Check the test suite for usage examples
+2. Review JSDoc comments in source code
+3. Consult the main project documentation
+4. Submit issues via the project repository
+
+---
+
+_This documentation is maintained as part of the Open Reception project. For the latest updates, refer to the project repository._
diff --git a/docs/entity-relationship-diagram.md b/docs/entity-relationship-diagram.md
new file mode 100644
index 0000000..2af0afb
--- /dev/null
+++ b/docs/entity-relationship-diagram.md
@@ -0,0 +1,200 @@
+# Entity-Relationship Diagram
+
+## Appointment Booking Platform Data Structure
+
+Please note that this is currently the **unencrypted data model**. Appointments, Notes, Attachments and Answers of course have to be encrypted before being stored in the database.
+
+```mermaid
+erDiagram
+ ADMINS {
+ uuid id PK
+ string email
+ string password
+ }
+
+ TENANT {
+ uuid id PK
+ string short_name "Used as subdomain"
+ string long_name
+ text description "Optional"
+ blob logo "PNG, JPEG, GIF, or WEBP"
+ string brand_color
+ string default_language
+ int max_channels
+ int max_team_members
+ int auto_delete_days "Days after appointment to auto-delete"
+ boolean require_email "Requires clients to enter their e-mail address"
+ boolean require_phone "Requires clients to enter a phone number"
+ boolean url "Requires clients to enter a phone number"
+ }
+
+ CLIENT {
+ uuid id PK
+ string hash_key "Generated from email"
+ string public_key
+ string private_key_share
+ string email "Optional"
+ string language
+ }
+
+ STAFF {
+ uuid id PK
+ uuid role_id "Role of staff member (e.g. 'tenant-admin', 'staff')"
+ string hash_key "Generated from login name"
+ string public_key
+ string name "Optional"
+ string email
+ string language
+ }
+
+ TEAM {
+ uuid id PK
+ string name
+ string description "Optional"
+ blob image "PNG, JPEG, GIF, or WEBP"
+ }
+
+ CHANNEL {
+ uuid id PK
+ string name
+ string description "Optional"
+ boolean public "A channel may be only bookable with Code or internally"
+ boolean require_confirmation "Must appointments be explicitly confirmed"
+ any slot_creation "am Wochentag XY von bis, länge; davon mehrere"
+ }
+
+ SLOT {
+ uuid id PK
+ uuid channel_id FK
+ datetime start
+ int length
+ uuid appointment_id FK "Optional, set when in use"
+ }
+
+ APPOINTMENT {
+ uuid id PK
+ uuid client_id FK
+ uuid channel_id FK
+ uuid team_id FK
+ uuid slot_id FK
+ date appointment_date
+ date expiry_date
+ string title
+ string description
+ enum status "NEW, CONFIRMED, HELD, REJECTED, NO_SHOW"
+ }
+
+ NOTE {
+ uuid id PK
+ uuid appointment_id FK
+ string title
+ text content
+ timestamp created_at
+ }
+
+ ATTACHMENT {
+ uuid id PK
+ uuid appointment_id FK
+ string title
+ blob file_data
+ string mime_type
+ timestamp created_at
+ }
+
+ QUESTIONNAIRE {
+ uuid id PK
+ uuid channel_id FK
+ string title
+ string description "Optional"
+ boolean is_active
+ }
+
+ QUESTION {
+ uuid id PK
+ string text
+ enum type "FREETEXT, SINGLE_CHOICE, MULTIPLE_CHOICE"
+ json options "For choice questions"
+ boolean is_required
+ }
+
+ QUESTIONNAIRE_QUESTION {
+ uuid questionnaire_id FK
+ uuid question_id FK
+ int order_index
+ }
+
+ QUESTIONNAIRE_ANSWER {
+ uuid id PK
+ uuid appointment_id FK
+ uuid questionnaire_id FK
+ uuid question_id FK
+ text answer
+ timestamp created_at
+ }
+
+ CLIENT ||--o{ APPOINTMENT : "has"
+ CHANNEL ||--o{ APPOINTMENT : "assigned_to"
+ CHANNEL ||--o{ QUESTIONNAIRE : "has"
+ CHANNEL }o--o{ TEAM : "associated"
+ APPOINTMENT ||--o{ NOTE : "contains"
+ APPOINTMENT ||--o{ ATTACHMENT : "contains"
+ QUESTIONNAIRE ||--o{ QUESTIONNAIRE_QUESTION : "contains"
+ QUESTION ||--o{ QUESTIONNAIRE_QUESTION : "used_in"
+ APPOINTMENT ||--o{ QUESTIONNAIRE_ANSWER : "answered"
+ QUESTIONNAIRE ||--o{ QUESTIONNAIRE_ANSWER : "answered_for"
+ QUESTION ||--o{ QUESTIONNAIRE_ANSWER : "answer_to"
+```
+
+## Database Architecture Notes
+
+- **Multi-tenancy**: Each tenant operates with a separate database instance
+- **No explicit tenant references**: Since each tenant has its own database, foreign key relationships don't need to reference the tenant
+- **Encryption**: The system uses end-to-end encryption with public/private key pairs for clients and staff
+- **Hash-based identification**: Both clients and staff are identified by hash keys derived from their credentials
+- **Flexible appointments**: Appointments support multiple notes and attachments for comprehensive record keeping
+
+## Entity Descriptions
+
+### TENANT
+
+Central configuration entity (exists in a separate management database, not in tenant-specific databases)
+
+### CLIENT
+
+End-to-end encrypted client records with optional email for notifications
+
+### STAFF
+
+Practice staff members with minimal required information for privacy
+
+### CHANNEL
+
+Represents bookable resources such as rooms, machines, or personnel that appointments can be scheduled for
+
+### APPOINTMENT
+
+Core booking entity with flexible status management and expiry handling, now linked to specific channels
+
+### NOTE
+
+Text-based annotations attached to appointments
+
+### ATTACHMENT
+
+File attachments (documents, images, etc.) associated with appointments
+
+### QUESTIONNAIRE
+
+Collection of questions associated with a specific channel, can be activated/deactivated
+
+### QUESTION
+
+Individual questions that can be reused across multiple questionnaires, supporting different answer types
+
+### QUESTIONNAIRE_QUESTION
+
+Junction table linking questionnaires to questions with ordering information
+
+### QUESTIONNAIRE_ANSWER
+
+Stores answers provided by clients for specific appointments and questionnaires
diff --git a/package-lock.json b/package-lock.json
index 1dc714b..3777f4e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"dotenv-expand": "^12.0.2",
"drizzle-orm": "^0.44.2",
"mode-watcher": "^1.0.8",
+ "nodemailer": "^7.0.3",
"postgres": "^3.4.7",
"winston": "^3.17.0"
},
@@ -31,6 +32,7 @@
"@testing-library/svelte": "^5.2.8",
"@types/dotenv": "^6.1.1",
"@types/node": "^24",
+ "@types/nodemailer": "^6.4.17",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.1",
"eslint": "^9.29.0",
@@ -2427,6 +2429,16 @@
"undici-types": "~7.8.0"
}
},
+ "node_modules/@types/nodemailer": {
+ "version": "6.4.17",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
+ "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -4982,6 +4994,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nodemailer": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
+ "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
diff --git a/package.json b/package.json
index 30c334e..b142f38 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"@tailwindcss/vite": "^4.1.10",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.8",
+ "@types/nodemailer": "^6.4.17",
"@types/dotenv": "^6.1.1",
"@types/node": "^24",
"clsx": "^2.1.1",
@@ -73,6 +74,7 @@
"dotenv-expand": "^12.0.2",
"drizzle-orm": "^0.44.2",
"mode-watcher": "^1.0.8",
+ "nodemailer": "^7.0.3",
"postgres": "^3.4.7",
"winston": "^3.17.0"
},
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
index bfe8980..362cc9e 100644
--- a/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -1,6 +1,173 @@
-import { pgTable, serial, integer } from "drizzle-orm/pg-core";
+import type { InferSelectModel } from "drizzle-orm";
+import {
+ pgTable,
+ uuid,
+ text,
+ boolean,
+ integer,
+ customType,
+ date,
+ pgEnum
+} from "drizzle-orm/pg-core";
-export const user = pgTable("user", {
- id: serial("id").primaryKey(),
- age: integer("age")
+/**
+ * Custom PostgreSQL bytea type for binary data storage
+ * Used for storing encrypted data, images, and other binary content
+ */
+const bytea = customType<{ data: Buffer; driverData: Buffer }>({
+ dataType() {
+ return "bytea";
+ }
});
+
+/**
+ * Database enums
+ */
+
+/** Channel type enumeration - defines what kind of resource a channel represents */
+export const channelTypeEnum = pgEnum("channel_type", ["ROOM", "MACHINE", "PERSONNEL"]);
+
+/** Appointment status enumeration - tracks the lifecycle of appointments */
+export const appointmentStatusEnum = pgEnum("appointment_status", [
+ "NEW",
+ "CONFIRMED",
+ "HELD",
+ "REJECTED",
+ "NO_SHOW"
+]);
+
+/**
+ * Tenant table - represents a single organization/company using the system
+ * Each tenant gets their own isolated data and subdomain
+ * @table tenant
+ */
+export const tenant = pgTable("tenant", {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Short name used as subdomain (e.g., 'acme' for acme.example.com) */
+ shortName: text("short_name").notNull().unique(),
+ /** Full organization name displayed to users */
+ longName: text("long_name").notNull(),
+ /** Optional description of the organization */
+ description: text("description"),
+ /** Organization logo as binary data (PNG, JPEG, GIF, or WEBP) */
+ logo: bytea("logo"),
+ /** Background color for email templates and UI theming */
+ backgroundColor: text("background_color"),
+ /** Primary brand color for buttons, links, and highlights */
+ primaryColor: text("primary_color"),
+ /** Secondary brand color for accents and success messages */
+ secondaryColor: text("secondary_color"),
+ /** Default language for the tenant (de/en) */
+ defaultLanguage: text("default_language").default("de"),
+ /** Days after appointment completion before auto-deletion */
+ autoDeleteDays: integer("auto_delete_days").default(365),
+ /** Whether appointments require explicit confirmation */
+ requireConfirmation: boolean("require_confirmation").default(false)
+});
+
+/**
+ * Client table - represents end users who book appointments
+ * Uses end-to-end encryption for privacy protection
+ * @table client
+ */
+export const client = pgTable("client", {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Hash of client email for identification without storing plaintext */
+ hashKey: text("hash_key").notNull().unique(),
+ /** Client's public key for end-to-end encryption */
+ publicKey: text("public_key").notNull(),
+ /** Server-side share of client's private key for recovery */
+ privateKeyShare: text("private_key_share").notNull(),
+ /** Client email address (optional for privacy) */
+ email: text("email"),
+ /** Preferred language for communications (de/en) */
+ language: text("language")
+});
+
+/**
+ * Staff table - represents employees/staff members who manage appointments
+ * Staff members have administrative access and can view/manage appointments
+ * @table staff
+ */
+export const staff = pgTable("staff", {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Hash of staff login name for identification */
+ hashKey: text("hash_key").notNull().unique(),
+ /** Staff member's public key for end-to-end encryption */
+ publicKey: text("public_key").notNull(),
+ /** Staff member's display name */
+ name: text("name"),
+ /** Job title or position within the organization */
+ position: text("position"),
+ /** Staff email address (required for notifications) */
+ email: text("email").notNull(),
+ /** Preferred language for communications (de/en) */
+ language: text("language")
+});
+
+/**
+ * Channel table - represents bookable resources (rooms, machines, personnel)
+ * Channels define what can be booked and when
+ * @table channel
+ */
+export const channel = pgTable("channel", {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Type of channel (ROOM, MACHINE, or PERSONNEL) */
+ type: channelTypeEnum("type").notNull(),
+ /** Display name of the channel */
+ name: text("name").notNull(),
+ /** Optional description of the channel and its capabilities */
+ description: text("description")
+});
+
+/**
+ * Appointment table - represents scheduled appointments between clients and channels
+ * Contains encrypted appointment data for privacy protection
+ * @table appointment
+ */
+export const appointment = pgTable("appointment", {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Foreign key to client who booked the appointment */
+ clientId: uuid("client_id")
+ .notNull()
+ .references(() => client.id),
+ /** Foreign key to channel/resource being booked */
+ channelId: uuid("channel_id")
+ .notNull()
+ .references(() => channel.id),
+ /** Date and time of the appointment */
+ appointmentDate: date("appointment_date").notNull(),
+ /** When appointment data expires and can be auto-deleted */
+ expiryDate: date("expiry_date").notNull(),
+ /** Appointment title/subject */
+ title: text("title").notNull(),
+ /** Optional detailed description of the appointment */
+ description: text("description"),
+ /** Current status of the appointment */
+ status: appointmentStatusEnum("status").notNull().default("NEW")
+});
+
+/**
+ * TypeScript type exports for use in application code
+ * These types represent the shape of data when queried from the database
+ */
+
+/** Tenant record type for database queries */
+export type SelectTenant = InferSelectModel;
+
+/** Client record type for database queries */
+export type SelectClient = InferSelectModel;
+
+/** Staff record type for database queries */
+export type SelectStaff = InferSelectModel;
+
+/** Channel record type for database queries */
+export type SelectChannel = InferSelectModel;
+
+/** Appointment record type for database queries */
+export type SelectAppointment = InferSelectModel;
diff --git a/src/lib/server/email/__tests__/email-system.test.ts b/src/lib/server/email/__tests__/email-system.test.ts
new file mode 100644
index 0000000..3a2a17a
--- /dev/null
+++ b/src/lib/server/email/__tests__/email-system.test.ts
@@ -0,0 +1,396 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+
+// Mock environment variables
+vi.mock("$env/dynamic/private", () => ({
+ env: {
+ SMTP_HOST: "smtp.test.com",
+ SMTP_PORT: "587",
+ SMTP_SECURE: "false",
+ SMTP_USER: "test@example.com",
+ SMTP_PASS: "test-password",
+ SMTP_FROM_NAME: "Test App",
+ SMTP_FROM_EMAIL: "noreply@test.com"
+ }
+}));
+
+// Mock nodemailer
+const mockSendMail = vi.fn();
+const mockVerify = vi.fn();
+const mockTransporter = {
+ sendMail: mockSendMail,
+ verify: mockVerify
+};
+
+vi.mock("nodemailer", () => ({
+ default: {
+ createTransport: vi.fn(() => mockTransporter)
+ }
+}));
+
+// Mock fs/promises
+vi.mock("fs/promises", () => ({
+ readFile: vi.fn()
+}));
+
+// Import modules after mocking
+import { sendEmail, testEmailConnection } from "../mailer";
+import { EmailTemplateEngine } from "../template-engine";
+import { sendUserCreatedEmail, sendTemplatedEmail } from "../email-service";
+import { readFile } from "fs/promises";
+import nodemailer from "nodemailer";
+
+const mockReadFile = vi.mocked(readFile);
+const mockCreateTransporter = vi.mocked(nodemailer.createTransport);
+
+describe("Email System", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockSendMail.mockResolvedValue({ messageId: "test-message-id" });
+ mockVerify.mockResolvedValue(true);
+ });
+
+ describe("Mailer", () => {
+ it("should send email successfully", async () => {
+ const recipient = { email: "test@example.com", name: "Test User" };
+ const subject = "Test Subject";
+ const htmlContent = "
+
+
diff --git a/src/lib/server/email/templates/appointment-reminder.en.txt b/src/lib/server/email/templates/appointment-reminder.en.txt
new file mode 100644
index 0000000..ba61121
--- /dev/null
+++ b/src/lib/server/email/templates/appointment-reminder.en.txt
@@ -0,0 +1,25 @@
+Appointment Reminder - Open Reception
+
+Hello {{recipient.name}},
+
+This is a reminder about your upcoming appointment:
+
+APPOINTMENT DETAILS
+===================
+Date: {{appointmentDate}}
+Time: {{appointmentTime}}
+{{#if location}}
+Location: {{location}}
+{{/if}}
+{{#if description}}
+Description: {{description}}
+{{/if}}
+
+{{#if cancelUrl}}
+Cancel appointment: {{cancelUrl}}
+{{/if}}
+
+We look forward to seeing you!
+
+--
+Open Reception Team
\ No newline at end of file
diff --git a/src/lib/server/email/templates/pin-reset.de.html b/src/lib/server/email/templates/pin-reset.de.html
new file mode 100644
index 0000000..28fa9bd
--- /dev/null
+++ b/src/lib/server/email/templates/pin-reset.de.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ PIN zurückgesetzt
+
+
+
+
+
+ {{#if tenant.logo}}
+
+ {{/if}}
+
{{tenant.longName}}
+
+
+
PIN zurückgesetzt
+
+
Hallo {{recipient.name}},
+
+
Ihre PIN wurde erfolgreich zurückgesetzt.
+
+
Sie können sich nun mit Ihrer neuen PIN anmelden.
+
+
+
+
+
diff --git a/src/lib/server/email/templates/pin-reset.de.txt b/src/lib/server/email/templates/pin-reset.de.txt
new file mode 100644
index 0000000..2a6830a
--- /dev/null
+++ b/src/lib/server/email/templates/pin-reset.de.txt
@@ -0,0 +1,12 @@
+PIN zurückgesetzt - Open Reception
+
+Hallo {{recipient.name}},
+
+Ihre PIN wurde erfolgreich zurückgesetzt.
+
+Sie können sich nun mit Ihrer neuen PIN anmelden.
+
+Falls Sie diese Zurücksetzung nicht angefordert haben, wenden Sie sich bitte an den Support.
+
+--
+Open Reception Team
\ No newline at end of file
diff --git a/src/lib/server/email/templates/pin-reset.en.html b/src/lib/server/email/templates/pin-reset.en.html
new file mode 100644
index 0000000..3eabb3b
--- /dev/null
+++ b/src/lib/server/email/templates/pin-reset.en.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ PIN Reset Successful
+
+
+
+
+
+ {{#if tenant.logo}}
+
+ {{/if}}
+
{{tenant.longName}}
+
+
+
PIN Reset Successful
+
+
Hello {{recipient.name}},
+
+
Your PIN has been successfully reset.
+
+
You can now sign in with your new PIN.
+
+
+
+
+
diff --git a/src/lib/server/email/templates/pin-reset.en.txt b/src/lib/server/email/templates/pin-reset.en.txt
new file mode 100644
index 0000000..350b658
--- /dev/null
+++ b/src/lib/server/email/templates/pin-reset.en.txt
@@ -0,0 +1,12 @@
+PIN Reset Successful - Open Reception
+
+Hello {{recipient.name}},
+
+Your PIN has been successfully reset.
+
+You can now sign in with your new PIN.
+
+If you did not request this reset, please contact support.
+
+--
+Open Reception Team
\ No newline at end of file
diff --git a/src/lib/server/email/templates/user-created.de.html b/src/lib/server/email/templates/user-created.de.html
new file mode 100644
index 0000000..1fe8bd5
--- /dev/null
+++ b/src/lib/server/email/templates/user-created.de.html
@@ -0,0 +1,99 @@
+
+
+
+
+
+ Willkommen bei Open Reception
+
+
+
+
+
+ {{#if tenant.logo}}
+
+ {{/if}}
+
{{tenant.longName}}
+
+
+
Willkommen!
+
+
Hallo {{recipient.name}},
+
+
Ihr Konto wurde erfolgreich erstellt. Sie können sich jetzt anmelden:
+
+
diff --git a/src/lib/server/email/templates/user-created.de.txt b/src/lib/server/email/templates/user-created.de.txt
new file mode 100644
index 0000000..f9ce507
--- /dev/null
+++ b/src/lib/server/email/templates/user-created.de.txt
@@ -0,0 +1,11 @@
+Willkommen bei Open Reception!
+
+Hallo {{recipient.name}},
+
+Ihr Konto wurde erfolgreich erstellt. Sie können sich jetzt anmelden:
+{{loginUrl}}
+
+Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren.
+
+--
+Open Reception Team
\ No newline at end of file
diff --git a/src/lib/server/email/templates/user-created.en.html b/src/lib/server/email/templates/user-created.en.html
new file mode 100644
index 0000000..42ebc06
--- /dev/null
+++ b/src/lib/server/email/templates/user-created.en.html
@@ -0,0 +1,99 @@
+
+
+
+
+
+ Welcome to Open Reception
+
+
+
+
+
+ {{#if tenant.logo}}
+
+ {{/if}}
+
{{tenant.longName}}
+
+
+
Welcome!
+
+
Hello {{recipient.name}},
+
+
Your account has been successfully created. You can now sign in:
+
+
diff --git a/src/lib/server/email/templates/user-created.en.txt b/src/lib/server/email/templates/user-created.en.txt
new file mode 100644
index 0000000..e02650b
--- /dev/null
+++ b/src/lib/server/email/templates/user-created.en.txt
@@ -0,0 +1,11 @@
+Welcome to Open Reception!
+
+Hello {{recipient.name}},
+
+Your account has been successfully created. You can now sign in:
+{{loginUrl}}
+
+If you did not request this email, you can safely ignore it.
+
+--
+Open Reception Team
\ No newline at end of file