@bobfrankston/mailx 1.0.237 → 1.0.239
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/android.html +2 -0
- package/client/compose/compose.html +3 -3
- package/client/compose/compose.js +77 -65
- package/package.json +3 -3
- package/packages/mailx-imap/index.js +24 -11
- package/packages/mailx-store-web/android-bootstrap.js +29 -1
- package/packages/mailx-store-web/imap-web-provider.d.ts +24 -0
- package/packages/mailx-store-web/imap-web-provider.js +103 -0
package/client/android.html
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
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
|
+
"@bobfrankston/iflow-direct": "../node_modules/@bobfrankston/iflow-direct/index.js",
|
|
19
|
+
"@bobfrankston/iflow-direct/": "../node_modules/@bobfrankston/iflow-direct/",
|
|
18
20
|
"sql.js": "../packages/mailx-store-web/sql-wasm-esm.js"
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
<body>
|
|
14
14
|
<div class="compose-header">
|
|
15
15
|
<div class="compose-field compose-from-field">
|
|
16
|
-
<label for="compose-from-
|
|
17
|
-
<
|
|
18
|
-
<
|
|
16
|
+
<label for="compose-from-input">From</label>
|
|
17
|
+
<input type="text" id="compose-from-input" list="compose-from-options" autocomplete="off" spellcheck="false">
|
|
18
|
+
<datalist id="compose-from-options"></datalist>
|
|
19
19
|
</div>
|
|
20
20
|
<div class="compose-field">
|
|
21
21
|
<label for="compose-to">To</label>
|
|
@@ -59,12 +59,16 @@ const container = document.getElementById("compose-editor");
|
|
|
59
59
|
container.classList.add(editorType === "tiptap" ? "editor-tiptap" : "editor-quill");
|
|
60
60
|
const editor = await createEditor(container, editorType);
|
|
61
61
|
// ── Populate from init data ──
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// From field is a free-text input with a <datalist> of known accounts. The
|
|
63
|
+
// user can pick a preset or type an arbitrary "Name <addr@domain>" — no
|
|
64
|
+
// separate "Other..." escape hatch, no hidden custom input toggle.
|
|
65
|
+
const fromInput = document.getElementById("compose-from-input");
|
|
66
|
+
const fromOptions = document.getElementById("compose-from-options");
|
|
64
67
|
const toInput = document.getElementById("compose-to");
|
|
65
68
|
const ccInput = document.getElementById("compose-cc");
|
|
66
69
|
const bccInput = document.getElementById("compose-bcc");
|
|
67
70
|
const subjectInput = document.getElementById("compose-subject");
|
|
71
|
+
let knownAccounts = [];
|
|
68
72
|
// ── AI ghost text autocomplete ──
|
|
69
73
|
if (appSettings?.autocomplete?.enabled && appSettings.autocomplete.provider !== "off") {
|
|
70
74
|
import("./ghost-text.js").then(({ initGhostText }) => {
|
|
@@ -74,53 +78,65 @@ if (appSettings?.autocomplete?.enabled && appSettings.autocomplete.provider !==
|
|
|
74
78
|
}, { debounceMs: appSettings.autocomplete.debounceMs || 600 });
|
|
75
79
|
}).catch(() => { });
|
|
76
80
|
}
|
|
77
|
-
/**
|
|
78
|
-
function
|
|
79
|
-
|
|
81
|
+
/** Format an account for the From field: "Name <email>". */
|
|
82
|
+
function formatAccountFrom(acct) {
|
|
83
|
+
return `${acct.name} <${acct.email}>`;
|
|
84
|
+
}
|
|
85
|
+
/** Populate the From <datalist> with one entry per known account and set
|
|
86
|
+
* the input's current value to the selected account (or the first/default
|
|
87
|
+
* account when no selection is given). */
|
|
88
|
+
function populateFromOptions(accounts, selectedId) {
|
|
89
|
+
knownAccounts = accounts;
|
|
90
|
+
fromOptions.innerHTML = "";
|
|
80
91
|
for (const acct of accounts) {
|
|
81
92
|
const opt = document.createElement("option");
|
|
82
|
-
opt.value = acct
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
opt.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
other.value = "__custom__";
|
|
96
|
-
other.textContent = "Other...";
|
|
97
|
-
fromSelect.appendChild(other);
|
|
98
|
-
}
|
|
99
|
-
fromSelect.addEventListener("change", () => {
|
|
100
|
-
if (fromSelect.value === "__custom__") {
|
|
101
|
-
fromCustom.hidden = false;
|
|
102
|
-
fromCustom.focus();
|
|
93
|
+
opt.value = formatAccountFrom(acct);
|
|
94
|
+
// datalist options can carry a label so the dropdown row shows the
|
|
95
|
+
// friendly account tag ("gmail", "bob.ma") next to the address.
|
|
96
|
+
const tag = acct.label || acct.name;
|
|
97
|
+
opt.label = tag;
|
|
98
|
+
fromOptions.appendChild(opt);
|
|
99
|
+
}
|
|
100
|
+
if (!fromInput.value) {
|
|
101
|
+
const selected = (selectedId && accounts.find(a => a.id === selectedId)) ||
|
|
102
|
+
accounts.find(a => a.defaultSend) ||
|
|
103
|
+
accounts[0];
|
|
104
|
+
if (selected)
|
|
105
|
+
fromInput.value = formatAccountFrom(selected);
|
|
103
106
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
}
|
|
108
|
+
/** Parse the current From input into { name, address } for header building. */
|
|
109
|
+
function parseFromInput() {
|
|
110
|
+
const raw = fromInput.value.trim();
|
|
111
|
+
const match = raw.match(/^(.+?)\s*<(.+?)>$/);
|
|
112
|
+
if (match)
|
|
113
|
+
return { name: match[1].trim(), address: match[2].trim() };
|
|
114
|
+
return { name: "", address: raw };
|
|
115
|
+
}
|
|
116
|
+
/** Match the From input's address against the known accounts table and
|
|
117
|
+
* return that account's id. Used by send() / saveDraft() to decide which
|
|
118
|
+
* account to send through. Falls back to defaultSend, then first account. */
|
|
110
119
|
function getFromAccountId() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
const { address } = parseFromInput();
|
|
121
|
+
const lower = address.toLowerCase();
|
|
122
|
+
// Exact match wins
|
|
123
|
+
const exact = knownAccounts.find(a => a.email.toLowerCase() === lower);
|
|
124
|
+
if (exact)
|
|
125
|
+
return exact.id;
|
|
126
|
+
// Same-domain match — handles +tag aliases and identity addresses
|
|
127
|
+
const domain = lower.split("@")[1] || "";
|
|
128
|
+
if (domain) {
|
|
129
|
+
const sameDomain = knownAccounts.find(a => a.email.toLowerCase().endsWith("@" + domain));
|
|
130
|
+
if (sameDomain)
|
|
131
|
+
return sameDomain.id;
|
|
132
|
+
}
|
|
133
|
+
// Give up — use default send account or the first account
|
|
134
|
+
const def = knownAccounts.find(a => a.defaultSend) || knownAccounts[0];
|
|
135
|
+
return def?.id || "";
|
|
117
136
|
}
|
|
118
|
-
/** Get the From
|
|
137
|
+
/** Get the raw From header string ("Name <addr>"). */
|
|
119
138
|
function getFromAddress() {
|
|
120
|
-
|
|
121
|
-
return fromCustom.value;
|
|
122
|
-
const opt = fromSelect.selectedOptions[0];
|
|
123
|
-
return opt ? `${opt.dataset.name} <${opt.dataset.email}>` : "";
|
|
139
|
+
return fromInput.value.trim();
|
|
124
140
|
}
|
|
125
141
|
/** Smart tab — skip to next empty field, ending at body */
|
|
126
142
|
function smartTab(current) {
|
|
@@ -245,32 +261,28 @@ function formatAddrs(addrs) {
|
|
|
245
261
|
function parseAddrs(s) {
|
|
246
262
|
if (!s.trim())
|
|
247
263
|
return [];
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
// Split on commas and drop empty segments. This handles trailing commas
|
|
265
|
+
// ("foo@x.com,") and stray whitespace ("foo@x.com, ,bar@y.com") without
|
|
266
|
+
// producing phantom empty addresses that fail validation on send.
|
|
267
|
+
return s.split(",")
|
|
268
|
+
.map(p => p.trim())
|
|
269
|
+
.filter(p => p.length > 0)
|
|
270
|
+
.map(part => {
|
|
271
|
+
const match = part.match(/^(.+?)\s*<(.+?)>$/);
|
|
250
272
|
if (match)
|
|
251
273
|
return { name: match[1].trim(), address: match[2].trim() };
|
|
252
|
-
return { name: "", address: part
|
|
274
|
+
return { name: "", address: part };
|
|
253
275
|
});
|
|
254
276
|
}
|
|
255
277
|
function applyInit(init) {
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
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);
|
|
278
|
+
// Populate the From datalist with known accounts
|
|
279
|
+
populateFromOptions(init.accounts, init.accountId);
|
|
280
|
+
// If the reply has a specific identity address (alias / +tag), set it
|
|
281
|
+
// as the From value directly — overrides the account default.
|
|
282
|
+
if (init.fromAddress) {
|
|
283
|
+
const account = init.accounts.find(a => a.id === init.accountId);
|
|
284
|
+
const displayName = account?.name || "";
|
|
285
|
+
fromInput.value = displayName ? `${displayName} <${init.fromAddress}>` : init.fromAddress;
|
|
274
286
|
}
|
|
275
287
|
toInput.value = formatAddrs(init.to);
|
|
276
288
|
ccInput.value = formatAddrs(init.cc);
|
|
@@ -390,7 +402,7 @@ function scheduleDraftSave() {
|
|
|
390
402
|
applyInit(init);
|
|
391
403
|
}
|
|
392
404
|
else {
|
|
393
|
-
|
|
405
|
+
populateFromOptions(accounts);
|
|
394
406
|
toInput.focus();
|
|
395
407
|
}
|
|
396
408
|
// Wire debounced saves to input events — checkpoint ~1.5s after the last
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.239",
|
|
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.22",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.301",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
79
79
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
80
80
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
81
|
-
"@bobfrankston/msger": "^0.1.
|
|
81
|
+
"@bobfrankston/msger": "^0.1.301",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -1221,18 +1221,27 @@ export class ImapManager extends EventEmitter {
|
|
|
1221
1221
|
/** Start periodic sync */
|
|
1222
1222
|
startPeriodicSync(intervalMinutes) {
|
|
1223
1223
|
this.stopPeriodicSync();
|
|
1224
|
-
// Per-account quick inbox check — adapts to server constraints
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
//
|
|
1224
|
+
// Per-account quick inbox check — adapts to server constraints.
|
|
1225
|
+
// Accounts with IDLE running get a long interval (5 min) because IDLE
|
|
1226
|
+
// already pushes instant notifications — the STATUS poll is just a
|
|
1227
|
+
// safety net. Non-IDLE accounts (rare) use a shorter interval.
|
|
1228
|
+
//
|
|
1229
|
+
// CRITICAL: the previous value (2500ms for everyone) was hammering
|
|
1230
|
+
// Dovecot with 24 logins per minute. That's what tripped the server
|
|
1231
|
+
// operator's fail2ban on mail1, and was still flooding the desktop
|
|
1232
|
+
// connection. Each STATUS poll creates a disposable connection
|
|
1233
|
+
// (TLS + auth + STATUS + close), not a lightweight keep-alive.
|
|
1228
1234
|
for (const [accountId] of this.configs) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1235
|
+
// Gmail uses API sync, not IMAP STATUS. IMAP accounts use IDLE
|
|
1236
|
+
// which gives instant push — the STATUS poll is just a fallback
|
|
1237
|
+
// in case IDLE silently dropped.
|
|
1238
|
+
const isGmail = this.isGmailAccount(accountId);
|
|
1239
|
+
const interval = isGmail ? 15000 : 300000; // Gmail API: 15s; IMAP with IDLE: 5min
|
|
1231
1240
|
const timer = setInterval(() => {
|
|
1232
1241
|
this.quickInboxCheckAccount(accountId).catch(() => { });
|
|
1233
1242
|
}, interval);
|
|
1234
1243
|
this.syncIntervals.set(`quick:${accountId}`, timer);
|
|
1235
|
-
console.log(` [periodic] ${accountId}: STATUS check every ${interval / 1000}s (${
|
|
1244
|
+
console.log(` [periodic] ${accountId}: STATUS check every ${interval / 1000}s (${isGmail ? "API" : "IMAP+IDLE"})`);
|
|
1236
1245
|
}
|
|
1237
1246
|
// Sync actions (sends + flags/deletes/moves) every 30 seconds — skip during active sync
|
|
1238
1247
|
const actionsInterval = setInterval(async () => {
|
|
@@ -2180,12 +2189,16 @@ export class ImapManager extends EventEmitter {
|
|
|
2180
2189
|
this.outboxBackoffDelay.delete(accountId);
|
|
2181
2190
|
}
|
|
2182
2191
|
catch (e) {
|
|
2183
|
-
// Stale-socket errors (Dovecot silently drops idle connections
|
|
2184
|
-
//
|
|
2185
|
-
//
|
|
2192
|
+
// Stale-socket errors (Dovecot silently drops idle connections,
|
|
2193
|
+
// or the sync path timed out and destroyed the socket): force a
|
|
2194
|
+
// fresh ops client so the next tick doesn't keep hitting the same
|
|
2195
|
+
// dead socket. Without reconnectOps, the dead client stays in the
|
|
2196
|
+
// opsClients map and every subsequent processOutbox call fails
|
|
2197
|
+
// immediately with "Not connected" — forever.
|
|
2186
2198
|
const msg = String(e?.message || e);
|
|
2187
2199
|
if (/Not connected|ECONNRESET|socket hang up|EPIPE|write after end/i.test(msg)) {
|
|
2188
|
-
|
|
2200
|
+
this.reconnectOps(accountId).catch(() => { });
|
|
2201
|
+
console.error(` [outbox] Stale connection for ${accountId}: ${msg} — reconnecting`);
|
|
2189
2202
|
}
|
|
2190
2203
|
else {
|
|
2191
2204
|
// Exponential backoff: 60s → 120s → 300s (max 5min)
|
|
@@ -16,6 +16,7 @@ import { WebMessageStore } from "./web-message-store.js";
|
|
|
16
16
|
import { WebMailxService } from "./web-service.js";
|
|
17
17
|
import { loadAccounts, loadAccountsFromCloud, saveAccounts, clearSettings, getDeviceId, setGDriveTokenProvider, setGDriveFolderId } from "./web-settings.js";
|
|
18
18
|
import { GmailApiWebProvider } from "./gmail-api-web.js";
|
|
19
|
+
import { ImapWebProvider } from "./imap-web-provider.js";
|
|
19
20
|
// ── State ──
|
|
20
21
|
let db;
|
|
21
22
|
let bodyStore;
|
|
@@ -70,8 +71,29 @@ class AndroidSyncManager {
|
|
|
70
71
|
console.warn(`[sync] ${account.id}: no token provider`);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
else if (account.imap?.host && account.imap?.user) {
|
|
75
|
+
// Generic IMAP account — use BridgeTransport through MAUI's TCP bridge
|
|
76
|
+
try {
|
|
77
|
+
const provider = new ImapWebProvider({
|
|
78
|
+
server: account.imap.host,
|
|
79
|
+
port: account.imap.port || 993,
|
|
80
|
+
username: account.imap.user,
|
|
81
|
+
password: account.imap.password,
|
|
82
|
+
inactivityTimeout: 300000, // 300s for slow Dovecot
|
|
83
|
+
fetchChunkSize: 10,
|
|
84
|
+
fetchChunkSizeMax: 100,
|
|
85
|
+
});
|
|
86
|
+
this.providers.set(account.id, provider);
|
|
87
|
+
vlog(`addAccount ${account.id}: IMAP provider registered (${account.imap.host}:${account.imap.port})`);
|
|
88
|
+
console.log(`[sync] ${account.id}: IMAP provider registered (${account.imap.host})`);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
vlog(`addAccount ${account.id}: IMAP provider FAILED: ${e.message}`);
|
|
92
|
+
console.error(`[sync] ${account.id}: IMAP provider failed: ${e.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
73
95
|
else {
|
|
74
|
-
vlog(`addAccount ${account.id}:
|
|
96
|
+
vlog(`addAccount ${account.id}: no imap config, skipping`);
|
|
75
97
|
}
|
|
76
98
|
}
|
|
77
99
|
setTokenProvider(accountId, provider) {
|
|
@@ -508,6 +530,12 @@ export async function initAndroid() {
|
|
|
508
530
|
// Wait for C# to inject the native bridge (TCP/FS/HTTP + OAuth)
|
|
509
531
|
await waitForNativeBridge();
|
|
510
532
|
console.log(`[android] Native bridge: ${window._nativeBridge ? "ready" : "timeout"}`);
|
|
533
|
+
// iflow-direct's BridgeTransport expects a global `msgapi` with a .tcp
|
|
534
|
+
// subobject. Our MAUI shell exposes the same API under `_nativeBridge`, so
|
|
535
|
+
// alias it. Must happen before any IMAP client is constructed.
|
|
536
|
+
if (window._nativeBridge && !window.msgapi) {
|
|
537
|
+
window.msgapi = window._nativeBridge;
|
|
538
|
+
}
|
|
511
539
|
db = new WebMailxDB("mailx");
|
|
512
540
|
await db.waitReady();
|
|
513
541
|
bodyStore = new WebMessageStore();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP web provider — implements MailProvider using CompatImapClient + BridgeTransport.
|
|
3
|
+
* Used for non-Gmail accounts on Android/WebView where Node.js isn't available.
|
|
4
|
+
*
|
|
5
|
+
* The native shell (MAUI) exposes TCP via window._nativeBridge.tcp.*; we alias it
|
|
6
|
+
* to window.msgapi in initAndroid() because iflow-direct's BridgeTransport expects
|
|
7
|
+
* that global name.
|
|
8
|
+
*/
|
|
9
|
+
import { type ImapClientConfig } from "@bobfrankston/iflow-direct";
|
|
10
|
+
import type { MailProvider, ProviderFolder, ProviderMessage, FetchOptions } from "./provider-types.js";
|
|
11
|
+
export declare class ImapWebProvider implements MailProvider {
|
|
12
|
+
private client;
|
|
13
|
+
private specialFolders;
|
|
14
|
+
private folderListCache;
|
|
15
|
+
constructor(config: ImapClientConfig);
|
|
16
|
+
listFolders(): Promise<ProviderFolder[]>;
|
|
17
|
+
fetchSince(folder: string, sinceUid: number, options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
18
|
+
fetchByDate(folder: string, since: Date, before: Date, options?: FetchOptions, onChunk?: (msgs: ProviderMessage[]) => void): Promise<ProviderMessage[]>;
|
|
19
|
+
fetchByUids(folder: string, uids: number[], options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
20
|
+
fetchOne(folder: string, uid: number, options?: FetchOptions): Promise<ProviderMessage | null>;
|
|
21
|
+
getUids(folder: string): Promise<number[]>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=imap-web-provider.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP web provider — implements MailProvider using CompatImapClient + BridgeTransport.
|
|
3
|
+
* Used for non-Gmail accounts on Android/WebView where Node.js isn't available.
|
|
4
|
+
*
|
|
5
|
+
* The native shell (MAUI) exposes TCP via window._nativeBridge.tcp.*; we alias it
|
|
6
|
+
* to window.msgapi in initAndroid() because iflow-direct's BridgeTransport expects
|
|
7
|
+
* that global name.
|
|
8
|
+
*/
|
|
9
|
+
import { CompatImapClient, BridgeTransport } from "@bobfrankston/iflow-direct";
|
|
10
|
+
/**
|
|
11
|
+
* Convert a NativeFolder (from iflow-direct) into a ProviderFolder,
|
|
12
|
+
* detecting special-use from the IMAP flags.
|
|
13
|
+
*/
|
|
14
|
+
function toProviderFolder(f, special) {
|
|
15
|
+
const flagsLower = (f.flags || []).map(x => x.toLowerCase());
|
|
16
|
+
let specialUse = "";
|
|
17
|
+
if (f.path === special.inbox || flagsLower.includes("\\inbox") || f.path.toUpperCase() === "INBOX")
|
|
18
|
+
specialUse = "inbox";
|
|
19
|
+
else if (f.path === special.sent || flagsLower.includes("\\sent"))
|
|
20
|
+
specialUse = "sent";
|
|
21
|
+
else if (f.path === special.trash || flagsLower.includes("\\trash"))
|
|
22
|
+
specialUse = "trash";
|
|
23
|
+
else if (f.path === special.drafts || flagsLower.includes("\\drafts"))
|
|
24
|
+
specialUse = "drafts";
|
|
25
|
+
else if (f.path === special.spam || f.path === special.junk || flagsLower.includes("\\junk"))
|
|
26
|
+
specialUse = "junk";
|
|
27
|
+
else if (f.path === special.archive || flagsLower.includes("\\archive"))
|
|
28
|
+
specialUse = "archive";
|
|
29
|
+
// Leaf name = last path segment after delimiter
|
|
30
|
+
const leaf = f.delimiter ? f.path.split(f.delimiter).pop() || f.path : f.path;
|
|
31
|
+
return {
|
|
32
|
+
path: f.path,
|
|
33
|
+
name: leaf,
|
|
34
|
+
delimiter: f.delimiter || "/",
|
|
35
|
+
specialUse,
|
|
36
|
+
flags: f.flags || [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function toProviderMessage(m) {
|
|
40
|
+
return {
|
|
41
|
+
uid: m.uid,
|
|
42
|
+
messageId: m.messageId || "",
|
|
43
|
+
providerId: "",
|
|
44
|
+
date: m.date || null,
|
|
45
|
+
subject: m.subject || "",
|
|
46
|
+
from: m.from || [],
|
|
47
|
+
to: m.to || [],
|
|
48
|
+
cc: m.cc || [],
|
|
49
|
+
seen: !!m.seen,
|
|
50
|
+
flagged: !!m.flagged,
|
|
51
|
+
answered: !!m.answered,
|
|
52
|
+
draft: !!m.draft,
|
|
53
|
+
size: m.size || 0,
|
|
54
|
+
source: m.source || "",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export class ImapWebProvider {
|
|
58
|
+
client;
|
|
59
|
+
specialFolders = {};
|
|
60
|
+
folderListCache = null;
|
|
61
|
+
constructor(config) {
|
|
62
|
+
const transportFactory = () => new BridgeTransport();
|
|
63
|
+
this.client = new CompatImapClient(config, transportFactory);
|
|
64
|
+
}
|
|
65
|
+
async listFolders() {
|
|
66
|
+
const native = await this.client.getFolderList();
|
|
67
|
+
const special = this.client.getSpecialFolders(native);
|
|
68
|
+
this.specialFolders = special;
|
|
69
|
+
const result = native.map(f => toProviderFolder(f, this.specialFolders));
|
|
70
|
+
this.folderListCache = result;
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
async fetchSince(folder, sinceUid, options) {
|
|
74
|
+
const msgs = await this.client.fetchMessagesSinceUid(folder, sinceUid, { source: !!options?.source });
|
|
75
|
+
return msgs.map(toProviderMessage);
|
|
76
|
+
}
|
|
77
|
+
async fetchByDate(folder, since, before, options, onChunk) {
|
|
78
|
+
const wrappedChunk = onChunk ? (raw) => onChunk(raw.map(toProviderMessage)) : undefined;
|
|
79
|
+
const msgs = await this.client.fetchMessageByDate(folder, since, before, { source: !!options?.source }, wrappedChunk);
|
|
80
|
+
return msgs.map(toProviderMessage);
|
|
81
|
+
}
|
|
82
|
+
async fetchByUids(folder, uids, options) {
|
|
83
|
+
if (!uids.length)
|
|
84
|
+
return [];
|
|
85
|
+
const range = uids.join(",");
|
|
86
|
+
const msgs = await this.client.fetchMessages(folder, range, { source: !!options?.source });
|
|
87
|
+
return msgs.map(toProviderMessage);
|
|
88
|
+
}
|
|
89
|
+
async fetchOne(folder, uid, options) {
|
|
90
|
+
const msg = await this.client.fetchMessageByUid(folder, uid, { source: !!options?.source });
|
|
91
|
+
return msg ? toProviderMessage(msg) : null;
|
|
92
|
+
}
|
|
93
|
+
async getUids(folder) {
|
|
94
|
+
return this.client.getUids(folder);
|
|
95
|
+
}
|
|
96
|
+
async close() {
|
|
97
|
+
try {
|
|
98
|
+
await this.client.logout();
|
|
99
|
+
}
|
|
100
|
+
catch { /* ignore */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=imap-web-provider.js.map
|