/** * Main thread Worker host — thin shim that keeps UI responsive. * * Responsibilities: * 1. Create Worker and proxy mailxapi calls to it * 2. Proxy TCP calls from Worker to msgapi.tcp (C# native bridge) * 3. Proxy native bridge calls (OAuth, device info) * 4. Forward events from Worker to UI event handlers * 5. Set up visibilitychange/periodic sync triggers * * The UI code (api-client.ts) calls window.mailxapi.* exactly as before — * this host just routes those calls to the Worker instead of running them inline. */ const eventHandlers: ((event: any) => void)[] = []; let worker: Worker; let rpcCounter = 0; const pendingRpc = new Map void; reject: (e: Error) => void }>(); // ── Worker communication ── function sendRpc(action: string, params: any = {}): Promise { const id = ++rpcCounter; return new Promise((resolve, reject) => { pendingRpc.set(id, { resolve, reject }); worker.postMessage({ type: "rpc", id, action, params }); // 5-minute timeout for long operations like syncAll setTimeout(() => { if (pendingRpc.has(id)) { pendingRpc.delete(id); reject(new Error(`RPC ${action} timeout`)); } }, 300000); }); } // ── TCP proxy: relay Worker TCP requests to msgapi.tcp ── function handleTcpRequest(msg: any): void { const msgapi = (window as any).msgapi; if (!msgapi?.tcp) { worker.postMessage({ type: "tcp-response", reqId: msg.reqId, error: "TCP bridge not available" }); return; } switch (msg.op) { case "connect": { msgapi.tcp.connect(msg.host, msg.port, msg.tls) .then((streamId: string) => { const sid = Number(streamId); // Register event forwarding for this stream msgapi.tcp.onData(sid, (data: string) => { worker.postMessage({ type: "tcp-data", streamId: sid, data }); }); msgapi.tcp.onClose(sid, (hadError: boolean) => { worker.postMessage({ type: "tcp-close", streamId: sid, hadError }); }); msgapi.tcp.onError(sid, (message: string) => { worker.postMessage({ type: "tcp-error", streamId: sid, message }); }); worker.postMessage({ type: "tcp-response", reqId: msg.reqId, result: String(sid) }); }) .catch((e: any) => { worker.postMessage({ type: "tcp-response", reqId: msg.reqId, error: e.message }); }); break; } case "write": { const data = msg.data; // btoa for the bridge (C# expects base64) msgapi.tcp.write(msg.streamId, data) .then(() => { worker.postMessage({ type: "tcp-response", reqId: msg.reqId, result: "ok" }); }) .catch((e: any) => { worker.postMessage({ type: "tcp-response", reqId: msg.reqId, error: e.message }); }); break; } case "upgradeTLS": { msgapi.tcp.upgradeTLS(msg.streamId, msg.servername) .then(() => { worker.postMessage({ type: "tcp-response", reqId: msg.reqId, result: "ok" }); }) .catch((e: any) => { worker.postMessage({ type: "tcp-response", reqId: msg.reqId, error: e.message }); }); break; } case "close": { try { msgapi.tcp.close(msg.streamId); } catch { /* ignore */ } break; } } } // ── Native bridge proxy: relay Worker native requests ── function handleNativeRequest(msg: any): void { const bridge = (window as any)._nativeBridge; switch (msg.op) { case "refreshToken": { const email = msg.args?.[0]; if (bridge?.app?.startOAuth) { // Use the existing OAuth flow // For now, use the cached token or refresh bridge.app.refreshToken?.(email) .then((token: string) => { worker.postMessage({ type: "native-response", reqId: msg.reqId, result: token }); }) .catch((e: any) => { worker.postMessage({ type: "native-response", reqId: msg.reqId, error: e.message }); }); } else { worker.postMessage({ type: "native-response", reqId: msg.reqId, error: "No OAuth bridge" }); } break; } case "vlog": { // Verbose logging via logit const text = msg.args?.[0] || ""; fetch(`https://rmf39.aaz.lt/logit/${encodeURIComponent("V/" + text)}?log=mailx-android`).catch(() => {}); break; } case "getAndroidId": { bridge?.app?.getAndroidId?.() .then((id: string) => { worker.postMessage({ type: "native-response", reqId: msg.reqId, result: id }); }) .catch((e: any) => { worker.postMessage({ type: "native-response", reqId: msg.reqId, error: e.message }); }); break; } case "getDeviceAccounts": { bridge?.app?.getDeviceAccounts?.() .then((accounts: any) => { worker.postMessage({ type: "native-response", reqId: msg.reqId, result: accounts }); }) .catch((e: any) => { worker.postMessage({ type: "native-response", reqId: msg.reqId, error: e.message }); }); break; } case "localStorageGet": { const key = msg.args?.[0]; worker.postMessage({ type: "native-response", reqId: msg.reqId, result: localStorage.getItem(key) }); break; } case "localStorageSet": { const [key, value] = msg.args || []; localStorage.setItem(key, value); worker.postMessage({ type: "native-response", reqId: msg.reqId, result: "ok" }); break; } default: { worker.postMessage({ type: "native-response", reqId: msg.reqId, error: `Unknown native op: ${msg.op}` }); } } } // ── Event handling ── function handleWorkerEvent(event: any): void { for (const handler of eventHandlers) { try { handler(event); } catch { /* ignore */ } } // Also dispatch to the global host service-channel event callback const cb = (window as any)._msgapiServiceEvent; if (cb) try { cb(event); } catch { /* ignore */ } } // ── Worker message router ── function onWorkerMessage(e: MessageEvent): void { const msg = e.data; if (msg.type === "rpc-response") { const pending = pendingRpc.get(msg.id); if (pending) { pendingRpc.delete(msg.id); if (msg.error) pending.reject(new Error(msg.error)); else pending.resolve(msg.result); } return; } if (msg.type === "event") { handleWorkerEvent(msg.event); return; } if (msg.type === "tcp") { handleTcpRequest(msg); return; } if (msg.type === "native") { handleNativeRequest(msg); return; } if (msg.type === "init-complete") { console.log("[host] Worker initialization complete"); return; } if (msg.type === "init-error") { console.error(`[host] Worker init failed: ${msg.error}`); return; } } // ── Install mailxapi bridge (same interface as before, routes to Worker) ── function installBridge(): void { (window as any).mailxapi = { isApp: true, platform: "android", // All service methods proxy to the Worker getAccounts: () => sendRpc("getAccounts"), getFolders: (accountId: string) => sendRpc("getFolders", { accountId }), getMessages: (accountId: string, folderId: number, page?: number, pageSize?: number) => sendRpc("getMessages", { accountId, folderId, page, pageSize }), getUnifiedInbox: (page?: number, pageSize?: number) => sendRpc("getUnifiedInbox", { page, pageSize }), getMessage: (accountId: string, uid: number, allowRemote?: boolean, folderId?: number) => sendRpc("getMessage", { accountId, uid, allowRemote, folderId }), getAttachment: (accountId: string, uid: number, index: number, folderId?: number) => sendRpc("getAttachment", { accountId, uid, index, folderId }), updateFlags: (accountId: string, uid: number, flags: any) => sendRpc("updateFlags", { accountId, uid, flags }), deleteMessage: (accountId: string, uid: number) => sendRpc("deleteMessage", { accountId, uid }), deleteMessages: (accountId: string, uids: number[]) => sendRpc("deleteMessages", { accountId, uids }), undeleteMessage: (accountId: string, uid: number, folderId: number) => sendRpc("undeleteMessage", { accountId, uid, folderId }), moveMessage: (accountId: string, uid: number, targetFolderId: number, targetAccountId?: string) => sendRpc("moveMessage", { accountId, uid, targetFolderId, targetAccountId }), moveMessages: (accountId: string, uids: number[], targetFolderId: number) => sendRpc("moveMessages", { accountId, uids, targetFolderId }), markFolderRead: (folderId: number) => sendRpc("markFolderRead", { folderId }), sendMessage: async (msg: any) => { await sendRpc("sendMessage", msg); return { ok: true }; }, saveDraft: (p: any) => sendRpc("saveDraft", p), deleteDraft: (accountId: string, draftUid: number) => sendRpc("deleteDraft", { accountId, draftUid }), syncAll: () => sendRpc("syncAll"), syncAccount: (accountId: string) => sendRpc("syncAccount", { accountId }), triggerSync: () => sendRpc("syncAll"), getSyncPending: () => sendRpc("getSyncPending"), reauthenticate: (accountId: string) => sendRpc("reauthenticate", { accountId }), searchMessages: (query: string, page?: number, pageSize?: number) => sendRpc("searchMessages", { query, page, pageSize }), searchContacts: (query: string) => sendRpc("searchContacts", { query }), listContacts: (query: string, page = 1, pageSize = 100) => sendRpc("listContacts", { query, page, pageSize }), upsertContact: (name: string, email: string) => sendRpc("upsertContact", { name, email }), deleteContact: (email: string) => sendRpc("deleteContact", { email }), addContact: (name: string, email: string) => sendRpc("addContact", { name, email }), getSettings: () => sendRpc("getSettings"), saveSettings: (data: any) => sendRpc("saveSettingsData", data), allowRemoteContent: (type: string, value: string) => sendRpc("allowRemoteContent", { type, value }), getVersion: () => sendRpc("getVersion"), getAutocompleteSettings: () => sendRpc("getAutocompleteSettings"), saveAutocompleteSettings: (data: any) => sendRpc("saveAutocompleteSettings", data), emptyFolder: (accountId: string, folderId: number) => sendRpc("emptyFolder", { accountId, folderId }), resetAll: () => sendRpc("resetStore"), // Event registration onEvent: (handler: (event: any) => void) => { eventHandlers.push(handler); }, }; } // ── Public entry point ──