98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
|
|
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`;
|
||
|
|
}
|