@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.
Files changed (28) hide show
  1. package/bin/mailx.js +112 -0
  2. package/client/.msger-window.json +1 -1
  3. package/client/components/message-viewer.js +82 -7
  4. package/package.json +7 -6
  5. package/packages/mailx-imap/index.d.ts +6 -0
  6. package/packages/mailx-imap/index.js +100 -33
  7. package/packages/mailx-imap/package.json +2 -1
  8. package/packages/mailx-imap/providers/gmail-api.d.ts +5 -40
  9. package/packages/mailx-imap/providers/gmail-api.js +5 -336
  10. package/packages/mailx-imap/providers/types.d.ts +6 -59
  11. package/packages/mailx-imap/providers/types.js +5 -2
  12. package/packages/mailx-service/index.js +16 -2
  13. package/packages/mailx-store-web/android-bootstrap.js +8 -6
  14. package/packages/mailx-store-web/gmail-api-web.d.ts +7 -37
  15. package/packages/mailx-store-web/gmail-api-web.js +7 -298
  16. package/packages/mailx-store-web/imap-web-provider.d.ts +1 -1
  17. package/packages/mailx-store-web/imap-web-provider.js +2 -2
  18. package/packages/mailx-store-web/main-thread-host.d.ts +15 -0
  19. package/packages/mailx-store-web/main-thread-host.js +287 -0
  20. package/packages/mailx-store-web/package.json +2 -1
  21. package/packages/mailx-store-web/provider-types.d.ts +4 -47
  22. package/packages/mailx-store-web/provider-types.js +3 -3
  23. package/packages/mailx-store-web/sync-manager.d.ts +61 -0
  24. package/packages/mailx-store-web/sync-manager.js +422 -0
  25. package/packages/mailx-store-web/worker-entry.d.ts +8 -0
  26. package/packages/mailx-store-web/worker-entry.js +187 -0
  27. package/packages/mailx-store-web/worker-tcp-transport.d.ts +28 -0
  28. package/packages/mailx-store-web/worker-tcp-transport.js +98 -0
@@ -1,339 +1,8 @@
1
1
  /**
2
- * Gmail API provider replaces IMAP for Gmail accounts.
3
- * Uses REST API for fast, reliable sync without connection limit issues.
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
- const API = "https://gmail.googleapis.com/gmail/v1/users/me";
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
- * Mail provider interface abstraction over IMAP, Gmail API, and Microsoft Graph.
3
- * Sync code uses this interface; never calls IMAP/REST directly.
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 interface ProviderFolder {
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
- * Mail provider interface abstraction over IMAP, Gmail API, and Microsoft Graph.
3
- * Sync code uses this interface; never calls IMAP/REST directly.
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: `[Message body unavailable: ${fetchErr.message || "connection failed"}]`,
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
- bodyText = "[Message body not cached locally and the server fetch returned nothing. Try re-syncing the folder.]";
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
- // Wait for C# to inject the native bridge (TCP/FS/HTTP + OAuth)
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
- * Web-compatible Gmail API provider.
3
- * Identical to packages/mailx-imap/providers/gmail-api.ts but uses
4
- * atob() instead of Buffer.from() for base64 decoding (no Node.js deps).
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
- * This file is a standalone copy not imported from mailx-imap because
7
- * mailx-imap depends on Node.js modules (node:events, node:fs, mailparser, etc.)
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
- import type { MailProvider, ProviderFolder, ProviderMessage, FetchOptions } from "./provider-types.js";
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