feat: phase 6: email sending implemented.

This commit is contained in:
Yusuf İpek
2025-12-27 14:42:56 +03:00
parent 226935e67d
commit 1c91a1fe9d
10 changed files with 389 additions and 89 deletions
+6 -5
View File
@@ -37,11 +37,11 @@
- [ ] Deadline kontrolü - [ ] Deadline kontrolü
- [ ] Status güncellemeleri - [ ] Status güncellemeleri
### Faz 6: Email Gönderimi 🔲 ### Faz 6: Email Gönderimi
- [ ] Nodemailer setup - [x] Nodemailer setup
- [ ] Email templates - [x] Email templates
- [ ] Deadline triggered email - [x] Deadline triggered email
- [ ] Check-in reminder emails - [x] Check-in reminder emails
### Faz 7: Admin Panel 🔲 ### Faz 7: Admin Panel 🔲
- [ ] Admin role ve guard - [ ] Admin role ve guard
@@ -81,6 +81,7 @@
- ✅ Faz 2: Mesaj CRUD - ✅ Faz 2: Mesaj CRUD
- ✅ Faz 3: Dosya Yükleme (R2) - ✅ Faz 3: Dosya Yükleme (R2)
- ✅ Faz 4: Client-side Encryption - ✅ Faz 4: Client-side Encryption
- ✅ Faz 6: Email Gönderimi
## Bilinen Sorunlar ## Bilinen Sorunlar
(henüz yok) (henüz yok)
+74
View File
@@ -20,6 +20,7 @@
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^16.1.1", "next": "^16.1.1",
"next-auth": "^4.24.13", "next-auth": "^4.24.13",
"nodemailer": "^7.0.12",
"pg": "^8.16.3", "pg": "^8.16.3",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
@@ -30,6 +31,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^20.19.27", "@types/node": "^20.19.27",
"@types/nodemailer": "^7.0.4",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -391,6 +393,58 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@aws-sdk/client-sesv2": {
"version": "3.958.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.958.0.tgz",
"integrity": "sha512-3x3n8IIxIMAkdpt9wy9zS7MO2lqTcJwQTdHMn6BlD7YUohb+r5Q4KCOEQ2uHWd4WIJv2tlbXnfypHaXReO/WXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.957.0",
"@aws-sdk/credential-provider-node": "3.958.0",
"@aws-sdk/middleware-host-header": "3.957.0",
"@aws-sdk/middleware-logger": "3.957.0",
"@aws-sdk/middleware-recursion-detection": "3.957.0",
"@aws-sdk/middleware-user-agent": "3.957.0",
"@aws-sdk/region-config-resolver": "3.957.0",
"@aws-sdk/signature-v4-multi-region": "3.957.0",
"@aws-sdk/types": "3.957.0",
"@aws-sdk/util-endpoints": "3.957.0",
"@aws-sdk/util-user-agent-browser": "3.957.0",
"@aws-sdk/util-user-agent-node": "3.957.0",
"@smithy/config-resolver": "^4.4.5",
"@smithy/core": "^3.20.0",
"@smithy/fetch-http-handler": "^5.3.8",
"@smithy/hash-node": "^4.2.7",
"@smithy/invalid-dependency": "^4.2.7",
"@smithy/middleware-content-length": "^4.2.7",
"@smithy/middleware-endpoint": "^4.4.1",
"@smithy/middleware-retry": "^4.4.17",
"@smithy/middleware-serde": "^4.2.8",
"@smithy/middleware-stack": "^4.2.7",
"@smithy/node-config-provider": "^4.3.7",
"@smithy/node-http-handler": "^4.4.7",
"@smithy/protocol-http": "^5.3.7",
"@smithy/smithy-client": "^4.10.2",
"@smithy/types": "^4.11.0",
"@smithy/url-parser": "^4.2.7",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
"@smithy/util-defaults-mode-browser": "^4.3.16",
"@smithy/util-defaults-mode-node": "^4.2.19",
"@smithy/util-endpoints": "^3.2.7",
"@smithy/util-middleware": "^4.2.7",
"@smithy/util-retry": "^4.2.7",
"@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@aws-sdk/client-sso": { "node_modules/@aws-sdk/client-sso": {
"version": "3.958.0", "version": "3.958.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.958.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.958.0.tgz",
@@ -3740,6 +3794,17 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/nodemailer": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz",
"integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@aws-sdk/client-sesv2": "^3.839.0",
"@types/node": "*"
}
},
"node_modules/@types/pg": { "node_modules/@types/pg": {
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz",
@@ -7757,6 +7822,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemailer": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz",
"integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nypm": { "node_modules/nypm": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
+2
View File
@@ -21,6 +21,7 @@
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^16.1.1", "next": "^16.1.1",
"next-auth": "^4.24.13", "next-auth": "^4.24.13",
"nodemailer": "^7.0.12",
"pg": "^8.16.3", "pg": "^8.16.3",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
@@ -31,6 +32,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^20.19.27", "@types/node": "^20.19.27",
"@types/nodemailer": "^7.0.4",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
+59
View File
@@ -0,0 +1,59 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { sendEmail, emailTemplates } from '@/lib/email';
export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const now = new Date();
const expiredMessages = await prisma.message.findMany({
where: {
status: 'ACTIVE',
lastPing: {
lt: new Date(now.getTime() - 1000 * 60 * 60 * 24), // 24 saat geçmiş
},
},
include: {
user: true,
recipients: true,
},
});
const results = [];
for (const message of expiredMessages) {
const deadlineTime = new Date(message.lastPing.getTime() + message.checkInterval * 60 * 60 * 1000);
if (now > deadlineTime) {
for (const recipient of message.recipients) {
const template = emailTemplates.deadlineTriggered(
recipient.email,
message.user.name || 'Bilinmeyen',
message.id
);
try {
await sendEmail({
to: recipient.email,
subject: template.subject,
html: template.html,
});
results.push({ messageId: message.id, recipient: recipient.email, status: 'sent' });
} catch (error) {
results.push({ messageId: message.id, recipient: recipient.email, status: 'failed', error });
}
}
await prisma.message.update({
where: { id: message.id },
data: { status: 'TRIGGERED' },
});
}
}
return NextResponse.json({ checked: expiredMessages.length, results });
}
+50
View File
@@ -0,0 +1,50 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { sendEmail, emailTemplates } from '@/lib/email';
export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const now = new Date();
const reminderHours = [24, 12, 6, 1];
const results = [];
const activeMessages = await prisma.message.findMany({
where: { status: 'ACTIVE' },
include: { user: true },
});
for (const message of activeMessages) {
const deadlineTime = new Date(message.lastPing.getTime() + message.checkInterval * 60 * 60 * 1000);
const hoursLeft = (deadlineTime.getTime() - now.getTime()) / (1000 * 60 * 60);
for (const reminderHour of reminderHours) {
if (hoursLeft <= reminderHour && hoursLeft > reminderHour - 1) {
if (message.user.email) {
const template = emailTemplates.checkInReminder(
message.user.name || 'Kullanıcı',
Math.round(hoursLeft),
message.title
);
try {
await sendEmail({
to: message.user.email,
subject: template.subject,
html: template.html,
});
results.push({ messageId: message.id, hoursLeft: Math.round(hoursLeft), status: 'sent' });
} catch (error) {
results.push({ messageId: message.id, status: 'failed', error });
}
}
break;
}
}
}
return NextResponse.json({ checked: activeMessages.length, reminders: results });
}
+4 -6
View File
@@ -7,7 +7,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Lock, Unlock } from "lucide-react"; import { Lock, Unlock } from "lucide-react";
import { deriveKeyFromPassword, decrypt, decryptFile } from "@/lib/crypto"; import { decrypt, decryptFile } from "@/lib/crypto";
export default function DecryptPage() { export default function DecryptPage() {
const [encryptedContent, setEncryptedContent] = useState(""); const [encryptedContent, setEncryptedContent] = useState("");
@@ -20,8 +20,7 @@ export default function DecryptPage() {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const key = await deriveKeyFromPassword(password); const decrypted = await decrypt(encryptedContent, password);
const decrypted = await decrypt(encryptedContent, key);
setDecryptedContent(decrypted); setDecryptedContent(decrypted);
} catch { } catch {
setError("Şifre çözme başarısız. Şifre veya içerik hatalı olabilir."); setError("Şifre çözme başarısız. Şifre veya içerik hatalı olabilir.");
@@ -37,14 +36,13 @@ export default function DecryptPage() {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const key = await deriveKeyFromPassword(password); const decryptedData = await decryptFile(file, password);
const decryptedData = await decryptFile(file, key);
let originalName = file.name; let originalName = file.name;
if (file.name.endsWith(".enc")) { if (file.name.endsWith(".enc")) {
const encryptedName = file.name.slice(0, -4); const encryptedName = file.name.slice(0, -4);
try { try {
originalName = await decrypt(encryptedName, key); originalName = await decrypt(encryptedName, password);
} catch { } catch {
originalName = "decrypted_file"; originalName = "decrypted_file";
} }
+5 -7
View File
@@ -3,7 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { deriveKeyFromPassword, encrypt, decrypt, encryptFile, decryptFile } from "@/lib/crypto"; import { encrypt, decrypt, encryptFile, decryptFile } from "@/lib/crypto";
interface FileUploadProps { interface FileUploadProps {
messageId: string; messageId: string;
@@ -31,9 +31,8 @@ export function FileUpload({ messageId, isEncrypted, password, onUploadComplete
let contentType = file.type; let contentType = file.type;
if (isEncrypted && password) { if (isEncrypted && password) {
const key = await deriveKeyFromPassword(password); fileToUpload = await encryptFile(file, password);
fileToUpload = await encryptFile(file, key); fileName = (await encrypt(file.name, password)) + ".enc";
fileName = (await encrypt(file.name, key)) + ".enc";
contentType = "application/octet-stream"; contentType = "application/octet-stream";
} }
@@ -103,13 +102,12 @@ export function AttachmentList({ attachments, onDelete, isEncrypted, password }:
if (isEncrypted && password && fileName.endsWith(".enc")) { if (isEncrypted && password && fileName.endsWith(".enc")) {
const fileRes = await fetch(downloadUrl); const fileRes = await fetch(downloadUrl);
const encryptedBlob = await fileRes.blob(); const encryptedBlob = await fileRes.blob();
const key = await deriveKeyFromPassword(password); const decryptedData = await decryptFile(encryptedBlob, password);
const decryptedData = await decryptFile(encryptedBlob, key);
let originalName = "decrypted_file"; let originalName = "decrypted_file";
try { try {
const encryptedName = fileName.slice(0, -4); const encryptedName = fileName.slice(0, -4);
originalName = await decrypt(encryptedName, key); originalName = await decrypt(encryptedName, password);
} catch {} } catch {}
const blob = new Blob([decryptedData]); const blob = new Blob([decryptedData]);
+6 -9
View File
@@ -9,7 +9,7 @@ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/componen
import { Plus, Trash2, Lock, Unlock } from "lucide-react"; import { Plus, Trash2, Lock, Unlock } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { FileUpload, AttachmentList } from "@/components/attachments/file-upload"; import { FileUpload, AttachmentList } from "@/components/attachments/file-upload";
import { deriveKeyFromPassword, encrypt, decrypt, encryptFile } from "@/lib/crypto"; import { encrypt, decrypt, encryptFile } from "@/lib/crypto";
interface Attachment { interface Attachment {
id: string; id: string;
@@ -75,8 +75,7 @@ export default function MessageForm({ initialData }: MessageFormProps) {
setDecryptError(null); setDecryptError(null);
setLoading(true); setLoading(true);
try { try {
const key = await deriveKeyFromPassword(password); const decryptedContent = await decrypt(content, password);
const decryptedContent = await decrypt(content, key);
setContent(decryptedContent); setContent(decryptedContent);
setIsDecrypted(true); setIsDecrypted(true);
} catch { } catch {
@@ -97,12 +96,10 @@ export default function MessageForm({ initialData }: MessageFormProps) {
setLoading(true); setLoading(true);
try { try {
let encryptionKey: string | null = null;
let finalContent = content; let finalContent = content;
if (isEncrypted) { if (isEncrypted) {
encryptionKey = await deriveKeyFromPassword(password); finalContent = await encrypt(content, password);
finalContent = await encrypt(content, encryptionKey);
} }
const url = initialData?.id const url = initialData?.id
@@ -130,9 +127,9 @@ export default function MessageForm({ initialData }: MessageFormProps) {
let fileToUpload: Blob = file; let fileToUpload: Blob = file;
let fileName = file.name; let fileName = file.name;
if (isEncrypted && encryptionKey) { if (isEncrypted) {
fileToUpload = await encryptFile(file, encryptionKey); fileToUpload = await encryptFile(file, password);
const encryptedFileName = await encrypt(file.name, encryptionKey); const encryptedFileName = await encrypt(file.name, password);
fileName = encryptedFileName + ".enc"; fileName = encryptedFileName + ".enc";
} }
+30 -59
View File
@@ -1,4 +1,4 @@
export async function deriveKeyFromPassword(password: string): Promise<string> { async function deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey( const keyMaterial = await crypto.subtle.importKey(
"raw", "raw",
@@ -7,90 +7,61 @@ export async function deriveKeyFromPassword(password: string): Promise<string> {
false, false,
["deriveBits", "deriveKey"] ["deriveBits", "deriveKey"]
); );
const key = await crypto.subtle.deriveKey( return crypto.subtle.deriveKey(
{ {
name: "PBKDF2", name: "PBKDF2",
salt: encoder.encode("dead-man-switch-salt"), salt: salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength) as ArrayBuffer,
iterations: 100000, iterations: 100000,
hash: "SHA-256", hash: "SHA-256",
}, },
keyMaterial, keyMaterial,
{ name: "AES-GCM", length: 256 }, { name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
const exported = await crypto.subtle.exportKey("raw", key);
return btoa(String.fromCharCode(...new Uint8Array(exported)));
}
export async function generateKey(): Promise<string> {
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
const exported = await crypto.subtle.exportKey("raw", key);
return btoa(String.fromCharCode(...new Uint8Array(exported)));
}
export async function importKey(keyString: string): Promise<CryptoKey> {
const keyData = Uint8Array.from(atob(keyString), (c) => c.charCodeAt(0));
return crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM", length: 256 },
false, false,
["encrypt", "decrypt"] ["encrypt", "decrypt"]
); );
} }
export async function encrypt(data: string, keyString: string): Promise<string> { export async function encrypt(data: string, password: string): Promise<string> {
const key = await importKey(keyString); const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12)); const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const encoded = new TextEncoder().encode(data); const encoded = new TextEncoder().encode(data);
const encrypted = await crypto.subtle.encrypt( const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
{ name: "AES-GCM", iv }, const combined = new Uint8Array(16 + 12 + encrypted.byteLength);
key, combined.set(salt);
encoded combined.set(iv, 16);
); combined.set(new Uint8Array(encrypted), 28);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return btoa(String.fromCharCode(...combined)); return btoa(String.fromCharCode(...combined));
} }
export async function decrypt(encryptedData: string, keyString: string): Promise<string> { export async function decrypt(encryptedData: string, password: string): Promise<string> {
const key = await importKey(keyString);
const combined = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0)); const combined = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0));
const iv = combined.slice(0, 12); const salt = combined.slice(0, 16);
const data = combined.slice(12); const iv = combined.slice(16, 28);
const decrypted = await crypto.subtle.decrypt( const data = combined.slice(28);
{ name: "AES-GCM", iv }, const key = await deriveKey(password, salt);
key, const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
data
);
return new TextDecoder().decode(decrypted); return new TextDecoder().decode(decrypted);
} }
export async function encryptFile(file: File, keyString: string): Promise<Blob> { export async function encryptFile(file: File, password: string): Promise<Blob> {
const key = await importKey(keyString); const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12)); const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const encrypted = await crypto.subtle.encrypt( const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, arrayBuffer);
{ name: "AES-GCM", iv }, const combined = new Uint8Array(16 + 12 + encrypted.byteLength);
key, combined.set(salt);
arrayBuffer combined.set(iv, 16);
); combined.set(new Uint8Array(encrypted), 28);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return new Blob([combined], { type: "application/octet-stream" }); return new Blob([combined], { type: "application/octet-stream" });
} }
export async function decryptFile(encryptedBlob: Blob, keyString: string): Promise<ArrayBuffer> { export async function decryptFile(encryptedBlob: Blob, password: string): Promise<ArrayBuffer> {
const key = await importKey(keyString);
const combined = new Uint8Array(await encryptedBlob.arrayBuffer()); const combined = new Uint8Array(await encryptedBlob.arrayBuffer());
const iv = combined.slice(0, 12); const salt = combined.slice(0, 16);
const data = combined.slice(12); const iv = combined.slice(16, 28);
const data = combined.slice(28);
const key = await deriveKey(password, salt);
return crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data); return crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
} }
+150
View File
@@ -0,0 +1,150 @@
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
interface EmailOptions {
to: string;
subject: string;
html: string;
}
export async function sendEmail({ to, subject, html }: EmailOptions) {
return transporter.sendMail({
from: process.env.SMTP_FROM || 'Dead Man Switch <[email protected]>',
to,
subject,
html,
});
}
export const emailTemplates = {
deadlineTriggered: (recipientEmail: string, senderName: string, messageId: string) => ({
subject: `Önemli Mesaj: ${escapeHtml(senderName)} tarafından gönderildi`,
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #dc2626; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; }
.button { display: inline-block; background: #3b82f6; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; margin-top: 15px; }
.footer { text-align: center; padding: 15px; color: #6b7280; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Dead Man's Switch Tetiklendi</h1>
</div>
<div class="content">
<p>Merhaba,</p>
<p><strong>${escapeHtml(senderName)}</strong> tarafından size önceden hazırlanmış bir mesaj bırakıldı.</p>
<p>Bu mesaj, belirlenen süre içinde check-in yapılmadığı için otomatik olarak gönderilmiştir.</p>
<p>Mesajı görüntülemek için aşağıdaki bağlantıya tıklayın:</p>
<a href="${process.env.NEXT_PUBLIC_APP_URL}/decrypt/${messageId}" class="button">Mesajı Görüntüle</a>
<p style="margin-top: 20px; font-size: 14px; color: #6b7280;">Mesaj şifreli ise, gönderen tarafından size iletilen şifreyi girmeniz gerekecektir.</p>
</div>
<div class="footer">
<p>Dead Man's Switch Sistemi</p>
</div>
</div>
</body>
</html>
`,
}),
checkInReminder: (userName: string, hoursLeft: number, switchTitle: string) => ({
subject: `Hatırlatma: ${escapeHtml(switchTitle)} için check-in gerekli`,
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #f59e0b; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; }
.warning { background: #fef3c7; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #f59e0b; }
.button { display: inline-block; background: #3b82f6; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; margin-top: 15px; }
.footer { text-align: center; padding: 15px; color: #6b7280; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Check-in Hatırlatması</h1>
</div>
<div class="content">
<p>Merhaba ${escapeHtml(userName)},</p>
<div class="warning">
<strong>"${escapeHtml(switchTitle)}"</strong> için son <strong>${hoursLeft} saat</strong> kaldı!
</div>
<p>Mesajlarınızın gönderilmemesi için lütfen check-in yapın.</p>
<a href="${process.env.NEXT_PUBLIC_APP_URL}/dashboard" class="button">Şimdi Check-in Yap</a>
</div>
<div class="footer">
<p>Dead Man's Switch Sistemi</p>
</div>
</div>
</body>
</html>
`,
}),
welcomeEmail: (userName: string) => ({
subject: 'Dead Man\'s Switch\'e Hoş Geldiniz',
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #3b82f6; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; }
.button { display: inline-block; background: #3b82f6; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; margin-top: 15px; }
.footer { text-align: center; padding: 15px; color: #6b7280; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Hoş Geldiniz!</h1>
</div>
<div class="content">
<p>Merhaba ${escapeHtml(userName)},</p>
<p>Dead Man's Switch sistemine kayıt olduğunuz için teşekkür ederiz.</p>
<p>Artık önemli mesajlarınızı güvenle saklayabilir ve belirlediğiniz kişilere iletilmesini sağlayabilirsiniz.</p>
<a href="${process.env.NEXT_PUBLIC_APP_URL}/dashboard" class="button">Dashboard'a Git</a>
</div>
<div class="footer">
<p>Dead Man's Switch Sistemi</p>
</div>
</div>
</body>
</html>
`,
}),
};