@bobfrankston/mailx 1.0.256 → 1.0.260
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/bin/mailx.js +112 -0
- package/client/.msger-window.json +1 -1
- package/client/components/message-viewer.js +82 -7
- package/package.json +7 -6
- package/packages/mailx-imap/index.d.ts +6 -0
- package/packages/mailx-imap/index.js +100 -33
- package/packages/mailx-imap/package.json +2 -1
- package/packages/mailx-imap/providers/gmail-api.d.ts +5 -40
- package/packages/mailx-imap/providers/gmail-api.js +5 -336
- package/packages/mailx-imap/providers/types.d.ts +6 -59
- package/packages/mailx-imap/providers/types.js +5 -2
- package/packages/mailx-service/index.js +16 -2
- package/packages/mailx-store-web/android-bootstrap.js +8 -6
- package/packages/mailx-store-web/gmail-api-web.d.ts +7 -37
- package/packages/mailx-store-web/gmail-api-web.js +7 -298
- package/packages/mailx-store-web/imap-web-provider.d.ts +1 -1
- package/packages/mailx-store-web/imap-web-provider.js +2 -2
- package/packages/mailx-store-web/main-thread-host.d.ts +15 -0
- package/packages/mailx-store-web/main-thread-host.js +287 -0
- package/packages/mailx-store-web/package.json +2 -1
- package/packages/mailx-store-web/provider-types.d.ts +4 -47
- package/packages/mailx-store-web/provider-types.js +3 -3
- package/packages/mailx-store-web/sync-manager.d.ts +61 -0
- package/packages/mailx-store-web/sync-manager.js +422 -0
- package/packages/mailx-store-web/worker-entry.d.ts +8 -0
- package/packages/mailx-store-web/worker-entry.js +187 -0
- package/packages/mailx-store-web/worker-tcp-transport.d.ts +28 -0
- package/packages/mailx-store-web/worker-tcp-transport.js +98 -0
|
@@ -1,339 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Back-compat re-export. The canonical Gmail provider lives in
|
|
3
|
+
* @bobfrankston/mailx-sync. mailx-imap re-exports it under its old name so
|
|
4
|
+
* call sites here keep compiling. Both desktop (this package) and Android
|
|
5
|
+
* (mailx-store-web) consume the same single implementation now.
|
|
4
6
|
*/
|
|
5
|
-
|
|
6
|
-
/** Convert Gmail hex ID to integer UID (lower 48 bits — deterministic, stable) */
|
|
7
|
-
function idToUid(id) {
|
|
8
|
-
const hex = id.length > 12 ? id.slice(-12) : id;
|
|
9
|
-
return parseInt(hex, 16);
|
|
10
|
-
}
|
|
11
|
-
/** Map Gmail label to IMAP-style specialUse */
|
|
12
|
-
function labelSpecialUse(label) {
|
|
13
|
-
switch (label.id) {
|
|
14
|
-
case "INBOX": return "inbox";
|
|
15
|
-
case "SENT": return "sent";
|
|
16
|
-
case "DRAFT": return "drafts";
|
|
17
|
-
case "TRASH": return "trash";
|
|
18
|
-
case "SPAM": return "junk";
|
|
19
|
-
case "STARRED": return "";
|
|
20
|
-
case "IMPORTANT": return "";
|
|
21
|
-
default: return "";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/** Parse RFC 2822 headers from Gmail metadata payload */
|
|
25
|
-
function getHeader(headers, name) {
|
|
26
|
-
return headers.find(h => h.name.toLowerCase() === name.toLowerCase())?.value || "";
|
|
27
|
-
}
|
|
28
|
-
/** Parse "Name <addr>" or "addr" into { name, address } */
|
|
29
|
-
function parseAddress(raw) {
|
|
30
|
-
const match = raw.match(/^"?([^"<]*?)"?\s*<([^>]+)>/);
|
|
31
|
-
if (match)
|
|
32
|
-
return { name: match[1].trim(), address: match[2].trim() };
|
|
33
|
-
return { address: raw.trim() };
|
|
34
|
-
}
|
|
35
|
-
function parseAddressList(raw) {
|
|
36
|
-
if (!raw)
|
|
37
|
-
return [];
|
|
38
|
-
// Split on commas that aren't inside quotes
|
|
39
|
-
return raw.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/).map(s => parseAddress(s.trim())).filter(a => a.address);
|
|
40
|
-
}
|
|
41
|
-
export class GmailApiProvider {
|
|
42
|
-
tokenProvider;
|
|
43
|
-
constructor(tokenProvider) {
|
|
44
|
-
this.tokenProvider = tokenProvider;
|
|
45
|
-
}
|
|
46
|
-
async fetch(path, options = {}) {
|
|
47
|
-
const token = await this.tokenProvider();
|
|
48
|
-
const maxAttempts = 6;
|
|
49
|
-
const baseDelayMs = 1000;
|
|
50
|
-
const maxDelayMs = 60_000;
|
|
51
|
-
let lastStatus = 0;
|
|
52
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
53
|
-
const res = await globalThis.fetch(`${API}${path}`, {
|
|
54
|
-
...options,
|
|
55
|
-
headers: {
|
|
56
|
-
"Authorization": `Bearer ${token}`,
|
|
57
|
-
"Content-Type": "application/json",
|
|
58
|
-
...options.headers,
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
if (res.status === 429 || res.status >= 500) {
|
|
62
|
-
lastStatus = res.status;
|
|
63
|
-
// Honor Retry-After if present (seconds or HTTP-date)
|
|
64
|
-
const retryAfter = res.headers.get("Retry-After");
|
|
65
|
-
let delay = baseDelayMs * Math.pow(2, attempt);
|
|
66
|
-
if (retryAfter) {
|
|
67
|
-
const asInt = parseInt(retryAfter, 10);
|
|
68
|
-
if (!isNaN(asInt))
|
|
69
|
-
delay = asInt * 1000;
|
|
70
|
-
else {
|
|
71
|
-
const when = Date.parse(retryAfter);
|
|
72
|
-
if (!isNaN(when))
|
|
73
|
-
delay = Math.max(0, when - Date.now());
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Full jitter to avoid synchronized retries
|
|
77
|
-
delay = Math.min(maxDelayMs, delay);
|
|
78
|
-
delay = Math.floor(delay * (0.5 + Math.random() * 0.5));
|
|
79
|
-
console.log(` [gmail] ${res.status} (attempt ${attempt + 1}/${maxAttempts}), waiting ${(delay / 1000).toFixed(1)}s${retryAfter ? ` (Retry-After: ${retryAfter})` : ""}...`);
|
|
80
|
-
await new Promise(r => setTimeout(r, delay));
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (!res.ok) {
|
|
84
|
-
const err = await res.text().catch(() => "");
|
|
85
|
-
throw new Error(`Gmail API ${res.status}: ${err.substring(0, 200)}`);
|
|
86
|
-
}
|
|
87
|
-
return res.json();
|
|
88
|
-
}
|
|
89
|
-
throw new Error(`Gmail API: failed after ${maxAttempts} retries (last status ${lastStatus})`);
|
|
90
|
-
}
|
|
91
|
-
async listFolders() {
|
|
92
|
-
const data = await this.fetch("/labels");
|
|
93
|
-
const labels = data.labels || [];
|
|
94
|
-
const folders = [];
|
|
95
|
-
for (const label of labels) {
|
|
96
|
-
// Skip system labels that aren't useful as folders
|
|
97
|
-
if (["UNREAD", "STARRED", "IMPORTANT", "CATEGORY_PERSONAL",
|
|
98
|
-
"CATEGORY_SOCIAL", "CATEGORY_PROMOTIONS", "CATEGORY_UPDATES",
|
|
99
|
-
"CATEGORY_FORUMS", "CHAT"].includes(label.id))
|
|
100
|
-
continue;
|
|
101
|
-
const specialUse = labelSpecialUse(label);
|
|
102
|
-
// Map Gmail path separators (/) to IMAP-style
|
|
103
|
-
const path = label.name || label.id;
|
|
104
|
-
const name = path.includes("/") ? path.split("/").pop() : path;
|
|
105
|
-
folders.push({
|
|
106
|
-
path,
|
|
107
|
-
name,
|
|
108
|
-
delimiter: "/",
|
|
109
|
-
specialUse,
|
|
110
|
-
flags: label.type === "system" ? ["\\Noselect"] : [],
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
return folders;
|
|
114
|
-
}
|
|
115
|
-
/** List message IDs matching a query, handling pagination.
|
|
116
|
-
* IMPORTANT: on any error we throw — do NOT return a partial list, because
|
|
117
|
-
* callers use this for sync reconciliation and a short list would delete
|
|
118
|
-
* real messages from the local DB. Returning [] silently caused the
|
|
119
|
-
* "INBOX empty in mailx" bug when a rate-limit hit mid-pagination. */
|
|
120
|
-
async listMessageIds(query, maxResults = 500) {
|
|
121
|
-
const ids = [];
|
|
122
|
-
let pageToken = "";
|
|
123
|
-
let truncated = false;
|
|
124
|
-
while (true) {
|
|
125
|
-
const params = new URLSearchParams({ q: query, maxResults: String(Math.min(maxResults - ids.length, 500)) });
|
|
126
|
-
if (pageToken)
|
|
127
|
-
params.set("pageToken", pageToken);
|
|
128
|
-
const data = await this.fetch(`/messages?${params}`);
|
|
129
|
-
for (const msg of data.messages || []) {
|
|
130
|
-
ids.push(msg.id);
|
|
131
|
-
}
|
|
132
|
-
if (!data.nextPageToken)
|
|
133
|
-
break;
|
|
134
|
-
if (ids.length >= maxResults) {
|
|
135
|
-
// Hit the caller's cap but the server has more. Flag it so
|
|
136
|
-
// reconcile-style callers can refuse to treat this as complete.
|
|
137
|
-
truncated = true;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
pageToken = data.nextPageToken;
|
|
141
|
-
}
|
|
142
|
-
ids._truncated = truncated;
|
|
143
|
-
return ids;
|
|
144
|
-
}
|
|
145
|
-
/** Batch-fetch message metadata or full content */
|
|
146
|
-
async batchFetch(ids, options = {}, onChunk) {
|
|
147
|
-
const all = [];
|
|
148
|
-
const chunkSize = options.source ? 10 : 50; // Smaller chunks for full bodies
|
|
149
|
-
const format = options.source ? "raw" : "metadata";
|
|
150
|
-
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
151
|
-
const chunk = ids.slice(i, i + chunkSize);
|
|
152
|
-
// Sequential fetches to avoid Gmail 429 rate limits
|
|
153
|
-
const messages = [];
|
|
154
|
-
for (const id of chunk) {
|
|
155
|
-
const params = new URLSearchParams({ format });
|
|
156
|
-
if (format === "metadata") {
|
|
157
|
-
for (const h of ["From", "To", "Cc", "Subject", "Message-ID", "Date", "In-Reply-To", "References"]) {
|
|
158
|
-
params.append("metadataHeaders", h);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
messages.push(await this.fetch(`/messages/${id}?${params}`));
|
|
162
|
-
}
|
|
163
|
-
const parsed = messages.map(msg => this.parseMessage(msg, options));
|
|
164
|
-
all.push(...parsed);
|
|
165
|
-
if (onChunk)
|
|
166
|
-
onChunk(parsed);
|
|
167
|
-
}
|
|
168
|
-
return all;
|
|
169
|
-
}
|
|
170
|
-
/** Parse a Gmail API message response into ProviderMessage */
|
|
171
|
-
parseMessage(msg, options = {}) {
|
|
172
|
-
const labels = msg.labelIds || [];
|
|
173
|
-
const headers = msg.payload?.headers || [];
|
|
174
|
-
let source = "";
|
|
175
|
-
if (options.source && msg.raw) {
|
|
176
|
-
// Gmail returns URL-safe base64 — convert to standard base64 then decode
|
|
177
|
-
const base64 = msg.raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
178
|
-
source = new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));
|
|
179
|
-
}
|
|
180
|
-
const fromRaw = getHeader(headers, "From");
|
|
181
|
-
const toRaw = getHeader(headers, "To");
|
|
182
|
-
const ccRaw = getHeader(headers, "Cc");
|
|
183
|
-
const dateRaw = getHeader(headers, "Date") || "";
|
|
184
|
-
const subject = getHeader(headers, "Subject") || msg.snippet || "";
|
|
185
|
-
const messageId = getHeader(headers, "Message-ID") || "";
|
|
186
|
-
const inReplyTo = getHeader(headers, "In-Reply-To") || "";
|
|
187
|
-
const referencesRaw = getHeader(headers, "References") || "";
|
|
188
|
-
const references = referencesRaw.trim()
|
|
189
|
-
? referencesRaw.split(/\s+/).filter(r => r.startsWith("<") && r.endsWith(">"))
|
|
190
|
-
: [];
|
|
191
|
-
return {
|
|
192
|
-
uid: idToUid(msg.id),
|
|
193
|
-
messageId,
|
|
194
|
-
providerId: msg.id,
|
|
195
|
-
date: dateRaw ? new Date(dateRaw) : (msg.internalDate ? new Date(Number(msg.internalDate)) : null),
|
|
196
|
-
subject,
|
|
197
|
-
from: parseAddressList(fromRaw),
|
|
198
|
-
to: parseAddressList(toRaw),
|
|
199
|
-
cc: parseAddressList(ccRaw),
|
|
200
|
-
inReplyTo,
|
|
201
|
-
references,
|
|
202
|
-
seen: !labels.includes("UNREAD"),
|
|
203
|
-
flagged: labels.includes("STARRED"),
|
|
204
|
-
answered: false, // Gmail API doesn't expose this directly
|
|
205
|
-
draft: labels.includes("DRAFT"),
|
|
206
|
-
size: msg.sizeEstimate || 0,
|
|
207
|
-
source,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
async fetchSince(folder, sinceUid, options = {}) {
|
|
211
|
-
// Gmail message IDs are hash-derived, NOT monotonic — filtering by
|
|
212
|
-
// `uid > sinceUid` silently drops new messages whose hash happens to
|
|
213
|
-
// fall below the high-water mark. Fetch the most recent page and let
|
|
214
|
-
// upsertMessage dedupe by (account, folder, uid). The sinceUid arg is
|
|
215
|
-
// kept for interface compatibility but no longer used for filtering.
|
|
216
|
-
void sinceUid;
|
|
217
|
-
const query = `in:${this.folderToLabel(folder)}`;
|
|
218
|
-
const ids = await this.listMessageIds(query, 200);
|
|
219
|
-
return this.batchFetch(ids, options);
|
|
220
|
-
}
|
|
221
|
-
async fetchByDate(folder, since, before, options = {}, onChunk) {
|
|
222
|
-
const afterDate = this.formatDate(since);
|
|
223
|
-
const beforeDate = this.formatDate(before);
|
|
224
|
-
const query = `in:${this.folderToLabel(folder)} after:${afterDate} before:${beforeDate}`;
|
|
225
|
-
const ids = await this.listMessageIds(query);
|
|
226
|
-
return this.batchFetch(ids, options, onChunk);
|
|
227
|
-
}
|
|
228
|
-
async fetchByUids(folder, uids, options = {}) {
|
|
229
|
-
// UIDs are derived from Gmail IDs — we'd need a reverse lookup
|
|
230
|
-
// For now, fetch all messages in folder and filter
|
|
231
|
-
const query = `in:${this.folderToLabel(folder)}`;
|
|
232
|
-
const ids = await this.listMessageIds(query);
|
|
233
|
-
const uidSet = new Set(uids);
|
|
234
|
-
const matchingIds = ids.filter(id => uidSet.has(idToUid(id)));
|
|
235
|
-
return this.batchFetch(matchingIds, options);
|
|
236
|
-
}
|
|
237
|
-
/** Bulk-fetch raw bodies for many UIDs in one "folder" (Gmail label).
|
|
238
|
-
* Lists the label once, builds UID→ID map, then streams bodies through
|
|
239
|
-
* `onBody` with bounded concurrency (lets Gmail's HTTP/2 stream multiplex;
|
|
240
|
-
* `fetch()`'s built-in 429/5xx retry handles backoff automatically).
|
|
241
|
-
*
|
|
242
|
-
* NOTE: Gmail's model is labels, not folders — a single message can be in
|
|
243
|
-
* many labels. Treating each label as a folder causes duplicate fetches
|
|
244
|
-
* across labels. Proper fix tracked as separate TODO ("Gmail label-native
|
|
245
|
-
* model"). For now we mirror the IMAP folder grouping, accepting duplicate
|
|
246
|
-
* fetches of multi-labeled messages. */
|
|
247
|
-
async fetchBodiesBatch(folder, uids, onBody) {
|
|
248
|
-
if (uids.length === 0)
|
|
249
|
-
return;
|
|
250
|
-
const query = `in:${this.folderToLabel(folder)}`;
|
|
251
|
-
const ids = await this.listMessageIds(query, 10000);
|
|
252
|
-
const uidToId = new Map();
|
|
253
|
-
for (const id of ids)
|
|
254
|
-
uidToId.set(idToUid(id), id);
|
|
255
|
-
const wanted = [];
|
|
256
|
-
for (const uid of uids) {
|
|
257
|
-
const id = uidToId.get(uid);
|
|
258
|
-
if (id)
|
|
259
|
-
wanted.push({ uid, id });
|
|
260
|
-
}
|
|
261
|
-
if (wanted.length === 0)
|
|
262
|
-
return;
|
|
263
|
-
// Bounded concurrency — 10 in-flight is safe under Gmail's per-user rate
|
|
264
|
-
// limit (250 quota units/sec, messages.get = 5 units each = 50/sec cap).
|
|
265
|
-
const CONCURRENCY = 10;
|
|
266
|
-
let cursor = 0;
|
|
267
|
-
const worker = async () => {
|
|
268
|
-
while (cursor < wanted.length) {
|
|
269
|
-
const idx = cursor++;
|
|
270
|
-
const { uid, id } = wanted[idx];
|
|
271
|
-
try {
|
|
272
|
-
const msg = await this.fetch(`/messages/${id}?format=raw`);
|
|
273
|
-
if (!msg?.raw)
|
|
274
|
-
continue;
|
|
275
|
-
const base64 = msg.raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
276
|
-
const source = new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));
|
|
277
|
-
onBody(uid, source);
|
|
278
|
-
}
|
|
279
|
-
catch (e) {
|
|
280
|
-
// Per-message failure is non-fatal; keep worker alive for the rest.
|
|
281
|
-
console.error(` [gmail batch] UID ${uid}: ${e.message}`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, wanted.length) }, () => worker()));
|
|
286
|
-
}
|
|
287
|
-
async fetchOne(folder, uid, options = {}) {
|
|
288
|
-
// Need to find the Gmail ID from the UID — search all messages in folder
|
|
289
|
-
const query = `in:${this.folderToLabel(folder)}`;
|
|
290
|
-
const ids = await this.listMessageIds(query, 1000);
|
|
291
|
-
const id = ids.find(id => idToUid(id) === uid);
|
|
292
|
-
if (!id)
|
|
293
|
-
return null;
|
|
294
|
-
const format = options.source ? "raw" : "metadata";
|
|
295
|
-
const params = new URLSearchParams({ format });
|
|
296
|
-
if (format === "metadata") {
|
|
297
|
-
for (const h of ["From", "To", "Cc", "Subject", "Message-ID", "Date", "In-Reply-To", "References"]) {
|
|
298
|
-
params.append("metadataHeaders", h);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const msg = await this.fetch(`/messages/${id}?${params}`);
|
|
302
|
-
return this.parseMessage(msg, options);
|
|
303
|
-
}
|
|
304
|
-
async getUids(folder) {
|
|
305
|
-
const query = `in:${this.folderToLabel(folder)}`;
|
|
306
|
-
const ids = await this.listMessageIds(query, 10000);
|
|
307
|
-
const result = ids.map(idToUid);
|
|
308
|
-
// Propagate the truncation flag so reconcile can refuse to delete.
|
|
309
|
-
if (ids._truncated)
|
|
310
|
-
result._truncated = true;
|
|
311
|
-
return result;
|
|
312
|
-
}
|
|
313
|
-
async close() {
|
|
314
|
-
// No persistent connection to close
|
|
315
|
-
}
|
|
316
|
-
/** Map folder path to Gmail label query term */
|
|
317
|
-
folderToLabel(path) {
|
|
318
|
-
const lower = path.toLowerCase();
|
|
319
|
-
if (lower === "inbox")
|
|
320
|
-
return "inbox";
|
|
321
|
-
if (lower === "sent" || lower === "[gmail]/sent mail")
|
|
322
|
-
return "sent";
|
|
323
|
-
if (lower === "drafts" || lower === "[gmail]/drafts")
|
|
324
|
-
return "drafts";
|
|
325
|
-
if (lower === "trash" || lower === "[gmail]/trash")
|
|
326
|
-
return "trash";
|
|
327
|
-
if (lower === "spam" || lower === "[gmail]/spam" || lower === "junk email")
|
|
328
|
-
return "spam";
|
|
329
|
-
if (lower === "archive" || lower === "[gmail]/all mail")
|
|
330
|
-
return "all";
|
|
331
|
-
// Custom label — use exact name
|
|
332
|
-
return `"${path}"`;
|
|
333
|
-
}
|
|
334
|
-
/** Format date for Gmail query (YYYY/MM/DD) */
|
|
335
|
-
formatDate(d) {
|
|
336
|
-
return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}`;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
7
|
+
export { GmailApiProvider } from "@bobfrankston/mailx-sync";
|
|
339
8
|
//# sourceMappingURL=gmail-api.js.map
|
|
@@ -1,62 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Back-compat re-export. The canonical mail provider types live in
|
|
3
|
+
* @bobfrankston/mailx-sync. New code should import from there directly.
|
|
4
|
+
*
|
|
5
|
+
* This shim keeps existing `import { ... } from "./types.js"` paths inside
|
|
6
|
+
* mailx-imap working without churn.
|
|
4
7
|
*/
|
|
5
|
-
export
|
|
6
|
-
path: string;
|
|
7
|
-
name: string;
|
|
8
|
-
delimiter: string;
|
|
9
|
-
specialUse: string;
|
|
10
|
-
flags: string[];
|
|
11
|
-
}
|
|
12
|
-
export interface ProviderMessage {
|
|
13
|
-
uid: number;
|
|
14
|
-
messageId: string;
|
|
15
|
-
providerId: string;
|
|
16
|
-
date: Date | null;
|
|
17
|
-
subject: string;
|
|
18
|
-
from: {
|
|
19
|
-
name?: string;
|
|
20
|
-
address?: string;
|
|
21
|
-
}[];
|
|
22
|
-
to: {
|
|
23
|
-
name?: string;
|
|
24
|
-
address?: string;
|
|
25
|
-
}[];
|
|
26
|
-
cc: {
|
|
27
|
-
name?: string;
|
|
28
|
-
address?: string;
|
|
29
|
-
}[];
|
|
30
|
-
inReplyTo?: string;
|
|
31
|
-
references?: string[];
|
|
32
|
-
seen: boolean;
|
|
33
|
-
flagged: boolean;
|
|
34
|
-
answered: boolean;
|
|
35
|
-
draft: boolean;
|
|
36
|
-
size: number;
|
|
37
|
-
source: string;
|
|
38
|
-
}
|
|
39
|
-
export interface FetchOptions {
|
|
40
|
-
source?: boolean;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* A mail provider that can list folders, fetch messages, and perform actions.
|
|
44
|
-
* Implementations: ImapProvider (existing iflow), GmailApiProvider, GraphApiProvider.
|
|
45
|
-
*/
|
|
46
|
-
export interface MailProvider {
|
|
47
|
-
/** List all folders/labels */
|
|
48
|
-
listFolders(): Promise<ProviderFolder[]>;
|
|
49
|
-
/** Fetch messages newer than sinceUid (incremental sync) */
|
|
50
|
-
fetchSince(folder: string, sinceUid: number, options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
51
|
-
/** Fetch messages by date range (first sync) */
|
|
52
|
-
fetchByDate(folder: string, since: Date, before: Date, options?: FetchOptions, onChunk?: (msgs: ProviderMessage[]) => void): Promise<ProviderMessage[]>;
|
|
53
|
-
/** Fetch specific messages by UID */
|
|
54
|
-
fetchByUids(folder: string, uids: number[], options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
55
|
-
/** Fetch a single message by UID */
|
|
56
|
-
fetchOne(folder: string, uid: number, options?: FetchOptions): Promise<ProviderMessage | null>;
|
|
57
|
-
/** Get all UIDs in a folder (for reconciliation) */
|
|
58
|
-
getUids(folder: string): Promise<number[]>;
|
|
59
|
-
/** Close/cleanup */
|
|
60
|
-
close(): Promise<void>;
|
|
61
|
-
}
|
|
8
|
+
export type { MailProvider, ProviderFolder, ProviderMessage, FetchOptions } from "@bobfrankston/mailx-sync";
|
|
62
9
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Back-compat re-export. The canonical mail provider types live in
|
|
3
|
+
* @bobfrankston/mailx-sync. New code should import from there directly.
|
|
4
|
+
*
|
|
5
|
+
* This shim keeps existing `import { ... } from "./types.js"` paths inside
|
|
6
|
+
* mailx-imap working without churn.
|
|
4
7
|
*/
|
|
5
8
|
export {};
|
|
6
9
|
//# sourceMappingURL=types.js.map
|
|
@@ -96,13 +96,27 @@ export class MailxService {
|
|
|
96
96
|
err.isNotFound = true;
|
|
97
97
|
throw err;
|
|
98
98
|
}
|
|
99
|
+
// Don't stuff the error text into bodyText — it bleeds into the
|
|
100
|
+
// viewer's main content area. Surface as a structured error field
|
|
101
|
+
// so the UI can render a banner with retry UX above the (empty)
|
|
102
|
+
// body. The caller keeps the envelope so the header still shows.
|
|
103
|
+
const rawErr = fetchErr.message || "connection failed";
|
|
104
|
+
const isTransient = /connection|Too many|UNAVAILABLE|rate|429|5\d\d|timeout|ENOTFOUND|ECONNRESET|ETIMEDOUT/i.test(rawErr);
|
|
99
105
|
return {
|
|
100
|
-
...envelope, bodyHtml: "", bodyText:
|
|
106
|
+
...envelope, bodyHtml: "", bodyText: "",
|
|
107
|
+
bodyError: rawErr,
|
|
108
|
+
bodyErrorTransient: isTransient,
|
|
101
109
|
hasRemoteContent: false, remoteAllowed: false, attachments: [], emlPath: "", deliveredTo: "", returnPath: "", listUnsubscribe: ""
|
|
102
110
|
};
|
|
103
111
|
}
|
|
104
112
|
if (!raw) {
|
|
105
|
-
|
|
113
|
+
// Same treatment as the thrown-error case: structured field, not body text.
|
|
114
|
+
return {
|
|
115
|
+
...envelope, bodyHtml: "", bodyText: "",
|
|
116
|
+
bodyError: "Message body not cached locally and the server fetch returned nothing.",
|
|
117
|
+
bodyErrorTransient: true,
|
|
118
|
+
hasRemoteContent: false, remoteAllowed: false, attachments: [], emlPath: "", deliveredTo: "", returnPath: "", listUnsubscribe: ""
|
|
119
|
+
};
|
|
106
120
|
}
|
|
107
121
|
else {
|
|
108
122
|
const parsed = await simpleParser(raw);
|
|
@@ -694,13 +694,15 @@ async function waitForNativeBridge(timeoutMs = 5000) {
|
|
|
694
694
|
});
|
|
695
695
|
}
|
|
696
696
|
export async function initAndroid() {
|
|
697
|
-
console.log("[android] Initializing mailx...");
|
|
698
|
-
//
|
|
697
|
+
console.log("[android] Initializing mailx (Worker mode)...");
|
|
698
|
+
// Use the Worker host — all service logic runs off the main thread.
|
|
699
|
+
const { createWorkerHost } = await import("./main-thread-host.js");
|
|
700
|
+
await createWorkerHost();
|
|
701
|
+
console.log("[android] Worker host initialized");
|
|
702
|
+
return;
|
|
703
|
+
// ── Legacy main-thread path (kept for reference, unreachable) ──
|
|
704
|
+
// eslint-disable-next-line no-unreachable
|
|
699
705
|
await waitForNativeBridge();
|
|
700
|
-
console.log(`[android] Native bridge: ${window._nativeBridge ? "ready" : "timeout"}`);
|
|
701
|
-
// iflow-direct's BridgeTransport expects a global `msgapi` with a .tcp
|
|
702
|
-
// subobject. Our MAUI shell exposes the same API under `_nativeBridge`, so
|
|
703
|
-
// alias it. Must happen before any IMAP client is constructed.
|
|
704
706
|
if (window._nativeBridge && !window.msgapi) {
|
|
705
707
|
window.msgapi = window._nativeBridge;
|
|
706
708
|
}
|
|
@@ -1,41 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Back-compat re-export. The Gmail provider is now a single implementation
|
|
3
|
+
* in @bobfrankston/mailx-sync, used by both desktop (mailx-imap) and
|
|
4
|
+
* Android (this package). Bug fixes (e.g. 403-quota retry, bounded
|
|
5
|
+
* concurrency) land in one place and apply to both platforms.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* that aren't available in a WebView.
|
|
7
|
+
* `GmailApiWebProvider` is kept as an alias for back-compat with existing
|
|
8
|
+
* Android wiring; it's the same class as `GmailApiProvider`.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
export declare class GmailApiWebProvider implements MailProvider {
|
|
12
|
-
private tokenProvider;
|
|
13
|
-
constructor(tokenProvider: () => Promise<string>);
|
|
14
|
-
private apiFetch;
|
|
15
|
-
listFolders(): Promise<ProviderFolder[]>;
|
|
16
|
-
private listMessageIds;
|
|
17
|
-
private batchFetch;
|
|
18
|
-
private parseMessage;
|
|
19
|
-
fetchSince(folder: string, sinceUid: number, options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
20
|
-
fetchByDate(folder: string, since: Date, before: Date, options?: FetchOptions, onChunk?: (msgs: ProviderMessage[]) => void): Promise<ProviderMessage[]>;
|
|
21
|
-
fetchByUids(folder: string, uids: number[], options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
22
|
-
/** Fetch a message directly by Gmail provider ID — fast path, no listing needed */
|
|
23
|
-
fetchById(providerId: string, options?: FetchOptions): Promise<ProviderMessage | null>;
|
|
24
|
-
fetchOne(folder: string, uid: number, options?: FetchOptions): Promise<ProviderMessage | null>;
|
|
25
|
-
getUids(folder: string): Promise<number[]>;
|
|
26
|
-
close(): Promise<void>;
|
|
27
|
-
/** Bulk-fetch raw bodies for many UIDs in one Gmail label. Lists the label
|
|
28
|
-
* once, builds UID→ID map, streams bodies via `onBody` with bounded
|
|
29
|
-
* concurrency. Mirrors GmailApiProvider.fetchBodiesBatch on desktop. */
|
|
30
|
-
fetchBodiesBatch(folder: string, uids: number[], onBody: (uid: number, source: string) => void): Promise<void>;
|
|
31
|
-
/** Send an RFC 2822 message via Gmail API users.messages.send. The server
|
|
32
|
-
* handles SMTP — we just hand it the raw bytes base64url-encoded. Auto-files
|
|
33
|
-
* a copy into the Sent label, so caller does NOT need to APPEND to Sent. */
|
|
34
|
-
sendRaw(rawRfc822: string): Promise<{
|
|
35
|
-
id: string;
|
|
36
|
-
threadId: string;
|
|
37
|
-
}>;
|
|
38
|
-
private folderToLabel;
|
|
39
|
-
private formatDate;
|
|
40
|
-
}
|
|
10
|
+
export { GmailApiProvider as GmailApiWebProvider } from "@bobfrankston/mailx-sync";
|
|
41
11
|
//# sourceMappingURL=gmail-api-web.d.ts.map
|