/**
 * @bobfrankston/mailx-core
 * Core mail functions — callable directly via IPC or wrapped by Express.
 * No HTTP, no Express, no WebSocket. Just plain async functions.
 */
import { MailxDB, FileMessageStore, Store, parseSerial } from "@bobfrankston/mailx-store";
import { ImapManager } from "@bobfrankston/mailx-imap";
import { loadSettings, saveSettings, loadAllowlist, saveAllowlist, getConfigDir, getStorePath, initLocalConfig } from "@bobfrankston/mailx-settings";
// ── Initialization ──
let db;
let imapManager;
const eventHandlers = [];
export function onEvent(handler) {
    eventHandlers.push(handler);
}
function emit(event) {
    for (const h of eventHandlers) {
        try {
            h(event);
        }
        catch { /* ignore handler errors */ }
    }
}
export async function initialize(transportFactory) {
    initLocalConfig();
    const dbDir = getConfigDir();
    db = new MailxDB(dbDir);
    const store = new Store(db, new FileMessageStore(getStorePath()));
    imapManager = new ImapManager(store, transportFactory || (() => { throw new Error("No transport factory provided"); }));
    // Seed contacts — async + chunked, runs in the background so it never
    // blocks boot.
    db.seedContactsFromMessages()
        .then(seeded => { if (seeded > 0)
        console.log(`  Seeded ${seeded} contacts`); })
        .catch(e => console.error(`  contact seed failed: ${e?.message || e}`));
    // Search index — only if empty
    let ftsCount = 0;
    try {
        ftsCount = db.db.prepare("SELECT COUNT(*) as cnt FROM messages_fts").get()?.cnt || 0;
    }
    catch { /* */ }
    if (ftsCount === 0) {
        const indexed = db.rebuildSearchIndex();
        if (indexed > 0)
            console.log(`  Search index: ${indexed} messages`);
    }
    // One-time background conversion of an existing index to the trigram
    // tokenizer (substring search). Long-lived host only — no-op once done.
    db.startFtsTrigramMigration();
    // Add accounts
    const settings = loadSettings();
    for (const account of settings.accounts) {
        if (!account.enabled)
            continue;
        try {
            await imapManager.addAccount(account);
            console.log(`  Account added: ${account.name} (${account.id})`);
        }
        catch (e) {
            console.error(`  Failed to add account ${account.id}: ${e.message}`);
        }
    }
    // Wire events to push notifications
    imapManager.on("syncProgress", (accountId, phase, progress) => {
        emit({ type: "syncProgress", accountId, phase, progress });
    });
    imapManager.on("folderCountsChanged", (accountId, counts) => {
        emit({ type: "folderCountsChanged", accountId, counts });
    });
    // Initial sync + IDLE
    if (settings.accounts.some(a => a.enabled)) {
        console.log("  Starting initial sync...");
        imapManager.syncAll().then(async () => {
            console.log("  Initial sync complete");
            await imapManager.startWatching();
            imapManager.syncAllContacts().catch(e => console.error(`  Google Contacts sync error: ${e.message}`));
        }).catch(e => {
            console.error(`  Initial sync error: ${e.message}`);
        });
    }
    imapManager.startPeriodicSync(settings.sync.intervalMinutes);
    imapManager.startOutboxWorker();
}
export async function shutdown() {
    imapManager?.stopPeriodicSync();
    imapManager?.stopOutboxWorker();
    await imapManager?.shutdown();
    db?.close();
}
// ── HTML Sanitization ──
function sanitizeHtml(html) {
    let hasRemoteContent = false;
    let clean = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
    clean = clean.replace(/\s+on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "");
    clean = clean.replace(/<img\b([^>]*)\bsrc\s*=\s*("[^"]*"|'[^']*')/gi, (match, before, src) => {
        const url = src.slice(1, -1);
        if (url.startsWith("data:") || url.startsWith("cid:"))
            return match;
        hasRemoteContent = true;
        return `<img${before}src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Crect fill='%23888' width='20' height='20' rx='3'/%3E%3Ctext x='10' y='14' text-anchor='middle' fill='white' font-size='12'%3E⊘%3C/text%3E%3C/svg%3E" data-blocked-src=${src} title="Remote image blocked"`;
    });
    clean = clean.replace(/<link\b[^>]*rel\s*=\s*["']stylesheet["'][^>]*>/gi, (match) => {
        hasRemoteContent = true;
        return `<!-- blocked: ${match.replace(/--/g, "")} -->`;
    });
    clean = clean.replace(/url\s*\(\s*(['"]?)(https?:\/\/[^)]+)\1\s*\)/gi, (_match, _q, url) => {
        hasRemoteContent = true;
        return `url("") /* blocked: ${url} */`;
    });
    clean = clean.replace(/<\/?form\b[^>]*>/gi, "");
    clean = clean.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, "");
    return { html: clean, hasRemoteContent };
}
// ── API Functions ──
export function getAccounts() {
    return db.getAccounts();
}
export function getFolders(params) {
    return db.getFolders(params.accountId);
}
export function getMessages(params) {
    return db.getMessages({
        accountId: params.accountId,
        folderId: params.folderId,
        page: params.page || 1,
        pageSize: params.pageSize || 50,
        sort: params.sort || "date",
        sortDir: params.sortDir || "desc",
        search: params.search,
        flaggedOnly: params.flaggedOnly,
    });
}
export function getUnifiedInbox(params) {
    return db.getUnifiedInbox(params.page, params.pageSize);
}
export async function getMessage(params) {
    const { accountId, uid } = params;
    let allowRemote = params.allowRemote || false;
    const envelope = db.getMessageByUid(accountId, uid, params.folderId);
    if (!envelope)
        throw new Error("Message not found");
    let bodyHtml = "";
    let bodyText = "";
    let hasRemoteContent = false;
    let attachments = [];
    let deliveredTo = "";
    let returnPath = "";
    let listUnsubscribe = "";
    let listUnsubscribeMail = "";
    let listUnsubscribeHttp = "";
    let listUnsubscribeOneClick = false;
    const raw = await imapManager.fetchMessageBody(accountId, envelope.folderId, envelope.uid);
    if (raw) {
        const parsed = await parseSerial(raw);
        bodyHtml = parsed.html || "";
        bodyText = parsed.text || "";
        attachments = (parsed.attachments || []).map((a, i) => ({
            id: i,
            filename: a.filename || `attachment-${i}`,
            mimeType: a.contentType || "application/octet-stream",
            size: a.size || 0,
            contentId: a.contentId || "",
        }));
        // Extract useful headers for the UI
        const hdr = (key) => {
            let v = parsed.headers.get(key);
            if (!v)
                return "";
            if (Array.isArray(v))
                v = v[0];
            if (typeof v === "string")
                return v;
            if (typeof v === "object" && v !== null) {
                if ("text" in v)
                    return v.text || "";
                if ("value" in v)
                    return String(v.value);
                if ("address" in v)
                    return v.address || "";
            }
            return String(v);
        };
        // Get the real Delivered-To, skipping relay domains from account config.
        // Prefix-strip is gone — mlproc cleans the header server-side now.
        const settings = loadSettings();
        const acctConfig = settings.accounts.find(a => a.id === accountId);
        const relayDomains = acctConfig?.relayDomains || [];
        const rawDelivered = parsed.headers.get("delivered-to");
        if (rawDelivered) {
            const deliveredList = Array.isArray(rawDelivered) ? rawDelivered : [rawDelivered];
            // Iterate FIRST → last. Each delivery agent PREPENDS its own
            // Delivered-To, so the first (topmost) header is the final
            // delivery to the user's actual mailbox; later ones are earlier
            // forwarding hops / internal routing artifacts. The old loop ran
            // backwards and picked the LAST one — on a forwarded message that
            // grabbed a stale hop (Bob 2026-05-21: a malformed two-@
            // `…@elkin.ws@trap-prot` trap-protection address). Relay domains
            // from account config are still skipped so a leading relay hop
            // doesn't win over the real address.
            for (let i = 0; i < deliveredList.length; i++) {
                const d = deliveredList[i];
                const addr = typeof d === "string" ? d : d?.text || d?.address || String(d);
                if (!relayDomains.some((rd) => addr.includes(`@${rd}`))) {
                    deliveredTo = addr;
                    break;
                }
            }
            if (!deliveredTo && deliveredList.length > 0) {
                // All entries were relay domains — fall back to the first.
                const d = deliveredList[0];
                deliveredTo = typeof d === "string" ? d : d?.text || d?.address || String(d);
            }
        }
        returnPath = hdr("return-path").replace(/[<>]/g, "");
        ({ listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick } =
            parseListUnsubscribe(parsed.headers));
        listUnsubscribe = listUnsubscribeHttp || listUnsubscribeMail;
    }
    if (bodyHtml && !allowRemote) {
        const allowList = loadAllowlist();
        const senderAddr = envelope.from?.address || "";
        const senderDomain = senderAddr.split("@")[1] || "";
        const toAddrs = (envelope.to || []).map((a) => a.address);
        const isAllowed = allowList.senders.includes(senderAddr) ||
            allowList.domains.includes(senderDomain) ||
            toAddrs.some((a) => allowList.recipients?.includes(a));
        if (isAllowed) {
            allowRemote = true;
        }
        else {
            const result = sanitizeHtml(bodyHtml);
            bodyHtml = result.html;
            hasRemoteContent = result.hasRemoteContent;
        }
    }
    // Build .eml file path for "View Source"
    const storePath = getStorePath();
    const emlPath = `${storePath}/${accountId}/${envelope.folderId}/${envelope.uid}.eml`;
    return { ...envelope, bodyHtml, bodyText, hasRemoteContent, remoteAllowed: allowRemote, attachments, deliveredTo, returnPath, listUnsubscribe, listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick, emlPath };
}
/** Parse `List-Unsubscribe` (RFC 2369) and `List-Unsubscribe-Post` (RFC 8058).
 *  Returns mailto URL, HTTPS URL, and whether one-click POST is allowed.
 *  mailparser only exposes ONE of mail/url even when both are present, so we
 *  also scan the raw header text for the full set of angle-bracketed URIs. */
function parseListUnsubscribe(headers) {
    let mail = "";
    let http = "";
    let oneClick = false;
    // Raw header scan — most reliable.
    const raw = headers.get("list-unsubscribe");
    const rawStr = typeof raw === "string" ? raw : (raw && typeof raw.text === "string" ? raw.text : "");
    if (rawStr) {
        const matches = rawStr.match(/<([^>]+)>/g) || [];
        for (const m of matches) {
            const url = m.slice(1, -1).trim();
            if (!mail && /^mailto:/i.test(url))
                mail = url;
            else if (!http && /^https?:/i.test(url))
                http = url;
        }
    }
    // mailparser fallback — for builds where headers.get returns the parsed object.
    if (!mail && !http) {
        const listHeaders = headers.get("list");
        if (listHeaders?.unsubscribe) {
            const unsub = listHeaders.unsubscribe;
            if (unsub.url)
                http = Array.isArray(unsub.url) ? unsub.url[0] : unsub.url;
            if (unsub.mail)
                mail = `mailto:${Array.isArray(unsub.mail) ? unsub.mail[0] : unsub.mail}`;
        }
    }
    const post = headers.get("list-unsubscribe-post");
    const postStr = typeof post === "string" ? post : (post && typeof post.text === "string" ? post.text : "");
    if (postStr && /one-?click/i.test(postStr))
        oneClick = true;
    return { listUnsubscribeMail: mail, listUnsubscribeHttp: http, listUnsubscribeOneClick: oneClick };
}
export async function updateFlags(params) {
    const envelope = db.getMessageByUid(params.accountId, params.uid);
    const folderId = envelope?.folderId || 0;
    db.updateMessageFlags(params.accountId, folderId, params.uid, params.flags);
    db.queueSyncAction(params.accountId, "flags", params.uid, folderId, { flags: params.flags });
    // Best-effort drain; failures retry from the next reconciler pass.
    imapManager.processSyncActions(params.accountId).catch(() => { });
}
export async function deleteMessage(params) {
    const envelope = db.getMessageByUid(params.accountId, params.uid);
    if (!envelope)
        throw new Error("Message not found");
    // mailx-core has its own DB/imapManager refs but no LocalStore — inline
    // the trash + queue ops here. (This package is unused; see refactor notes.)
    const folders = db.getFolders(params.accountId);
    const trash = folders.find(f => f.specialUse === "trash" || f.path.toLowerCase() === "trash");
    if (trash && trash.id !== envelope.folderId) {
        db.moveMessageLocal(params.accountId, params.uid, envelope.folderId, trash.id);
        db.queueSyncAction(params.accountId, "move", params.uid, envelope.folderId, { targetFolderId: trash.id });
    }
    else {
        db.deleteMessage(params.accountId, envelope.folderId, params.uid, "mailx-core deleteMessage (no trash)", "mailx-core");
        db.queueSyncAction(params.accountId, "delete", params.uid, envelope.folderId);
    }
    imapManager.processSyncActions(params.accountId).catch(() => { });
}
export async function undeleteMessage(params) {
    const folders = db.getFolders(params.accountId);
    const trash = folders.find(f => f.specialUse === "trash" || f.path.toLowerCase() === "trash");
    if (!trash)
        throw new Error("No Trash folder configured");
    db.moveMessageLocal(params.accountId, params.uid, trash.id, params.folderId);
    db.queueSyncAction(params.accountId, "move", params.uid, trash.id, { targetFolderId: params.folderId });
    imapManager.processSyncActions(params.accountId).catch(() => { });
}
export async function sendMessage(params) {
    const settings = loadSettings();
    const account = settings.accounts.find(a => a.id === params.from);
    if (!account)
        throw new Error(`Unknown account: ${params.from}`);
    const to = params.to.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
    const cc = params.cc?.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
    const bcc = params.bcc?.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
    const headers = [
        `From: ${account.name} <${account.email}>`,
        `To: ${to}`,
        cc ? `Cc: ${cc}` : null,
        bcc ? `Bcc: ${bcc}` : null,
        `Subject: ${params.subject}`,
        `Date: ${new Date().toUTCString()}`,
        params.inReplyTo ? `In-Reply-To: ${params.inReplyTo}` : null,
        params.references?.length ? `References: ${params.references.join(" ")}` : null,
        `MIME-Version: 1.0`,
        `Content-Type: text/html; charset=UTF-8`,
    ].filter(h => h !== null).join("\r\n");
    const rawMessage = `${headers}\r\n\r\n${params.bodyHtml || params.bodyText || ""}`;
    imapManager.queueOutgoingLocal(account.id, rawMessage);
    for (const addr of params.to)
        db.recordSentAddress(addr.name, addr.address);
    if (params.cc)
        for (const addr of params.cc)
            db.recordSentAddress(addr.name, addr.address);
    if (params.bcc)
        for (const addr of params.bcc)
            db.recordSentAddress(addr.name, addr.address);
}
export async function saveDraft(params) {
    const settings = loadSettings();
    const account = settings.accounts.find(a => a.id === params.accountId);
    if (!account)
        throw new Error(`Unknown account: ${params.accountId}`);
    const headers = [
        `From: ${account.name} <${account.email}>`,
        params.to ? `To: ${params.to}` : null,
        params.cc ? `Cc: ${params.cc}` : null,
        `Subject: ${params.subject || "(no subject)"}`,
        `Date: ${new Date().toUTCString()}`,
        `MIME-Version: 1.0`,
        `Content-Type: text/html; charset=UTF-8`,
    ].filter(h => h !== null).join("\r\n");
    const raw = `${headers}\r\n\r\n${params.bodyHtml || params.bodyText || ""}`;
    return await imapManager.saveDraft(params.accountId, raw, params.previousDraftUid);
}
export async function deleteDraft(params) {
    await imapManager.deleteDraft(params.accountId, params.draftUid);
}
export function searchMessages(params) {
    if (!params.query.trim())
        return { items: [], total: 0, page: 1, pageSize: 50 };
    return db.searchMessages(params.query, params.page || 1, params.pageSize || 50);
}
export function searchContacts(params) {
    if (params.query.length < 1)
        return [];
    return db.searchContacts(params.query);
}
export async function syncAll() {
    await imapManager.syncAll();
}
export function getSyncPending() {
    return { pending: db.getTotalPendingSyncCount() };
}
export async function allowRemoteContent(params) {
    const list = loadAllowlist();
    if (params.type === "sender" && !list.senders.includes(params.value))
        list.senders.push(params.value);
    else if (params.type === "domain" && !list.domains.includes(params.value))
        list.domains.push(params.value);
    else if (params.type === "recipient") {
        if (!list.recipients)
            list.recipients = [];
        if (!list.recipients.includes(params.value))
            list.recipients.push(params.value);
    }
    await saveAllowlist(list);
}
export function getSettings() {
    return loadSettings();
}
export function saveSettingsData(data) {
    saveSettings(data);
}
export function rebuildSearchIndex() {
    return db.rebuildSearchIndex();
}
export function seedContacts() {
    return db.seedContactsFromMessages();
}
export async function syncGoogleContacts() {
    await imapManager.syncAllContacts();
}
export function getVersion() {
    return { server: "1.0.0", client: "1.0.0" }; // Updated by build
}
// ── Action dispatcher for IPC ──
const actions = {
    getAccounts, getFolders, getMessages, getUnifiedInbox, getMessage,
    updateFlags, deleteMessage, undeleteMessage, sendMessage,
    saveDraft, deleteDraft, searchMessages, searchContacts,
    syncAll, getSyncPending, allowRemoteContent,
    getSettings, saveSettingsData, rebuildSearchIndex,
    seedContacts, syncGoogleContacts, getVersion,
};
/** Dispatch an action by name — used by IPC and Express wrapper */
export async function dispatch(action, params = {}) {
    const fn = actions[action];
    if (!fn)
        throw new Error(`Unknown action: ${action}`);
    return await fn(params);
}
export { db, imapManager };
//# sourceMappingURL=index.js.map