email-content-compose/src/eml.ts

98 lines
2.7 KiB
TypeScript
Raw Normal View History

2026-03-11 23:36:44 +00:00
import type { EmailDraft } from "./types.js";
/**
* Convert a draft to RFC 5322 EML format with MIME multipart/alternative.
*/
export function draftToEml(draft: EmailDraft, from: string): string {
const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const date = new Date().toUTCString();
const messageId = `<${Date.now()}.${Math.random().toString(36).slice(2)}@outreach.local>`;
const toHeader = draft.recipient_name
? `"${draft.recipient_name}" <${draft.recipient_email}>`
: draft.recipient_email;
const headers = [
`From: ${from}`,
`To: ${toHeader}`,
`Subject: ${encodeSubject(draft.subject)}`,
`Date: ${date}`,
`Message-ID: ${messageId}`,
`MIME-Version: 1.0`,
`Content-Type: multipart/alternative; boundary="${boundary}"`,
`X-Mailer: email-export-skill`,
];
const textPart = [
`--${boundary}`,
`Content-Type: text/plain; charset="utf-8"`,
`Content-Transfer-Encoding: quoted-printable`,
``,
quotedPrintableEncode(draft.body_text),
].join("\r\n");
const htmlPart = [
`--${boundary}`,
`Content-Type: text/html; charset="utf-8"`,
`Content-Transfer-Encoding: quoted-printable`,
``,
quotedPrintableEncode(draft.body_html),
].join("\r\n");
return [
headers.join("\r\n"),
``,
textPart,
``,
htmlPart,
``,
`--${boundary}--`,
``,
].join("\r\n");
}
/** RFC 2047 encoded-word for non-ASCII subjects */
function encodeSubject(subject: string): string {
if (/^[\x20-\x7E]*$/.test(subject)) return subject;
const encoded = Buffer.from(subject, "utf-8").toString("base64");
return `=?UTF-8?B?${encoded}?=`;
}
/** Simple quoted-printable encoding */
function quotedPrintableEncode(text: string): string {
const lines: string[] = [];
let current = "";
for (const char of text) {
const code = char.charCodeAt(0);
let encoded: string;
if (char === "\r" || char === "\n") {
lines.push(current);
current = "";
continue;
} else if (code === 9 || (code >= 32 && code <= 126 && char !== "=")) {
encoded = char;
} else {
const buf = Buffer.from(char, "utf-8");
encoded = Array.from(buf).map(b => `=${b.toString(16).toUpperCase().padStart(2, "0")}`).join("");
}
if (current.length + encoded.length > 75) {
lines.push(current + "=");
current = encoded;
} else {
current += encoded;
}
}
if (current) lines.push(current);
return lines.join("\r\n");
}
/** Generate a safe filename from draft */
export function emlFilename(draft: EmailDraft, index: number): string {
const domain = draft.recipient_email.split("@")[1] || "unknown";
const safe = domain.replace(/[^a-zA-Z0-9.-]/g, "_");
return `${String(index + 1).padStart(3, "0")}_${safe}.eml`;
}