| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- const dropZone = document.getElementById("dropZone");
- const fieldList = document.getElementById("fieldList");
- const textBody = document.getElementById("textBody");
- const htmlBody = document.getElementById("htmlBody");
- const rawPreview = document.getElementById("rawPreview");
- const FIELDS = [
- "From",
- "To",
- "Cc",
- "Bcc",
- "Reply-To",
- "Subject",
- "Date",
- "Message-ID",
- "Content-Type",
- ];
- ["dragenter", "dragover"].forEach((eventName) => {
- dropZone.addEventListener(eventName, (event) => {
- event.preventDefault();
- event.stopPropagation();
- dropZone.classList.add("active");
- });
- });
- ["dragleave", "drop"].forEach((eventName) => {
- dropZone.addEventListener(eventName, (event) => {
- event.preventDefault();
- event.stopPropagation();
- dropZone.classList.remove("active");
- });
- });
- dropZone.addEventListener("drop", async (event) => {
- const file = event.dataTransfer?.files?.[0];
- if (!file) {
- renderError("No file was dropped.");
- return;
- }
- if (!file.name.toLowerCase().endsWith(".eml")) {
- renderError("Please drop a .eml file from Thunderbird.");
- return;
- }
- try {
- const messageText = await file.text();
- const parsed = parseEml(messageText);
- renderParsedEmail(parsed, messageText);
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- renderError(`Failed to parse email: ${message}`);
- }
- });
- function parseEml(messageText) {
- const normalized = messageText.replace(/\r\n/g, "\n");
- const splitIndex = normalized.indexOf("\n\n");
- const headerBlock = splitIndex >= 0 ? normalized.slice(0, splitIndex) : normalized;
- const bodyBlock = splitIndex >= 0 ? normalized.slice(splitIndex + 2) : "";
- const headers = parseHeaders(headerBlock);
- const contentType = headers.get("content-type") || "";
- const bodyParts = extractBodiesFromMime(bodyBlock, contentType);
- return { headers, bodyParts };
- }
- function parseHeaders(headerBlock) {
- const map = new Map();
- const lines = headerBlock.split("\n");
- let currentName = "";
- let currentValue = "";
- const commitHeader = () => {
- if (!currentName) return;
- map.set(currentName.toLowerCase(), currentValue.trim());
- };
- for (const line of lines) {
- if (/^\s/.test(line) && currentName) {
- currentValue += ` ${line.trim()}`;
- continue;
- }
- commitHeader();
- const separatorIndex = line.indexOf(":");
- if (separatorIndex < 0) {
- currentName = "";
- currentValue = "";
- continue;
- }
- currentName = line.slice(0, separatorIndex).trim();
- currentValue = line.slice(separatorIndex + 1).trim();
- }
- commitHeader();
- return map;
- }
- function extractBodiesFromMime(bodyBlock, contentTypeHeader) {
- const result = { text: "", html: "" };
- if (!contentTypeHeader.includes("multipart/")) {
- result.text = decodeBodyByEncoding(bodyBlock, "");
- return result;
- }
- const boundaryMatch = contentTypeHeader.match(/boundary="?([^";]+)"?/i);
- if (!boundaryMatch) {
- result.text = decodeBodyByEncoding(bodyBlock, "");
- return result;
- }
- const boundary = boundaryMatch[1];
- const boundaryToken = `--${boundary}`;
- const segments = bodyBlock.split(boundaryToken);
- for (const segment of segments) {
- const trimmed = segment.trim();
- if (!trimmed || trimmed === "--") continue;
- const normalized = trimmed.replace(/^\n+/, "");
- const splitIndex = normalized.indexOf("\n\n");
- if (splitIndex < 0) continue;
- const rawPartHeaders = normalized.slice(0, splitIndex);
- const rawPartBody = normalized.slice(splitIndex + 2);
- const partHeaders = parseHeaders(rawPartHeaders);
- const partType = (partHeaders.get("content-type") || "").toLowerCase();
- const transferEncoding = (partHeaders.get("content-transfer-encoding") || "").toLowerCase();
- const decodedPartBody = decodeBodyByEncoding(rawPartBody, transferEncoding);
- if (!result.text && partType.includes("text/plain")) {
- result.text = decodedPartBody;
- } else if (!result.html && partType.includes("text/html")) {
- result.html = decodedPartBody;
- }
- }
- if (!result.text) {
- result.text = decodeBodyByEncoding(bodyBlock, "");
- }
- return result;
- }
- function decodeBodyByEncoding(body, transferEncoding) {
- const normalized = body.replace(/\r\n/g, "\n").trim();
- if (transferEncoding.includes("base64")) {
- try {
- return atob(normalized.replace(/\s+/g, ""));
- } catch {
- return normalized;
- }
- }
- if (transferEncoding.includes("quoted-printable")) {
- return decodeQuotedPrintable(normalized);
- }
- return normalized;
- }
- function decodeQuotedPrintable(input) {
- const softBreaksRemoved = input.replace(/=\n/g, "");
- return softBreaksRemoved.replace(/=([A-Fa-f0-9]{2})/g, (_, hex) =>
- String.fromCharCode(parseInt(hex, 16)),
- );
- }
- function renderParsedEmail(parsed, messageText) {
- fieldList.innerHTML = "";
- for (const field of FIELDS) {
- const value = parsed.headers.get(field.toLowerCase()) || "(not present)";
- appendField(field, value);
- }
- setBoxContent(textBody, parsed.bodyParts.text || "No text body found.");
- setBoxContent(htmlBody, parsed.bodyParts.html || "No HTML body found.");
- setBoxContent(rawPreview, messageText.slice(0, 12000));
- }
- function appendField(label, value) {
- const dt = document.createElement("dt");
- dt.textContent = label;
- const dd = document.createElement("dd");
- dd.textContent = value;
- fieldList.append(dt, dd);
- }
- function setBoxContent(element, text) {
- element.textContent = text;
- element.classList.toggle("empty", !text || text.startsWith("No "));
- }
- function renderError(message) {
- fieldList.innerHTML = "";
- appendField("Error", message);
- setBoxContent(textBody, "No text body parsed yet.");
- setBoxContent(htmlBody, "No HTML body parsed yet.");
- setBoxContent(rawPreview, "Drop an email to preview raw content.");
- }
|