mail-viewer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process')
4
+ const electron = require('electron')
5
+ const path = require('path')
6
+
7
+ const proc = spawn(String(electron), [path.join(__dirname, '../out/main/index.js')], {
8
+ stdio: 'inherit',
9
+ windowsHide: false
10
+ })
11
+
12
+ proc.on('close', (code) => process.exit(code ?? 0))
@@ -0,0 +1,552 @@
1
+ "use strict";
2
+ const electron = require("electron");
3
+ const path = require("path");
4
+ const utils = require("@electron-toolkit/utils");
5
+ const Database = require("better-sqlite3");
6
+ const imapflow = require("imapflow");
7
+ const mailparser = require("mailparser");
8
+ const fs = require("fs");
9
+ const XLSX = require("xlsx");
10
+ function _interopNamespaceDefault(e) {
11
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
12
+ if (e) {
13
+ for (const k in e) {
14
+ if (k !== "default") {
15
+ const d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: () => e[k]
19
+ });
20
+ }
21
+ }
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+ const XLSX__namespace = /* @__PURE__ */ _interopNamespaceDefault(XLSX);
27
+ let db = null;
28
+ function getDb() {
29
+ if (!db) {
30
+ const dbPath = process.env.SQLITE_PATH ?? path.join(electron.app.getPath("userData"), "mail-viewer.db");
31
+ db = new Database(dbPath);
32
+ db.pragma("journal_mode = WAL");
33
+ db.pragma("foreign_keys = ON");
34
+ }
35
+ return db;
36
+ }
37
+ function closeDb() {
38
+ if (db) {
39
+ db.close();
40
+ db = null;
41
+ }
42
+ }
43
+ const SCHEMA_SQL = `
44
+ CREATE TABLE IF NOT EXISTS accounts (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ display_name TEXT NOT NULL,
47
+ email_address TEXT NOT NULL UNIQUE,
48
+ imap_host TEXT NOT NULL,
49
+ imap_port INTEGER NOT NULL DEFAULT 993,
50
+ imap_secure INTEGER NOT NULL DEFAULT 1,
51
+ username TEXT NOT NULL,
52
+ password_enc BLOB NOT NULL,
53
+ unseen_count INTEGER NOT NULL DEFAULT 0,
54
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
55
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
56
+ );
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email_address);
59
+ CREATE INDEX IF NOT EXISTS idx_accounts_unseen ON accounts(unseen_count DESC, display_name ASC);
60
+ `;
61
+ async function runMigrations() {
62
+ const db2 = getDb();
63
+ db2.exec(SCHEMA_SQL);
64
+ console.log("[migrations] Schema applied successfully");
65
+ }
66
+ function encryptPassword(plaintext) {
67
+ if (!electron.safeStorage.isEncryptionAvailable()) {
68
+ throw new Error("safeStorage encryption is not available on this system");
69
+ }
70
+ return electron.safeStorage.encryptString(plaintext);
71
+ }
72
+ function decryptPassword(encrypted) {
73
+ if (!electron.safeStorage.isEncryptionAvailable()) {
74
+ throw new Error("safeStorage encryption is not available on this system");
75
+ }
76
+ return electron.safeStorage.decryptString(encrypted);
77
+ }
78
+ const connections = /* @__PURE__ */ new Map();
79
+ function getAccountCredentials(accountId) {
80
+ const row = getDb().prepare("SELECT imap_host, imap_port, imap_secure, username, password_enc FROM accounts WHERE id = ?").get(accountId);
81
+ if (!row) throw new Error(`Account ${accountId} not found`);
82
+ return {
83
+ host: row.imap_host,
84
+ port: row.imap_port,
85
+ secure: Boolean(row.imap_secure),
86
+ auth: {
87
+ user: row.username,
88
+ pass: decryptPassword(row.password_enc)
89
+ },
90
+ logger: false
91
+ };
92
+ }
93
+ async function getConnection(accountId) {
94
+ let client = connections.get(accountId);
95
+ if (client && client.usable) {
96
+ return client;
97
+ }
98
+ if (client) {
99
+ try {
100
+ await client.logout();
101
+ } catch {
102
+ }
103
+ connections.delete(accountId);
104
+ }
105
+ const options = getAccountCredentials(accountId);
106
+ client = new imapflow.ImapFlow(options);
107
+ client.on("close", () => {
108
+ connections.delete(accountId);
109
+ });
110
+ client.on("error", (err) => {
111
+ console.error(`[IMAP] Account ${accountId} error:`, err.message);
112
+ connections.delete(accountId);
113
+ });
114
+ await client.connect();
115
+ connections.set(accountId, client);
116
+ return client;
117
+ }
118
+ async function closeConnection(accountId) {
119
+ const client = connections.get(accountId);
120
+ if (client) {
121
+ try {
122
+ await client.logout();
123
+ } catch {
124
+ }
125
+ connections.delete(accountId);
126
+ }
127
+ }
128
+ async function closeAllConnections() {
129
+ const ids = Array.from(connections.keys());
130
+ await Promise.allSettled(ids.map((id) => closeConnection(id)));
131
+ }
132
+ async function testConnection(options) {
133
+ const client = new imapflow.ImapFlow({ ...options, logger: false });
134
+ await client.connect();
135
+ await client.logout();
136
+ }
137
+ const UNSEEN_CONCURRENCY = 20;
138
+ async function runWithConcurrency(ids, concurrency, fn) {
139
+ let index = 0;
140
+ async function worker() {
141
+ while (index < ids.length) {
142
+ const id = ids[index++];
143
+ await fn(id);
144
+ }
145
+ }
146
+ await Promise.all(Array.from({ length: Math.min(concurrency, ids.length) }, worker));
147
+ }
148
+ function rowToAccount(row) {
149
+ return {
150
+ id: row.id,
151
+ displayName: row.display_name,
152
+ emailAddress: row.email_address,
153
+ imapHost: row.imap_host,
154
+ imapPort: row.imap_port,
155
+ imapSecure: Boolean(row.imap_secure),
156
+ username: row.username,
157
+ unseenCount: row.unseen_count ?? 0,
158
+ createdAt: row.created_at,
159
+ updatedAt: row.updated_at
160
+ };
161
+ }
162
+ function registerAccountHandlers() {
163
+ electron.ipcMain.handle("accounts:list", () => {
164
+ const db2 = getDb();
165
+ const rows = db2.prepare(
166
+ `SELECT id, display_name, email_address, imap_host, imap_port, imap_secure,
167
+ username, unseen_count, created_at, updated_at
168
+ FROM accounts ORDER BY unseen_count DESC, display_name ASC`
169
+ ).all();
170
+ return rows.map(rowToAccount);
171
+ });
172
+ electron.ipcMain.handle("accounts:create", (_event, input) => {
173
+ const db2 = getDb();
174
+ const passwordEnc = encryptPassword(input.password);
175
+ const row = db2.prepare(
176
+ `INSERT INTO accounts (display_name, email_address, imap_host, imap_port, imap_secure, username, password_enc)
177
+ VALUES (?, ?, ?, ?, ?, ?, ?)
178
+ RETURNING id, display_name, email_address, imap_host, imap_port, imap_secure,
179
+ username, unseen_count, created_at, updated_at`
180
+ ).get(
181
+ input.displayName,
182
+ input.emailAddress,
183
+ input.imapHost,
184
+ input.imapPort,
185
+ input.imapSecure ? 1 : 0,
186
+ input.username,
187
+ passwordEnc
188
+ );
189
+ return rowToAccount(row);
190
+ });
191
+ electron.ipcMain.handle(
192
+ "accounts:update",
193
+ async (_event, id, input) => {
194
+ const db2 = getDb();
195
+ const passwordEnc = encryptPassword(input.password);
196
+ const row = db2.prepare(
197
+ `UPDATE accounts
198
+ SET display_name = ?, email_address = ?, imap_host = ?, imap_port = ?,
199
+ imap_secure = ?, username = ?, password_enc = ?, updated_at = datetime('now')
200
+ WHERE id = ?
201
+ RETURNING id, display_name, email_address, imap_host, imap_port, imap_secure,
202
+ username, unseen_count, created_at, updated_at`
203
+ ).get(
204
+ input.displayName,
205
+ input.emailAddress,
206
+ input.imapHost,
207
+ input.imapPort,
208
+ input.imapSecure ? 1 : 0,
209
+ input.username,
210
+ passwordEnc,
211
+ id
212
+ );
213
+ if (!row) throw new Error(`Account ${id} not found`);
214
+ await closeConnection(id);
215
+ return rowToAccount(row);
216
+ }
217
+ );
218
+ electron.ipcMain.handle("accounts:delete", async (_event, id) => {
219
+ await closeConnection(id);
220
+ getDb().prepare("DELETE FROM accounts WHERE id = ?").run(id);
221
+ return { success: true };
222
+ });
223
+ electron.ipcMain.handle("accounts:deleteAll", async () => {
224
+ await closeAllConnections();
225
+ const result = getDb().prepare("DELETE FROM accounts").run();
226
+ return { deleted: result.changes };
227
+ });
228
+ electron.ipcMain.handle(
229
+ "accounts:testConnection",
230
+ async (_event, input) => {
231
+ try {
232
+ await testConnection({
233
+ host: input.imapHost,
234
+ port: input.imapPort,
235
+ secure: input.imapSecure,
236
+ auth: { user: input.username, pass: input.password }
237
+ });
238
+ return { ok: true };
239
+ } catch (err) {
240
+ return { ok: false, error: err.message };
241
+ }
242
+ }
243
+ );
244
+ electron.ipcMain.handle("accounts:fetchUnseenCounts", async () => {
245
+ const db2 = getDb();
246
+ const ids = db2.prepare("SELECT id FROM accounts").all().map((r) => r.id);
247
+ const updateStmt = db2.prepare("UPDATE accounts SET unseen_count = ? WHERE id = ?");
248
+ let updated = 0;
249
+ await runWithConcurrency(ids, UNSEEN_CONCURRENCY, async (id) => {
250
+ try {
251
+ const client = await getConnection(id);
252
+ const status = await client.status("INBOX", { unseen: true });
253
+ updateStmt.run(status.unseen ?? 0, id);
254
+ updated++;
255
+ } catch (err) {
256
+ console.error(`[unseen] account ${id}:`, err.message);
257
+ }
258
+ });
259
+ return { updated };
260
+ });
261
+ }
262
+ async function listFolders(accountId) {
263
+ const client = await getConnection(accountId);
264
+ const lock = await client.getMailboxLock("INBOX");
265
+ try {
266
+ const rawList = await client.list();
267
+ return buildTree(rawList);
268
+ } finally {
269
+ lock.release();
270
+ }
271
+ }
272
+ function buildTree(flat) {
273
+ const map = /* @__PURE__ */ new Map();
274
+ const roots = [];
275
+ const sorted = [...flat].sort((a, b) => a.path.localeCompare(b.path));
276
+ for (const box of sorted) {
277
+ const entry = {
278
+ path: box.path,
279
+ name: box.name,
280
+ delimiter: box.delimiter,
281
+ flags: Array.from(box.flags),
282
+ specialUse: box.specialUse,
283
+ listed: box.listed,
284
+ subscribed: box.subscribed,
285
+ children: []
286
+ };
287
+ map.set(box.path, entry);
288
+ if (!box.delimiter) {
289
+ roots.push(entry);
290
+ continue;
291
+ }
292
+ const lastDelim = box.path.lastIndexOf(box.delimiter);
293
+ if (lastDelim === -1) {
294
+ roots.push(entry);
295
+ } else {
296
+ const parentPath = box.path.substring(0, lastDelim);
297
+ const parent = map.get(parentPath);
298
+ if (parent) {
299
+ parent.children = parent.children ?? [];
300
+ parent.children.push(entry);
301
+ } else {
302
+ roots.push(entry);
303
+ }
304
+ }
305
+ }
306
+ return roots;
307
+ }
308
+ function registerFolderHandlers() {
309
+ electron.ipcMain.handle("folders:list", async (_event, accountId) => {
310
+ return listFolders(accountId);
311
+ });
312
+ }
313
+ const PAGE_SIZE = 50;
314
+ async function listMessages(accountId, folderPath, page) {
315
+ const client = await getConnection(accountId);
316
+ const lock = await client.getMailboxLock(folderPath);
317
+ try {
318
+ const mailbox = client.mailbox;
319
+ if (!mailbox || mailbox.exists === 0) return [];
320
+ const total = mailbox.exists;
321
+ const end = total - page * PAGE_SIZE;
322
+ if (end <= 0) return [];
323
+ const start = Math.max(1, end - PAGE_SIZE + 1);
324
+ const range = `${start}:${end}`;
325
+ const messages = [];
326
+ for await (const msg of client.fetch(range, {
327
+ uid: true,
328
+ flags: true,
329
+ envelope: true,
330
+ bodyStructure: true,
331
+ bodyParts: ["TEXT"]
332
+ })) {
333
+ const from = msg.envelope?.from?.[0];
334
+ const fromStr = from ? from.name ? `${from.name} <${from.address}>` : from.address ?? "" : "";
335
+ const flags = msg.flags ?? /* @__PURE__ */ new Set();
336
+ const seen = flags.has("\\Seen");
337
+ let hasAttachments = false;
338
+ if (msg.bodyStructure) {
339
+ hasAttachments = hasAttachmentParts(msg.bodyStructure);
340
+ }
341
+ const textPart = msg.bodyParts?.get("TEXT");
342
+ const preview = textPart ? textPart.toString("utf-8").replace(/\s+/g, " ").slice(0, 120) : "";
343
+ messages.push({
344
+ uid: msg.uid,
345
+ seq: msg.seq,
346
+ subject: msg.envelope?.subject ?? "(no subject)",
347
+ from: fromStr,
348
+ date: msg.envelope?.date?.toISOString() ?? "",
349
+ seen,
350
+ hasAttachments,
351
+ textPreview: preview
352
+ });
353
+ }
354
+ return messages.reverse();
355
+ } finally {
356
+ lock.release();
357
+ }
358
+ }
359
+ function hasAttachmentParts(structure) {
360
+ if (!structure) return false;
361
+ if (structure.disposition === "attachment") return true;
362
+ if (Array.isArray(structure.childNodes)) {
363
+ return structure.childNodes.some(hasAttachmentParts);
364
+ }
365
+ return false;
366
+ }
367
+ async function getMessage(accountId, folderPath, uid) {
368
+ const client = await getConnection(accountId);
369
+ const lock = await client.getMailboxLock(folderPath);
370
+ try {
371
+ let rawSource = null;
372
+ for await (const msg of client.fetch(
373
+ { uid },
374
+ { uid: true, source: true },
375
+ { uid: true }
376
+ )) {
377
+ rawSource = msg.source ?? null;
378
+ }
379
+ if (!rawSource) {
380
+ throw new Error(`Message UID ${uid} not found in ${folderPath}`);
381
+ }
382
+ const parsed = await mailparser.simpleParser(rawSource);
383
+ const fromAddr = parsed.from?.value?.[0];
384
+ const fromStr = fromAddr ? fromAddr.name ? `${fromAddr.name} <${fromAddr.address}>` : fromAddr.address ?? "" : "";
385
+ const toStr = (parsed.to ? Array.isArray(parsed.to) ? parsed.to.flatMap((a) => a.value).map((a) => a.name ? `${a.name} <${a.address}>` : a.address ?? "") : parsed.to.value.map((a) => a.name ? `${a.name} <${a.address}>` : a.address ?? "") : []).join(", ");
386
+ const ccStr = (parsed.cc ? Array.isArray(parsed.cc) ? parsed.cc.flatMap((a) => a.value).map((a) => a.name ? `${a.name} <${a.address}>` : a.address ?? "") : parsed.cc.value.map((a) => a.name ? `${a.name} <${a.address}>` : a.address ?? "") : []).join(", ");
387
+ const attachments = (parsed.attachments ?? []).map((att) => ({
388
+ filename: att.filename ?? "attachment",
389
+ contentType: att.contentType,
390
+ size: att.size ?? 0,
391
+ contentId: att.contentId ?? void 0
392
+ }));
393
+ return {
394
+ uid,
395
+ subject: parsed.subject ?? "(no subject)",
396
+ from: fromStr,
397
+ to: toStr,
398
+ cc: ccStr || void 0,
399
+ date: parsed.date?.toISOString() ?? "",
400
+ html: parsed.html || void 0,
401
+ text: parsed.text || void 0,
402
+ attachments
403
+ };
404
+ } finally {
405
+ lock.release();
406
+ }
407
+ }
408
+ function registerMessageHandlers() {
409
+ electron.ipcMain.handle(
410
+ "messages:list",
411
+ async (_event, accountId, folderPath, page) => {
412
+ return listMessages(accountId, folderPath, page);
413
+ }
414
+ );
415
+ electron.ipcMain.handle(
416
+ "messages:get",
417
+ async (_event, accountId, folderPath, uid) => {
418
+ return getMessage(accountId, folderPath, uid);
419
+ }
420
+ );
421
+ }
422
+ function registerImportHandlers() {
423
+ electron.ipcMain.handle("accounts:openXlsxFile", async () => {
424
+ const win = electron.BrowserWindow.getAllWindows()[0] ?? null;
425
+ const { canceled, filePaths } = await electron.dialog.showOpenDialog(win, {
426
+ title: "Select spreadsheet",
427
+ filters: [
428
+ { name: "Spreadsheets", extensions: ["xlsx", "xls", "csv"] },
429
+ { name: "All Files", extensions: ["*"] }
430
+ ],
431
+ properties: ["openFile"]
432
+ });
433
+ if (canceled || filePaths.length === 0) {
434
+ return { rows: [] };
435
+ }
436
+ const buffer = fs.readFileSync(filePaths[0]);
437
+ const workbook = XLSX__namespace.read(buffer, { type: "buffer" });
438
+ const sheetName = workbook.SheetNames[0];
439
+ const sheet = workbook.Sheets[sheetName];
440
+ const rows = XLSX__namespace.utils.sheet_to_json(sheet, {
441
+ header: 1,
442
+ defval: ""
443
+ });
444
+ return { rows };
445
+ });
446
+ electron.ipcMain.handle(
447
+ "accounts:importFromXlsx",
448
+ (_event, config) => {
449
+ const db2 = getDb();
450
+ const { rows, hasHeader, columns, imapSettings } = config;
451
+ const dataRows = hasHeader ? rows.slice(1) : rows;
452
+ const ready = [];
453
+ const errors = [];
454
+ for (let i = 0; i < dataRows.length; i++) {
455
+ const row = dataRows[i];
456
+ const rowNumber = hasHeader ? i + 2 : i + 1;
457
+ const username = (row[columns.username] ?? "").trim();
458
+ const password = (row[columns.password] ?? "").trim();
459
+ if (!username || !password) {
460
+ errors.push({ row: rowNumber, message: "Username or password is blank" });
461
+ continue;
462
+ }
463
+ const displayName = columns.displayName !== void 0 ? (row[columns.displayName] ?? "").trim() || username : username;
464
+ const emailAddress = columns.emailAddress !== void 0 ? (row[columns.emailAddress] ?? "").trim() || username : username;
465
+ ready.push({ rowNumber, displayName, emailAddress, username, passwordEnc: encryptPassword(password) });
466
+ }
467
+ const stmt = db2.prepare(
468
+ `INSERT INTO accounts (display_name, email_address, imap_host, imap_port, imap_secure, username, password_enc)
469
+ VALUES (?, ?, ?, ?, ?, ?, ?)
470
+ ON CONFLICT (email_address) DO NOTHING`
471
+ );
472
+ const imapSecureInt = imapSettings.imapSecure ? 1 : 0;
473
+ let imported = 0;
474
+ const runAll = db2.transaction((readyRows) => {
475
+ for (const r of readyRows) {
476
+ try {
477
+ const result = stmt.run(
478
+ r.displayName,
479
+ r.emailAddress,
480
+ imapSettings.imapHost,
481
+ imapSettings.imapPort,
482
+ imapSecureInt,
483
+ r.username,
484
+ r.passwordEnc
485
+ );
486
+ imported += result.changes;
487
+ } catch (err) {
488
+ errors.push({ row: r.rowNumber, message: err.message });
489
+ }
490
+ }
491
+ });
492
+ runAll(ready);
493
+ return { imported, errors };
494
+ }
495
+ );
496
+ }
497
+ function createWindow() {
498
+ const mainWindow = new electron.BrowserWindow({
499
+ width: 1280,
500
+ height: 800,
501
+ minWidth: 900,
502
+ minHeight: 600,
503
+ show: false,
504
+ autoHideMenuBar: true,
505
+ titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
506
+ webPreferences: {
507
+ preload: path.join(__dirname, "../preload/index.js"),
508
+ sandbox: false,
509
+ contextIsolation: true,
510
+ nodeIntegration: false
511
+ }
512
+ });
513
+ mainWindow.on("ready-to-show", () => {
514
+ mainWindow.show();
515
+ });
516
+ mainWindow.webContents.setWindowOpenHandler((details) => {
517
+ electron.shell.openExternal(details.url);
518
+ return { action: "deny" };
519
+ });
520
+ if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
521
+ mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
522
+ } else {
523
+ mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
524
+ }
525
+ return mainWindow;
526
+ }
527
+ electron.app.whenReady().then(async () => {
528
+ utils.electronApp.setAppUserModelId("com.mailviewer.app");
529
+ electron.app.on("browser-window-created", (_, window) => {
530
+ utils.optimizer.watchWindowShortcuts(window);
531
+ });
532
+ try {
533
+ await runMigrations();
534
+ } catch (err) {
535
+ console.error("[main] Failed to run migrations:", err);
536
+ }
537
+ registerAccountHandlers();
538
+ registerFolderHandlers();
539
+ registerMessageHandlers();
540
+ registerImportHandlers();
541
+ createWindow();
542
+ electron.app.on("activate", function() {
543
+ if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
544
+ });
545
+ });
546
+ electron.app.on("window-all-closed", async () => {
547
+ await closeAllConnections();
548
+ closeDb();
549
+ if (process.platform !== "darwin") {
550
+ electron.app.quit();
551
+ }
552
+ });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ const electron = require("electron");
3
+ const preload = require("@electron-toolkit/preload");
4
+ const api = {
5
+ accounts: {
6
+ list: () => electron.ipcRenderer.invoke("accounts:list"),
7
+ create: (input) => electron.ipcRenderer.invoke("accounts:create", input),
8
+ update: (id, input) => electron.ipcRenderer.invoke("accounts:update", id, input),
9
+ delete: (id) => electron.ipcRenderer.invoke("accounts:delete", id),
10
+ testConnection: (input) => electron.ipcRenderer.invoke("accounts:testConnection", input),
11
+ fetchUnseenCounts: () => electron.ipcRenderer.invoke("accounts:fetchUnseenCounts"),
12
+ deleteAll: () => electron.ipcRenderer.invoke("accounts:deleteAll")
13
+ },
14
+ folders: {
15
+ list: (accountId) => electron.ipcRenderer.invoke("folders:list", accountId)
16
+ },
17
+ messages: {
18
+ list: (accountId, folderPath, page) => electron.ipcRenderer.invoke("messages:list", accountId, folderPath, page),
19
+ get: (accountId, folderPath, uid) => electron.ipcRenderer.invoke("messages:get", accountId, folderPath, uid)
20
+ },
21
+ import: {
22
+ openXlsxFile: () => electron.ipcRenderer.invoke("accounts:openXlsxFile"),
23
+ importFromXlsx: (config) => electron.ipcRenderer.invoke("accounts:importFromXlsx", config)
24
+ }
25
+ };
26
+ if (process.contextIsolated) {
27
+ try {
28
+ electron.contextBridge.exposeInMainWorld("electron", preload.electronAPI);
29
+ electron.contextBridge.exposeInMainWorld("api", api);
30
+ } catch (error) {
31
+ console.error(error);
32
+ }
33
+ } else {
34
+ window.electron = preload.electronAPI;
35
+ window.api = api;
36
+ }