Files
webmailserver/components/mail/MessageView.tsx

153 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import type { MailMessage } from "@/app/[lang]/dashboard/mail/page";
import { formatBytes } from "@/lib/format";
import { useDictionary } from "@/components/DictionaryContext";
import { useParams } from "next/navigation";
function getFileIcon(contentType: string, filename: string): string {
if (contentType.startsWith("image/")) return "🖼️";
if (contentType === "application/pdf") return "📄";
if (contentType.includes("zip") || contentType.includes("rar") || contentType.includes("tar")) return "📦";
if (contentType.includes("word") || filename.endsWith(".doc") || filename.endsWith(".docx")) return "📝";
if (contentType.includes("sheet") || contentType.includes("excel") || filename.endsWith(".xls")) return "📊";
if (contentType.includes("presentation") || filename.endsWith(".ppt")) return "📑";
if (contentType.startsWith("video/")) return "🎬";
if (contentType.startsWith("audio/")) return "🎵";
return "📎";
}
function canPreview(contentType: string): boolean {
return contentType.startsWith("image/") || contentType === "application/pdf";
}
export default function MessageView({ message, onReply, onDelete, folder }: {
message: MailMessage;
onReply: () => void;
onDelete: () => void;
folder: string;
}) {
const dict = useDictionary();
const params = useParams();
const lang = (params.lang as string) || "en";
const from = message.from[0];
const date = new Date(message.date).toLocaleString(lang === "tr" ? "tr-TR" : "en-US", {
day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit",
});
const downloadUrl = (filename: string) =>
`/api/mail/messages/${message.uid}/attachments?folder=${encodeURIComponent(folder)}&filename=${encodeURIComponent(filename)}`;
const handleAttachment = (att: { filename: string; contentType: string }, preview: boolean) => {
const url = downloadUrl(att.filename);
if (preview && canPreview(att.contentType)) {
window.open(url, "_blank");
} else {
const a = document.createElement("a");
a.href = url;
a.download = att.filename;
a.click();
}
};
return (
<div className="message-view">
{/* Header */}
<div className="message-view-header">
<h2 className="message-view-subject">{message.subject || dict.mailClient.noSubject || "(Konu Yok)"}</h2>
<div className="message-view-actions">
<button className="btn btn-ghost btn-sm" onClick={onReply} title={dict.mailClient.reply || "Yanıtla"}>
<ReplyIcon /> {dict.mailClient.reply || "Yanıtla"}
</button>
<button className="btn btn-danger btn-sm" onClick={onDelete} title={dict.mailClient.delete || "Sil"}>
<TrashIcon /> {dict.mailClient.delete || "Sil"}
</button>
</div>
</div>
{/* Meta */}
<div className="message-view-meta">
<div className="message-view-avatar">{(from?.name || from?.address)?.[0]?.toUpperCase() ?? "?"}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 13 }}>{from?.name || from?.address}</div>
<div style={{ fontSize: 11, color: "var(--text-muted)" }}>
{from?.address}
{message.to.length > 0 && <> {message.to.map((t) => t.address).join(", ")}</>}
</div>
</div>
<div style={{ fontSize: 11, color: "var(--text-muted)", flexShrink: 0 }}>{date}</div>
</div>
{/* Attachments */}
{message.attachments.length > 0 && (
<div className="message-attachments">
<div style={{ width: "100%", fontSize: 12, fontWeight: 600, color: "var(--text-secondary)", marginBottom: 6 }}>
📎 {message.attachments.length} {dict.mailClient.attachments || "ek"}
</div>
{message.attachments.map((att, i) => (
<div key={i} className="attachment-chip" onClick={() => handleAttachment(att, false)}>
<span>{getFileIcon(att.contentType, att.filename)}</span>
<span style={{ maxWidth: 160, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{att.filename}
</span>
<span style={{ color: "var(--text-muted)", fontSize: 11 }}>{formatBytes(att.size)}</span>
<span className="attachment-actions">
<button
className="att-btn"
onClick={(e) => { e.stopPropagation(); handleAttachment(att, false); }}
title={dict.mailClient.download || "İndir"}
>
</button>
{canPreview(att.contentType) && (
<button
className="att-btn"
onClick={(e) => { e.stopPropagation(); handleAttachment(att, true); }}
title="Önizle"
>
👁
</button>
)}
</span>
</div>
))}
</div>
)}
{/* Body */}
<div className="message-view-body">
{message.html ? (
<iframe
srcDoc={sanitizeHtml(message.html)}
sandbox="allow-same-origin"
style={{ width: "100%", border: "none", minHeight: 400 }}
onLoad={(e) => {
const iframe = e.target as HTMLIFrameElement;
if (iframe.contentDocument) {
iframe.style.height = iframe.contentDocument.body.scrollHeight + 20 + "px";
}
}}
/>
) : (
<pre style={{ whiteSpace: "pre-wrap", fontFamily: "inherit", fontSize: 13, color: "var(--text-secondary)" }}>
{message.text}
</pre>
)}
</div>
</div>
);
}
function sanitizeHtml(html: string): string {
const style = `<style>
body { font-family: Inter, -apple-system, sans-serif; font-size: 14px; color: #c9d1d9; background: transparent; margin: 0; padding: 8px; line-height: 1.6; }
a { color: #58a6ff; }
img { max-width: 100%; height: auto; }
</style>`;
let clean = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
clean = clean.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, "");
return `<!DOCTYPE html><html><head>${style}</head><body>${clean}</body></html>`;
}
function ReplyIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>; }
function TrashIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>; }