/**
 * mailxapi — IPC bridge injected into WebView by the Rust launcher.
 * Follows the msgapi pattern: callNode → Promise → _msgapiServiceResolve/Reject.
 *
 * When running in a browser (no IPC), this file is not loaded.
 * api-client.ts auto-detects and falls back to HTTP fetch.
 */

(function() {
    "use strict";

    // Reliable Android-host detection. navigator.userAgent is NOT dependable —
    // on the Pixel's MAUI WebView it does NOT contain "Android", so the old
    // `/Android/i.test(navigator.userAgent)` returned false and every
    // openExternal / openContact / openCalendarEvent fell through to the desktop
    // path and did nothing (Bob 2026-06-18, confirmed via [android-link] trace:
    // "android=false"). The MAUI host injects `window._nativeBridge`
    // (MainPage.xaml.cs) — that's the authoritative signal. UA kept as a backup.
    function isAndroidHost() {
        try { return !!(window._nativeBridge) || /Android/i.test(navigator.userAgent); }
        catch (e) { return false; }
    }

    var _callbacks = {};
    var _callbackId = 0;
    var _eventHandlers = [];
    // 2026-05-13: trace WebView↔daemon pipe latency. The IPC has three
    // stages: (1) WebView _ptick before postMessage, (2) Rust msger forwards
    // to daemon stdin, (3) daemon logs `[ipc] ← <action> (<cbid>)`. Diffing
    // (3) - (1) by cbid reveals whether a delay is in the WebView call
    // queue or in the daemon's event loop. The tag IS visible in the daemon
    // log via logClientEvent — `[client] ipc-send <action> cbid=N tEpoch=...`.
    // High-res `performance.now()` is local to the WebView; we also include
    // wall-clock so daemon-side timestamps align.
    // The service channel. Normally the host-injected window.ipc (msger Rust
    // WebView2 / MAUI). Popout windows can't use it — msger injects window.ipc
    // as a NON-WRITABLE, NON-CONFIGURABLE property wired to the messagebox
    // channel, where service RPC dies silently. There the popout bridge
    // (popout-bridge.ts) sets window.__rmfIpcOverride, an HTTP-backed
    // postMessage that carries the same payloads to the popout server's /rpc.
    function _chan() {
        return window.__rmfIpcOverride || window.ipc;
    }

    function _traceIpcSend(action, id) {
        try {
            var hr = (typeof performance !== "undefined" && performance.now) ? performance.now().toFixed(1) : "n/a";
            var line = "ipc-send " + action + " cbid=" + id + " hr=" + hr + " wall=" + Date.now();
            // Send via direct postMessage to avoid recursion — logClientEvent
            // would re-enter callNode and create a circular trace.
            var chan = _chan();
            if (chan && chan.postMessage) {
                chan.postMessage(JSON.stringify({
                    _action: "logClientEvent", _cbid: "trace-" + id, tag: line,
                }));
            }
        } catch (e) { /* tracing must never break the call */ }
    }
    // Per-action IPC timeout ceiling. Default 30s — covers local DB reads,
    // parses, and writes that complete in well under 1s on a healthy
    // daemon (anything longer = event loop wedged, retry faster). Actions
    // that legitimately wait on a network round-trip get a higher ceiling.
    // aiTransform: cloud-AI inference (Claude/OpenAI/Ollama) takes 5-30s
    // for a paragraph of text, 30-60s for a long message — 90s ceiling
    // catches genuine wedges without falsely failing real waits.
    var ACTION_TIMEOUT_MS = {
        aiTransform: 90000,
        // Attachment open/fetch parses the full .eml via the parse worker.
        // A cold worker is 14-25s and a large PDF adds base64 transfer time,
        // so the default 30s ceiling fired a spurious "failed to open"
        // *after* the file had actually opened. 120s covers a cold start.
        getAttachment: 120000,
        openAttachment: 120000,
        // A server-side ("Server" checkbox) search goes out to IMAP/Gmail
        // and can run well past 30s on a large mailbox. The default ceiling
        // turned a slow-but-fine search into "mailxapi timeout:
        // searchMessages". Local search stays fast and well under any limit.
        searchMessages: 120000,
        // getVersion reads package.json — instant. With the startup-race
        // re-send above (it's in RESENDABLE) a dropped post is re-tried every
        // 2s, so a 5s ceiling normally allows ~2 re-sends before giving up —
        // enough to ride out the brief window before the Rust→daemon pipe is
        // wired. Short ceiling so a genuinely wedged daemon surfaces fast
        // rather than hanging the version display for 30s.
        getVersion: 5000,
        // A reminder popup sits on screen until the HUMAN reacts — minutes,
        // hours, overnight. 0 = no client-side timeout. The 30s default was
        // auto-dismissing reminders under the user: popup up, ceiling fired,
        // the timeout was treated as Dismiss (and persisted!), and the
        // user's actual click went to a dead callback (Bob 2026-07-26, the
        // 20:00 Walgreens task reminder). Genuine transport failures still
        // surface: msger crash / daemon error rejects via
        // _msgapiServiceReject, so no-timeout does not mean hung-forever.
        showReminderPopup: 0,
    };
    // Actions that are PURE READS and therefore safe to auto-re-send if the
    // first post is lost. Why a post gets lost: at startup the WebView can
    // call `window.ipc.postMessage` after `window.ipc` is injected but BEFORE
    // the Rust host has wired its message handler to the daemon's stdin — the
    // post is accepted by WebView2 and silently dropped downstream, so no
    // response ever comes and the callback times out. That's the real cause of
    // "getVersion got dropped" (Bob 2026-06-03) — a startup race in our own
    // bridge, not a mysterious msger behavior. Re-sending a read is harmless
    // (the daemon ignores a duplicate response — _msgapiServiceResolve no-ops
    // when the callback is already gone). WRITES are NOT listed: re-sending a
    // send/saveDraft/delete/move/flag could double-apply.
    var RESENDABLE = {
        getVersion: 1, getAccounts: 1, getFolders: 1, getUnifiedInbox: 1, getMessages: 1,
        getSettings: 1, getAutocompleteSettings: 1, getDiagnostics: 1, getOutboxStatus: 1,
        getSyncPending: 1, getCalendars: 1, getCalendarEvents: 1, getTasks: 1,
        getPrimaryAccount: 1, getThreadMessages: 1, getPriorityLists: 1
    };
    var RESEND_INTERVAL_MS = 2000;
    function callNode(action, params) {
        var id = String(++_callbackId);
        var msgStr = JSON.stringify(Object.assign({ _action: action, _cbid: id }, params || {}));
        return new Promise(function(resolve, reject) {
            function send() {
                var chan = _chan();
                if (chan && chan.postMessage) {
                    // Trace BEFORE the call so a slow daemon-receive shows up as
                    // ipc-send + N s before the daemon logs `[ipc] ←`. Skip the
                    // trace event itself to avoid recursion.
                    if (action !== "logClientEvent") _traceIpcSend(action, id);
                    chan.postMessage(msgStr);
                    return true;
                }
                return false;
            }
            var entry = { resolve: resolve, reject: reject, timer: null, resend: null };
            _callbacks[id] = entry;
            // Explicit 0 in the table = no timeout (human-paced actions).
            var ceiling = (action in ACTION_TIMEOUT_MS) ? ACTION_TIMEOUT_MS[action] : 30000;
            if (ceiling > 0) {
                entry.timer = setTimeout(function() {
                    delete _callbacks[id];
                    if (entry.resend) clearInterval(entry.resend);
                    reject(new Error("mailxapi timeout: " + action));
                }, ceiling);
            }
            var sent = send();
            if (!sent && !RESENDABLE[action]) {
                // Channel object not injected yet AND not safe to wait/re-send
                // (a write) → fail now rather than risk it.
                clearTimeout(entry.timer);
                delete _callbacks[id];
                reject(new Error("No IPC channel available"));
                return;
            }
            // Re-send pure reads until a response lands or the ceiling fires —
            // heals a startup-race drop (and a not-yet-injected window.ipc).
            if (RESENDABLE[action]) {
                entry.resend = setInterval(send, RESEND_INTERVAL_MS);
            }
        });
    }

    // Called by host (msger Rust / msgview preload) to resolve service-channel promises
    window._msgapiServiceResolve = function(id, value) {
        var cb = _callbacks[id];
        if (!cb) return;
        delete _callbacks[id];
        clearTimeout(cb.timer);
        if (cb.resend) clearInterval(cb.resend);
        cb.resolve(value);
    };

    // Called by host to reject service-channel promises
    window._msgapiServiceReject = function(id, error) {
        var cb = _callbacks[id];
        if (!cb) return;
        delete _callbacks[id];
        clearTimeout(cb.timer);
        if (cb.resend) clearInterval(cb.resend);
        cb.reject(new Error(error));
    };

    // Called by host to push events (new mail, sync progress, etc.)
    window._msgapiServiceEvent = function(event) {
        for (var i = 0; i < _eventHandlers.length; i++) {
            try { _eventHandlers[i](event); } catch(e) { /* ignore */ }
        }
    };

    window.mailxapi = {
        isApp: true,
        platform: "webview2",

        // Accounts
        getAccounts: function() { return callNode("getAccounts"); },
        getFolders: function(accountId) { return callNode("getFolders", { accountId: accountId }); },

        // Messages
        getMessages: function(accountId, folderId, page, pageSize, sort, sortDir, search, flaggedOnly, dateBasis) {
            return callNode("getMessages", { accountId: accountId, folderId: folderId, page: page, pageSize: pageSize, sort: sort, sortDir: sortDir, search: search, flaggedOnly: flaggedOnly, dateBasis: dateBasis });
        },
        getUnifiedInbox: function(page, pageSize, flaggedOnly, dateBasis) {
            return callNode("getUnifiedInbox", { page: page, pageSize: pageSize, flaggedOnly: flaggedOnly, dateBasis: dateBasis });
        },
        getMessage: function(accountId, uid, allowRemote, folderId) {
            return callNode("getMessage", { accountId: accountId, uid: uid, allowRemote: allowRemote, folderId: folderId });
        },

        // Actions
        updateFlags: function(accountId, uid, flags, folderId) {
            return callNode("updateFlags", { accountId: accountId, uid: uid, flags: flags, folderId: folderId });
        },
        deleteMessage: function(accountId, uid, folderId) {
            return callNode("deleteMessage", { accountId: accountId, uid: uid, folderId: folderId });
        },
        undeleteMessage: function(accountId, uid, folderId) {
            return callNode("undeleteMessage", { accountId: accountId, uid: uid, folderId: folderId });
        },

        // Diagnostic tracing — callers (compose iframe, app, components) can
        // ship arbitrary named events to the Node log. Surfaces as
        // `[client] <tag> <data>` so a failing pipeline can be traced end
        // to end in a single log file.
        logClientEvent: function(tag, data) { return callNode("logClientEvent", { tag: tag, data: data }); },

        // Answer leg of --debug-server /api/eval (daemon pushes a debugEval
        // event; app.ts evaluates and replies through this).
        debugEvalResult: function(id, result, error) { return callNode("debugEvalResult", { id: id, result: result, error: error }); },

        // Compose
        sendMessage: function(msg) { return callNode("sendMessage", msg); },
        saveDraft: function(params) { return callNode("saveDraft", params); },
        deleteDraft: function(accountId, draftUid, draftId) {
            return callNode("deleteDraft", { accountId: accountId, draftUid: draftUid, draftId: draftId });
        },
        // Compose-open probe: newer copy of this draft on the server (edited
        // on another machine)? Returns { uid, folderId } or null.
        checkDraftNewer: function(accountId, draftId, sinceUid) {
            return callNode("checkDraftNewer", { accountId: accountId, draftId: draftId, sinceUid: sinceUid });
        },

        // Search
        searchMessages: function(query, page, pageSize, scope, accountId, folderId, includeTrashSpam) {
            return callNode("searchMessages", { query: query, page: page, pageSize: pageSize, scope: scope, accountId: accountId, folderId: folderId, includeTrashSpam: !!includeTrashSpam });
        },
        addContact: function(name, email) {
            return callNode("addContact", { name: name, email: email });
        },
        getThreadMessages: function(accountId, threadId) {
            return callNode("getThreadMessages", { accountId: accountId, threadId: threadId });
        },
        // Calendar / Tasks two-way cache. Reads return local DB immediately;
        // writes commit locally and queue a push-to-Google action.
        getCalendarEvents: function(fromMs, toMs) {
            return callNode("getCalendarEvents", { fromMs: fromMs, toMs: toMs });
        },
        getCalendars: function() { return callNode("getCalendars"); },
        createCalendarEvent: function(ev) { return callNode("createCalendarEvent", ev); },
        updateCalendarEvent: function(uuid, patch) { return callNode("updateCalendarEvent", { uuid: uuid, patch: patch }); },
        deleteCalendarEvent: function(uuid) { return callNode("deleteCalendarEvent", { uuid: uuid }); },
        getTasks: function(includeCompleted) { return callNode("getTasks", { includeCompleted: !!includeCompleted }); },
        createTask: function(t) { return callNode("createTask", t); },
        updateTask: function(uuid, patch) { return callNode("updateTask", { uuid: uuid, patch: patch }); },
        deleteTask: function(uuid) { return callNode("deleteTask", { uuid: uuid }); },
        drainStoreSync: function() { return callNode("drainStoreSync"); },
        recordSpamReport: function(accountId, uid, folderId) {
            return callNode("recordSpamReport", { accountId: accountId, uid: uid, folderId: folderId });
        },
        readJsoncFile: function(name) {
            return callNode("readJsoncFile", { name: name });
        },
        writeJsoncFile: function(name, content) {
            return callNode("writeJsoncFile", { name: name, content: content });
        },
        formatJsonc: function(content) {
            return callNode("formatJsonc", { content: content });
        },
        readConfigHelp: function(name) {
            return callNode("readConfigHelp", { name: name });
        },
        unsubscribeOneClick: function(url) {
            return callNode("unsubscribeOneClick", { url: url });
        },
        openInWord: function(editId, html, fromPopout) {
            return callNode("openInWord", { editId: editId, html: html, fromPopout: !!fromPopout });
        },
        closeWordEdit: function(editId) {
            return callNode("closeWordEdit", { editId: editId });
        },
        showReminderPopup: function(opts) {
            return callNode("showReminderPopup", opts);
        },
        // Retract an open reminder popup by its occurrence key — used when
        // the cross-device state sync learns another machine handled it.
        closeReminderPopup: function(key) {
            return callNode("closeReminderPopup", { key: key });
        },
        // Cross-device reminder dismissed/snoozed state (reminders.jsonc on
        // Drive). mergeReminderState sends the device's local maps and gets
        // the merged union back — push and pull in one round-trip.
        getReminderState: function() {
            return callNode("getReminderState");
        },
        mergeReminderState: function(patch) {
            return callNode("mergeReminderState", { patch: patch });
        },
        consumePendingMailto: function() {
            return callNode("consumePendingMailto");
        },
        aiTransform: function(req) {
            return callNode("aiTransform", req);
        },
        // Restart the background service so edits to accounts.jsonc /
        // allowlist.jsonc / other live config take effect without the user
        // having to type `mailx -kill`. The service spawns a fresh detached
        // daemon and then exits; the new daemon takes over.
        // Windows taskbar overlay badge (Thunderbird-style unread count pill).
        // HOST action — handled by msger's Rust WebView, NOT the Node daemon —
        // so it posts the `_action` directly and does NOT go through callNode
        // (which waits for a Node response that never comes for host actions).
        // Fire-and-forget. `pngB64` = base64 PNG of the badge, "" to clear.
        // No-op on Linux/Mac (msger ignores it there).
        setTaskbarOverlay: function(pngB64, description) {
            try {
                if (window.ipc && window.ipc.postMessage) {
                    window.ipc.postMessage(JSON.stringify({
                        _action: "setTaskbarOverlay",
                        png: pngB64 || "",
                        description: description || "",
                    }));
                }
            } catch (e) { /* host unavailable */ }
        },

        restartDaemon: function() {
            return new Promise(function(resolve) {
                var id = String(++_callbackId);
                _callbacks[id] = { resolve: resolve, reject: resolve, timer: setTimeout(function() {
                    delete _callbacks[id];
                    resolve({ ok: true, status: "service-gone" });
                }, 5000) };
                try { window.ipc.postMessage(JSON.stringify({ _action: "restartDaemon", _cbid: id })); } catch(e) { resolve({ ok: false }); }
            });
        },
        searchContacts: function(query) {
            return callNode("searchContacts", { query: query });
        },
        listContacts: function(query, page, pageSize) {
            return callNode("listContacts", { query: query || "", page: page || 1, pageSize: pageSize || 100 });
        },
        upsertContact: function(name, email, fields) {
            return callNode("upsertContact", { name: name, email: email, fields: fields });
        },
        deleteContact: function(email) {
            return callNode("deleteContact", { email: email });
        },
        addPreferredContact: function(name, email, source, organization) {
            return callNode("addPreferredContact", {
                name: name || "", email: email,
                source: source || "", organization: organization || "",
            });
        },
        addToDenylist: function(email) {
            return callNode("addToDenylist", { email: email });
        },
        getPriorityLists: function() {
            return callNode("getPriorityLists", {});
        },
        setPrioritySender: function(email, value, name) {
            return callNode("setPrioritySender", { email: email, value: !!value, name: name || "" });
        },
        setPriorityDomain: function(domain, value) {
            return callNode("setPriorityDomain", { domain: domain, value: !!value });
        },
        openLocalPath: function(which) {
            return callNode("openLocalPath", { which: which });
        },

        // Item 17: platform-aware external openers. On Android (MAUI shell)
        // these fire native Intents so Contacts/Calendar links jump to the
        // OS apps instead of the browser. On desktop the MAUI shell isn't
        // in play; we fall through to a URL open via the navigation path
        // that msger / external launcher handles the same as any http link.
        openContact: function(email) {
            try {
                // MAUI intercepts this scheme in OnNavigating and fires a
                // native ACTION_VIEW intent on the mailto: URI — system
                // Contacts registers for it on stock Android.
                if (isAndroidHost()) {
                    window.location.href = "mailxapi-intent://contact/" + encodeURIComponent(email);
                    return;
                }
            } catch (e) { /* navigation blocked — fall through */ }
            // Desktop / browser fallback: open mailto: — msger forwards
            // mailto URLs to the system default handler, which on a mailx
            // install is mailx itself (our own compose window takes over).
            window.location.href = "mailto:" + email;
        },
        openCalendarEvent: function(htmlLink) {
            if (!htmlLink) return;
            try {
                if (isAndroidHost()) {
                    // MAUI dispatches this to the native Calendar app via
                    // ACTION_VIEW on the Google Calendar event URL.
                    window.location.href = "mailxapi-intent://calendar/" + encodeURIComponent(htmlLink);
                    return;
                }
            } catch (e) { /* */ }
            // Desktop: route through openExternal so it lands in the system
            // browser, not a local msger WebView window.
            this.openExternal(htmlLink);
        },

        // Open an http/https/mailto URL in the OS default browser/handler.
        // Desktop: window.open inside msger's WebView2 opens a LOCAL in-app
        // window (or no-ops), so links/unsubscribe "stayed local" (Bob
        // 2026-06-13). Route through the Node process, which spawns the OS
        // opener. Android: hand the URL to MAUI's native intent bridge.
        openExternal: function(url) {
            if (!url) return;
            try {
                console.log("[android-link] openExternal url=" + url + " androidHost=" + isAndroidHost() + " bridge=" + !!(window._nativeBridge) + " ua=" + /Android/i.test(navigator.userAgent));
                if (isAndroidHost()) {
                    console.log("[android-link] navigating top frame to mailxapi-intent://open/");
                    // Use the SAME mechanism the contact/calendar intents use: a
                    // top-frame location.href to mailxapi-intent://, which the MAUI
                    // WebView's OnNavigating reliably intercepts. The old hidden-
                    // iframe to mailxapi://openurl did NOT work — OnNavigating fires
                    // for MAIN-frame navigations only, not sub-frame (iframe) ones,
                    // so LinkedIn "View message" and every preview link silently did
                    // nothing (Bob 2026-06-17). location.href is canceled by the
                    // handler (e.Cancel) so the page doesn't navigate away.
                    window.location.href = "mailxapi-intent://open/" + encodeURIComponent(url);
                    return;
                }
            } catch (e) { /* fall through to IPC */ }
            return callNode("openExternal", { url: url });
        },

        // Open a message in its own NATIVE msger window (daemon spawns it via
        // the loopback popout server). Returns { ok, reason }.
        popoutWindow: function(accountId, uid, folderId, subject) {
            return callNode("popoutWindow", { accountId: accountId, uid: uid, folderId: folderId, subject: subject });
        },

        // Register rmfmail as the OS-level mailto: handler (Settings menu).
        // Returns { ok, message } — message includes the remaining manual
        // Windows Default-apps step.
        registerMailto: function() {
            return callNode("registerMailto");
        },

        // Offline grammar lint (Harper in the daemon). blocks = plain-text
        // strings; returns { blocks: [[{start,end,kind,message,suggestions}]] }.
        grammarLint: function(blocks) {
            return callNode("grammarLint", { blocks: blocks });
        },

        // Delete attachment(s) from a received message — local strip + queued
        // server replace. ids = chip id array, or "all".
        deleteAttachments: function(accountId, uid, folderId, ids) {
            return callNode("deleteAttachments", { accountId: accountId, uid: uid, folderId: folderId, ids: ids });
        },

        // Take a live compose into its own NATIVE msger window. init is the
        // full compose state (fields, body, attachments, draft identity).
        popoutCompose: function(init) {
            return callNode("popoutCompose", { init: init });
        },
        // Popout compose page boot: one-shot fetch of the stashed init.
        consumePopoutComposeInit: function(id) {
            return callNode("consumePopoutComposeInit", { id: id });
        },
        // Popout compose page after send/discard: close my window.
        closeComposePopout: function(id) {
            return callNode("closeComposePopout", { id: id });
        },
        // Compose paste: daemon-fetch a remote image → data: URI.
        fetchImageAsDataUri: function(url) {
            return callNode("fetchImageAsDataUri", { url: url });
        },
        // View → New window: another full mailx window as its own OS window.
        popoutMainWindow: function(accountId, folderId, name) {
            return callNode("popoutMainWindow", { accountId: accountId, folderId: folderId, name: name });
        },

        // Reminder sound (custom file bytes or mute flag; {} = built-in chime)
        getReminderSound: function() { return callNode("getReminderSound"); },

        // Sync
        syncAll: function() { return callNode("syncAll"); },
        syncAccount: function(accountId) { return callNode("syncAccount", { accountId: accountId }); },
        syncFolderNow: function(accountId, folderId) { return callNode("syncFolderNow", { accountId: accountId, folderId: folderId }); },
        getSyncPending: function() { return callNode("getSyncPending"); },
        getOutboxStatus: function() { return callNode("getOutboxStatus"); },
        getDiagnostics: function() { return callNode("getDiagnostics"); },
        getPrimaryAccount: function(feature) { return callNode("getPrimaryAccount", { feature: feature }); },
        listQueuedOutgoing: function() { return callNode("listQueuedOutgoing"); },
        cancelQueuedOutgoing: function(p) { return callNode("cancelQueuedOutgoing", { path: p }); },
        reauthenticate: function(accountId) { return callNode("reauthenticate", { accountId: accountId }); },
        reauthGoogleScopes: function() { return callNode("reauthGoogleScopes"); },
        hasCcHistoryTo: function(email) { return callNode("hasCcHistoryTo", { email: email }); },
        hasBccHistoryTo: function(email) { return callNode("hasBccHistoryTo", { email: email }); },

        // Bulk operations
        deleteMessages: function(accountId, uids, folderIds) {
            return callNode("deleteMessages", { accountId: accountId, uids: uids, folderIds: folderIds });
        },
        moveMessage: function(accountId, uid, targetFolderId, targetAccountId) {
            return callNode("moveMessage", { accountId: accountId, uid: uid, targetFolderId: targetFolderId, targetAccountId: targetAccountId });
        },
        moveMessages: function(accountId, uids, targetFolderId, folderIds) {
            return callNode("moveMessages", { accountId: accountId, uids: uids, targetFolderId: targetFolderId, folderIds: folderIds });
        },
        // autocomplete takes the whole request object — the daemon handler
        // reads it as `p` directly (jsonrpc: svc.autocomplete(p)).
        autocomplete: function(body) {
            return callNode("autocomplete", body || {});
        },
        cancelServerSearch: function() {
            return callNode("cancelServerSearch", {});
        },
        copyMessages: function(accountId, uids, folderIds, targetFolderId) {
            return callNode("copyMessages", { accountId: accountId, uids: uids, folderIds: folderIds, targetFolderId: targetFolderId });
        },
        markAsSpamMessages: function(accountId, uids, folderIds) {
            return callNode("markAsSpamMessages", { accountId: accountId, uids: uids, folderIds: folderIds });
        },
        performUpdate: function() {
            return callNode("performUpdate", {});
        },
        markFolderRead: function(accountId, folderId) {
            return callNode("markFolderRead", { folderId: folderId });
        },
        createFolder: function(accountId, parentPath, name) {
            return callNode("createFolder", { accountId: accountId, parentPath: parentPath, name: name });
        },
        renameFolder: function(accountId, folderId, newName, newParentId) {
            return callNode("renameFolder", { accountId: accountId, folderId: folderId, newName: newName, newParentId: newParentId });
        },
        deleteFolder: function(accountId, folderId) {
            return callNode("deleteFolder", { accountId: accountId, folderId: folderId });
        },
        moveFolderToTrash: function(accountId, folderId) {
            return callNode("moveFolderToTrash", { accountId: accountId, folderId: folderId });
        },
        emptyFolder: function(accountId, folderId) {
            return callNode("emptyFolder", { accountId: accountId, folderId: folderId });
        },

        // Settings
        allowRemoteContent: function(type, value) {
            return callNode("allowRemoteContent", { type: type, value: value });
        },
        flagSenderOrDomain: function(type, value) {
            return callNode("flagSenderOrDomain", { type: type, value: value });
        },

        // User dictionary (cloud-mirrored spellcheck additions)
        getUserDict: function() { return callNode("getUserDict"); },
        addUserDictWord: function(word) { return callNode("addUserDictWord", { word: word }); },
        addUserDictWords: function(words) { return callNode("addUserDictWords", { words: words }); },
        removeUserDictWord: function(word) { return callNode("removeUserDictWord", { word: word }); },
        getSettings: function() { return callNode("getSettings"); },
        saveSettingsData: function(data) { return callNode("saveSettingsData", data); },
        getVersion: function() { return callNode("getVersion"); },
        getAutocompleteSettings: function() { return callNode("getAutocompleteSettings"); },
        saveAutocompleteSettings: function(settings) { return callNode("saveAutocompleteSettings", settings); },

        // Attachments
        getAttachment: function(accountId, uid, attachmentId, folderId) {
            return callNode("getAttachment", { accountId: accountId, uid: uid, attachmentId: attachmentId, folderId: folderId });
        },
        openAttachment: function(accountId, uid, attachmentId, folderId) {
            return callNode("openAttachment", { accountId: accountId, uid: uid, attachmentId: attachmentId, folderId: folderId });
        },
        getMessageSource: function(accountId, uid, folderId) {
            return callNode("getMessageSource", { accountId: accountId, uid: uid, folderId: folderId });
        },

        // Setup & Repair
        setupAccount: function(name, email, password) {
            return callNode("setupAccount", { name: name, email: email, password: password });
        },
        repairAccounts: function() { return callNode("repairAccounts"); },

        // Window geometry
        saveWindowGeometry: function(geom) {
            return callNode("saveWindowGeometry", geom);
        },

        // Events
        onEvent: function(handler) { _eventHandlers.push(handler); },
    };

    window.dispatchEvent(new CustomEvent("mailxapiready"));
})();
