@bobfrankston/mailx 1.0.184 → 1.0.186
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.
- package/client/.msger-window.json +1 -1
- package/client/android.html +1 -1
- package/client/app.js +16 -15
- package/client/components/folder-tree.js +38 -2
- package/client/components/message-viewer.js +1 -6
- package/client/compose/compose.js +20 -15
- package/client/lib/api-client.js +5 -0
- package/package.json +2 -2
- package/packages/mailx-imap/index.js +19 -53
- package/packages/mailx-store-web/android-bootstrap.js +44 -2
- package/packages/mailx-store-web/sql-wasm-esm.js +10 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"height":1344,"width":
|
|
1
|
+
{"height":1344,"width":1586,"x":793,"y":81}
|
package/client/android.html
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@bobfrankston/mailx-store-web": "../packages/mailx-store-web/index.js",
|
|
16
16
|
"@bobfrankston/mailx-store-web/": "../packages/mailx-store-web/",
|
|
17
17
|
"@bobfrankston/mailx-types": "../packages/mailx-types/index.js",
|
|
18
|
-
"sql.js": "../
|
|
18
|
+
"sql.js": "../packages/mailx-store-web/sql-wasm-esm.js"
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
</script>
|
package/client/app.js
CHANGED
|
@@ -268,16 +268,15 @@ async function openCompose(mode) {
|
|
|
268
268
|
// Auto-detect reply From: if the message was delivered to an identity domain,
|
|
269
269
|
// reply from that address instead of the default account address.
|
|
270
270
|
// Identity domains configured per-account in accounts.jsonc (identityDomains array).
|
|
271
|
-
|
|
272
|
-
const identityDomains = [
|
|
271
|
+
const account = accounts.find((a) => a.id === accountId);
|
|
272
|
+
const identityDomains = account?.identityDomains || [];
|
|
273
273
|
function detectReplyFrom() {
|
|
274
|
-
if (!msg)
|
|
274
|
+
if (!msg || identityDomains.length === 0)
|
|
275
275
|
return undefined;
|
|
276
|
-
// Check deliveredTo first (most reliable), then To addresses
|
|
277
276
|
const candidates = [msg.deliveredTo, ...(msg.to || []).map((a) => a.address)].filter(Boolean);
|
|
278
277
|
for (const addr of candidates) {
|
|
279
278
|
const domain = addr.split("@")[1]?.toLowerCase();
|
|
280
|
-
if (domain && identityDomains.some(d => domain === d
|
|
279
|
+
if (domain && identityDomains.some((d) => domain === d.toLowerCase())) {
|
|
281
280
|
return addr;
|
|
282
281
|
}
|
|
283
282
|
}
|
|
@@ -306,14 +305,10 @@ async function openCompose(mode) {
|
|
|
306
305
|
}
|
|
307
306
|
// Store init data for compose window to pick up
|
|
308
307
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
|
|
316
|
-
}
|
|
308
|
+
// Open compose — always as popup via window.open().
|
|
309
|
+
// In IPC mode, msger's new_window_req_handler allows msger.localhost popups.
|
|
310
|
+
// DO NOT use window.location.href — msger exits on navigation.
|
|
311
|
+
window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
|
|
317
312
|
}
|
|
318
313
|
function quoteBody(msg) {
|
|
319
314
|
const date = new Date(msg.date).toLocaleString();
|
|
@@ -585,10 +580,16 @@ onWsEvent((event) => {
|
|
|
585
580
|
break;
|
|
586
581
|
}
|
|
587
582
|
case "folderCountsChanged": {
|
|
588
|
-
// Update folder badges
|
|
589
|
-
// The list refreshes when the user clicks a folder or presses Sync.
|
|
583
|
+
// Update folder badges + silently refresh message list (preserves selection and viewer)
|
|
590
584
|
updateFolderCounts();
|
|
591
585
|
updateNewMessageCount();
|
|
586
|
+
// Debounced silent reload — preserves scroll position, selection, and viewer
|
|
587
|
+
if (reloadDebounceTimer)
|
|
588
|
+
clearTimeout(reloadDebounceTimer);
|
|
589
|
+
reloadDebounceTimer = setTimeout(() => {
|
|
590
|
+
reloadDebounceTimer = null;
|
|
591
|
+
reloadCurrentFolder();
|
|
592
|
+
}, 2000);
|
|
592
593
|
// Sync finished — re-enable sync button
|
|
593
594
|
const syncBtn = document.getElementById("btn-sync");
|
|
594
595
|
if (syncBtn) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Folder tree component -- renders account folders with hierarchy,
|
|
3
3
|
* expand/collapse, and optional unified inbox.
|
|
4
4
|
*/
|
|
5
|
-
import { getAccounts, getFolders, moveMessage, moveMessages, markFolderRead, createFolder, renameFolder, deleteFolder, emptyFolder, setupAccount, getVersion } from "../lib/api-client.js";
|
|
5
|
+
import { getAccounts, getFolders, moveMessage, moveMessages, markFolderRead, createFolder, renameFolder, deleteFolder, emptyFolder, setupAccount, getDeviceAccounts, getVersion } from "../lib/api-client.js";
|
|
6
6
|
import { showContextMenu } from "./context-menu.js";
|
|
7
7
|
let onFolderSelect;
|
|
8
8
|
let onUnifiedInbox = null;
|
|
@@ -357,8 +357,9 @@ async function loadFolderTree(container) {
|
|
|
357
357
|
if (mainBody) {
|
|
358
358
|
mainBody.innerHTML = `<div style="padding:2rem;line-height:1.8;max-width:500px">
|
|
359
359
|
<h2 style="margin-bottom:1rem">Welcome to mailx</h2>
|
|
360
|
+
<div id="setup-device-accounts"></div>
|
|
360
361
|
<div id="setup-cloud-status"></div>
|
|
361
|
-
<p>Add your email account to get started.</p>
|
|
362
|
+
<p id="setup-form-intro">Add your email account to get started.</p>
|
|
362
363
|
<form id="setup-form" style="margin-top:1rem">
|
|
363
364
|
<label style="display:block;margin-bottom:0.5rem">
|
|
364
365
|
Your name
|
|
@@ -462,6 +463,41 @@ async function loadFolderTree(container) {
|
|
|
462
463
|
</div>`;
|
|
463
464
|
}
|
|
464
465
|
}).catch(() => { });
|
|
466
|
+
// On Android, check for device Google accounts and show picker
|
|
467
|
+
getDeviceAccounts().then(async (deviceAccounts) => {
|
|
468
|
+
const pickerEl = document.getElementById("setup-device-accounts");
|
|
469
|
+
if (!pickerEl || deviceAccounts.length === 0)
|
|
470
|
+
return;
|
|
471
|
+
const formEl = document.getElementById("setup-form");
|
|
472
|
+
const introEl = document.getElementById("setup-form-intro");
|
|
473
|
+
if (introEl)
|
|
474
|
+
introEl.textContent = "Select an account:";
|
|
475
|
+
if (formEl)
|
|
476
|
+
formEl.style.display = "none";
|
|
477
|
+
pickerEl.innerHTML = deviceAccounts.map((a) => `<button class="device-account-btn" data-email="${a.email}" data-name="${a.name}" style="display:block;width:100%;padding:0.75rem 1rem;margin-bottom:0.5rem;background:var(--color-bg-surface);color:var(--color-text);border:1px solid var(--color-border);border-radius:6px;cursor:pointer;font-size:1rem;text-align:left">${a.email}</button>`).join("") + `<button id="setup-show-form" style="margin-top:0.5rem;padding:0.5rem 1rem;background:none;color:var(--color-text-muted);border:none;cursor:pointer;font-size:0.9rem">Use a different account...</button>`;
|
|
478
|
+
pickerEl.querySelectorAll(".device-account-btn").forEach((btn) => {
|
|
479
|
+
btn.addEventListener("click", async () => {
|
|
480
|
+
const email = btn.dataset.email || "";
|
|
481
|
+
const name = btn.dataset.name || "";
|
|
482
|
+
pickerEl.innerHTML = `<div style="padding:0.5rem;color:var(--color-text-muted)">Setting up ${email}...</div>`;
|
|
483
|
+
const result = await setupAccount(name, email, "");
|
|
484
|
+
if (result?.ok) {
|
|
485
|
+
pickerEl.innerHTML = `<div style="padding:0.5rem;color:var(--color-accent)">${result.message || "Account added!"}</div>`;
|
|
486
|
+
setTimeout(() => location.reload(), 2000);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
pickerEl.innerHTML = `<div style="padding:0.5rem;color:#f55">${result?.error || "Setup failed"}</div>`;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
document.getElementById("setup-show-form")?.addEventListener("click", () => {
|
|
494
|
+
pickerEl.style.display = "none";
|
|
495
|
+
if (formEl)
|
|
496
|
+
formEl.style.display = "block";
|
|
497
|
+
if (introEl)
|
|
498
|
+
introEl.textContent = "Add your email account to get started.";
|
|
499
|
+
});
|
|
500
|
+
}).catch(() => { });
|
|
465
501
|
}
|
|
466
502
|
// Dismiss startup overlay
|
|
467
503
|
const overlay = document.getElementById("startup-overlay");
|
|
@@ -144,12 +144,7 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
144
144
|
draftFolderId: msg.folderId,
|
|
145
145
|
};
|
|
146
146
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
147
|
-
|
|
148
|
-
window.location.href = "compose/compose.html";
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
|
|
152
|
-
}
|
|
147
|
+
window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
|
|
153
148
|
};
|
|
154
149
|
}
|
|
155
150
|
else {
|
|
@@ -5,14 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createEditor } from "./editor.js";
|
|
7
7
|
import { getVersion, getSettings, getAccounts, searchContacts, sendMessage, saveDraft as apiSaveDraft, deleteDraft } from "../lib/api-client.js";
|
|
8
|
-
/** Close compose
|
|
8
|
+
/** Close compose window */
|
|
9
9
|
function closeCompose() {
|
|
10
|
-
|
|
11
|
-
window.location.href = "../index.html";
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
closeCompose();
|
|
15
|
-
}
|
|
10
|
+
window.close();
|
|
16
11
|
}
|
|
17
12
|
// ── Load editor scripts dynamically ──
|
|
18
13
|
function loadScript(src) {
|
|
@@ -258,14 +253,24 @@ function parseAddrs(s) {
|
|
|
258
253
|
});
|
|
259
254
|
}
|
|
260
255
|
function applyInit(init) {
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
// If identity domain matched, add as first option in dropdown
|
|
257
|
+
const replyAddr = init.fromAddress;
|
|
258
|
+
const account = init.accounts.find(a => a.id === init.accountId);
|
|
259
|
+
const displayName = account?.name || "";
|
|
260
|
+
if (replyAddr) {
|
|
261
|
+
// Insert identity address as first option, selected
|
|
262
|
+
const idOpt = document.createElement("option");
|
|
263
|
+
idOpt.value = init.accountId;
|
|
264
|
+
idOpt.textContent = `${displayName} <${replyAddr}>`;
|
|
265
|
+
idOpt.dataset.email = replyAddr;
|
|
266
|
+
idOpt.dataset.name = displayName;
|
|
267
|
+
idOpt.selected = true;
|
|
268
|
+
// Populate rest of dropdown, then prepend identity option
|
|
269
|
+
populateFromSelect(init.accounts, "");
|
|
270
|
+
fromSelect.prepend(idOpt);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
populateFromSelect(init.accounts, init.accountId);
|
|
269
274
|
}
|
|
270
275
|
toInput.value = formatAddrs(init.to);
|
|
271
276
|
ccInput.value = formatAddrs(init.cc);
|
package/client/lib/api-client.js
CHANGED
|
@@ -309,6 +309,11 @@ export function setupAccount(name, email, password) {
|
|
|
309
309
|
return getIpc().setupAccount?.(name, email, password);
|
|
310
310
|
return api("/setup", { method: "POST", body: JSON.stringify({ name, email, password }) });
|
|
311
311
|
}
|
|
312
|
+
export async function getDeviceAccounts() {
|
|
313
|
+
if (hasIPC())
|
|
314
|
+
return getIpc().getDeviceAccounts?.() ?? [];
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
312
317
|
// Legacy exports for backward compatibility
|
|
313
318
|
export const connectWebSocket = connectEvents;
|
|
314
319
|
export const onWsEvent = onEvent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.186",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.21",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.236",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -12,8 +12,10 @@ import * as fs from "node:fs";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { simpleParser } from "mailparser";
|
|
14
14
|
import { GmailApiProvider } from "./providers/gmail-api.js";
|
|
15
|
-
import { createTransport } from "nodemailer";
|
|
16
15
|
import * as os from "node:os";
|
|
16
|
+
// Well-known ports — no magic numbers
|
|
17
|
+
const SMTP_PORT_STARTTLS = 587;
|
|
18
|
+
const SMTP_PORT_IMPLICIT_TLS = 465;
|
|
17
19
|
/** Extract full error detail with provenance */
|
|
18
20
|
function imapError(err) {
|
|
19
21
|
const msg = err.message || err.reason || err.code || (typeof err === "string" ? err : "");
|
|
@@ -1734,21 +1736,27 @@ export class ImapManager extends EventEmitter {
|
|
|
1734
1736
|
const account = settings.accounts.find(a => a.id === accountId);
|
|
1735
1737
|
if (!account?.smtp)
|
|
1736
1738
|
throw new Error(`No SMTP config for ${accountId}`);
|
|
1739
|
+
// SMTP auth: use explicit SMTP credentials, fall back to IMAP credentials
|
|
1737
1740
|
let smtpAuth;
|
|
1738
|
-
|
|
1739
|
-
|
|
1741
|
+
const smtpAuthType = account.smtp.auth || (account.imap?.password ? "password" : undefined);
|
|
1742
|
+
if (smtpAuthType === "password") {
|
|
1743
|
+
smtpAuth = {
|
|
1744
|
+
user: account.smtp.user || account.imap?.user || account.email,
|
|
1745
|
+
pass: account.smtp.password || account.imap?.password,
|
|
1746
|
+
};
|
|
1740
1747
|
}
|
|
1741
|
-
else if (
|
|
1748
|
+
else if (smtpAuthType === "oauth2") {
|
|
1742
1749
|
const accessToken = await this.getOAuthToken(accountId);
|
|
1743
1750
|
if (!accessToken)
|
|
1744
1751
|
throw new Error("OAuth token not available");
|
|
1745
|
-
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
1752
|
+
smtpAuth = { type: "OAuth2", user: account.smtp.user || account.imap?.user || account.email, accessToken };
|
|
1746
1753
|
}
|
|
1747
1754
|
const { createTransport } = await import("nodemailer");
|
|
1755
|
+
const smtpPort = account.smtp.port || SMTP_PORT_STARTTLS;
|
|
1748
1756
|
const transport = createTransport({
|
|
1749
|
-
host: account.smtp.host,
|
|
1750
|
-
port:
|
|
1751
|
-
secure:
|
|
1757
|
+
host: account.smtp.host || account.imap?.host,
|
|
1758
|
+
port: smtpPort,
|
|
1759
|
+
secure: smtpPort === SMTP_PORT_IMPLICIT_TLS, // 465 = implicit TLS, 587 = STARTTLS
|
|
1752
1760
|
auth: smtpAuth,
|
|
1753
1761
|
tls: { rejectUnauthorized: false },
|
|
1754
1762
|
});
|
|
@@ -1824,52 +1832,10 @@ export class ImapManager extends EventEmitter {
|
|
|
1824
1832
|
await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
1825
1833
|
continue;
|
|
1826
1834
|
}
|
|
1827
|
-
// Send via SMTP
|
|
1835
|
+
// Send via shared SMTP method
|
|
1828
1836
|
try {
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
|
|
1832
|
-
}
|
|
1833
|
-
else if (account.smtp.auth === "oauth2") {
|
|
1834
|
-
const accessToken = await this.getOAuthToken(accountId);
|
|
1835
|
-
if (!accessToken)
|
|
1836
|
-
throw new Error("OAuth token not available — re-authenticate");
|
|
1837
|
-
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
1838
|
-
}
|
|
1839
|
-
const transport = createTransport({
|
|
1840
|
-
host: account.smtp.host,
|
|
1841
|
-
port: account.smtp.port,
|
|
1842
|
-
secure: account.smtp.port === 465,
|
|
1843
|
-
auth: smtpAuth,
|
|
1844
|
-
tls: { rejectUnauthorized: false },
|
|
1845
|
-
});
|
|
1846
|
-
// Parse recipients from raw message headers for SMTP envelope
|
|
1847
|
-
const toMatch = msg.source.match(/^To:\s*(.+)$/mi);
|
|
1848
|
-
const ccMatch = msg.source.match(/^Cc:\s*(.+)$/mi);
|
|
1849
|
-
const bccMatch = msg.source.match(/^Bcc:\s*(.+)$/mi);
|
|
1850
|
-
const fromMatch = msg.source.match(/^From:\s*(.+)$/mi);
|
|
1851
|
-
const parseAddrs = (s) => s.match(/[\w.+-]+@[\w.-]+/g) || [];
|
|
1852
|
-
const recipients = [
|
|
1853
|
-
...(toMatch ? parseAddrs(toMatch[1]) : []),
|
|
1854
|
-
...(ccMatch ? parseAddrs(ccMatch[1]) : []),
|
|
1855
|
-
...(bccMatch ? parseAddrs(bccMatch[1]) : []),
|
|
1856
|
-
];
|
|
1857
|
-
const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
|
|
1858
|
-
if (recipients.length === 0) {
|
|
1859
|
-
console.error(` [outbox] No recipients in UID ${uid} — permanent failure`);
|
|
1860
|
-
await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
1861
|
-
await client.addFlags(outboxFolder.path, uid, ["$PermanentFailure"]);
|
|
1862
|
-
continue;
|
|
1863
|
-
}
|
|
1864
|
-
// Strip Bcc header from raw message before sending
|
|
1865
|
-
const rawToSend = msg.source.replace(/^Bcc:.*\r?\n/mi, "");
|
|
1866
|
-
// Save debug copy before sending
|
|
1867
|
-
this.saveSendingCopy(accountId, rawToSend, `sent-${uid}`);
|
|
1868
|
-
await transport.sendMail({
|
|
1869
|
-
raw: rawToSend,
|
|
1870
|
-
envelope: { from: sender, to: recipients },
|
|
1871
|
-
});
|
|
1872
|
-
console.log(` [outbox] Sent UID ${uid} → ${recipients.join(", ")}`);
|
|
1837
|
+
await this.sendRawViaSMTP(accountId, msg.source);
|
|
1838
|
+
console.log(` [outbox] Sent UID ${uid}`);
|
|
1873
1839
|
// Delete from Outbox FIRST to prevent double-send if move-to-Sent fails.
|
|
1874
1840
|
// The message is already sent via SMTP — worst case we lose the Sent copy,
|
|
1875
1841
|
// which is better than sending the message twice.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { WebMailxDB } from "./db.js";
|
|
15
15
|
import { WebMessageStore } from "./web-message-store.js";
|
|
16
16
|
import { WebMailxService } from "./web-service.js";
|
|
17
|
-
import { loadAccounts, clearSettings } from "./web-settings.js";
|
|
17
|
+
import { loadAccounts, saveAccounts, clearSettings } from "./web-settings.js";
|
|
18
18
|
import { GmailApiWebProvider } from "./gmail-api-web.js";
|
|
19
19
|
// ── State ──
|
|
20
20
|
let db;
|
|
@@ -327,7 +327,49 @@ function installBridge() {
|
|
|
327
327
|
},
|
|
328
328
|
getAutocompleteSettings: () => service.getAutocompleteSettings(),
|
|
329
329
|
saveAutocompleteSettings: async (settings) => { await service.saveAutocompleteSettings(settings); return { ok: true }; },
|
|
330
|
-
|
|
330
|
+
getDeviceAccounts: async () => {
|
|
331
|
+
const bridge = window._nativeBridge;
|
|
332
|
+
if (bridge?.app?.getDeviceAccounts) {
|
|
333
|
+
return bridge.app.getDeviceAccounts();
|
|
334
|
+
}
|
|
335
|
+
return [];
|
|
336
|
+
},
|
|
337
|
+
setupAccount: async (name, email, _password) => {
|
|
338
|
+
try {
|
|
339
|
+
if (!email || !email.includes("@")) {
|
|
340
|
+
return { ok: false, error: "Email address required" };
|
|
341
|
+
}
|
|
342
|
+
const domain = email.split("@")[1].toLowerCase();
|
|
343
|
+
const id = domain.split(".")[0] || "account";
|
|
344
|
+
const account = {
|
|
345
|
+
id,
|
|
346
|
+
name: name || email.split("@")[0],
|
|
347
|
+
email,
|
|
348
|
+
enabled: true,
|
|
349
|
+
imap: { host: `imap.${domain}`, port: 993, tls: true, auth: "oauth2", user: email },
|
|
350
|
+
smtp: { host: `smtp.${domain}`, port: 587, tls: true, auth: "oauth2", user: email },
|
|
351
|
+
};
|
|
352
|
+
// Apply known provider defaults
|
|
353
|
+
if (domain === "gmail.com" || domain === "googlemail.com") {
|
|
354
|
+
account.label = "Gmail";
|
|
355
|
+
account.imap = { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2", user: email };
|
|
356
|
+
account.smtp = { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2", user: email };
|
|
357
|
+
}
|
|
358
|
+
const existing = await loadAccounts();
|
|
359
|
+
if (existing.some(a => a.email === email)) {
|
|
360
|
+
return { ok: true, message: "Account already exists" };
|
|
361
|
+
}
|
|
362
|
+
existing.push(account);
|
|
363
|
+
await saveAccounts(existing);
|
|
364
|
+
await syncManager.addAccount(account);
|
|
365
|
+
db.upsertAccount(account.id, account.name, account.email, JSON.stringify(account));
|
|
366
|
+
console.log(`[android] Account added: ${email}`);
|
|
367
|
+
return { ok: true, message: `Added ${email}. Syncing...` };
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
return { ok: false, error: e.message };
|
|
371
|
+
}
|
|
372
|
+
},
|
|
331
373
|
repairAccounts: async () => ({ ok: false, error: "Use desktop for repair" }),
|
|
332
374
|
resetStore: () => resetStore(),
|
|
333
375
|
restart: () => { location.reload(); },
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// ESM wrapper for sql.js (UMD) — loads as classic script, re-exports the global
|
|
2
|
+
await new Promise((resolve, reject) => {
|
|
3
|
+
const s = document.createElement("script");
|
|
4
|
+
s.src = new URL("../../node_modules/sql.js/dist/sql-wasm.js", import.meta.url).href;
|
|
5
|
+
s.onload = resolve;
|
|
6
|
+
s.onerror = reject;
|
|
7
|
+
document.head.appendChild(s);
|
|
8
|
+
});
|
|
9
|
+
const initSqlJs = globalThis.initSqlJs;
|
|
10
|
+
export default initSqlJs;
|