@bobfrankston/mailx-sync 0.1.7 → 0.1.9
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/gmail.d.ts +12 -0
- package/gmail.js +75 -8
- package/package.json +3 -3
- package/types.d.ts +4 -0
package/gmail.d.ts
CHANGED
|
@@ -51,6 +51,18 @@ export declare class GmailApiProvider implements MailProvider {
|
|
|
51
51
|
* remove so the end state matches regardless of what was there before,
|
|
52
52
|
* which makes the call idempotent and safe to retry. */
|
|
53
53
|
setFlags(folder: string, uid: number, flags: string[]): Promise<void>;
|
|
54
|
+
/** Move a message to the trash label. Gmail treats trash as a label, not
|
|
55
|
+
* as a destination folder — `POST /messages/{id}/trash` is the native
|
|
56
|
+
* path (equivalent to setting TRASH and removing INBOX in one op).
|
|
57
|
+
* Used by mailx's delete/trash path. */
|
|
58
|
+
trashMessage(folder: string, uid: number): Promise<void>;
|
|
59
|
+
/** Move between "folders" == swap one label for another via modifyLabels.
|
|
60
|
+
* System labels (INBOX/SENT/TRASH/SPAM) are translated from the folder
|
|
61
|
+
* path; user labels use the folder path verbatim as the label id. */
|
|
62
|
+
moveMessage(fromFolder: string, uid: number, toFolder: string): Promise<void>;
|
|
63
|
+
/** Folder path → Gmail label id. System folders map to uppercase label
|
|
64
|
+
* constants; anything else is treated as a user label (identical name). */
|
|
65
|
+
private folderPathToLabelId;
|
|
54
66
|
getUids(folder: string): Promise<number[]>;
|
|
55
67
|
close(): Promise<void>;
|
|
56
68
|
/** Map folder path to Gmail label query term */
|
package/gmail.js
CHANGED
|
@@ -207,11 +207,15 @@ export class GmailApiProvider {
|
|
|
207
207
|
* real messages from the local DB. Returning [] silently caused the
|
|
208
208
|
* "INBOX empty in mailx" bug when a rate-limit hit mid-pagination. */
|
|
209
209
|
async listMessageIds(query, maxResults = 500) {
|
|
210
|
+
// maxResults === 0 → unlimited (for date-bounded queries where the
|
|
211
|
+
// query itself caps the result set).
|
|
212
|
+
const unlimited = maxResults === 0;
|
|
210
213
|
const ids = [];
|
|
211
214
|
let pageToken = "";
|
|
212
215
|
let truncated = false;
|
|
213
216
|
while (true) {
|
|
214
|
-
const
|
|
217
|
+
const pageSize = unlimited ? 500 : Math.min(maxResults - ids.length, 500);
|
|
218
|
+
const params = new URLSearchParams({ q: query, maxResults: String(pageSize) });
|
|
215
219
|
if (pageToken)
|
|
216
220
|
params.set("pageToken", pageToken);
|
|
217
221
|
const data = await this.fetch(`/messages?${params}`);
|
|
@@ -220,7 +224,7 @@ export class GmailApiProvider {
|
|
|
220
224
|
}
|
|
221
225
|
if (!data.nextPageToken)
|
|
222
226
|
break;
|
|
223
|
-
if (ids.length >= maxResults) {
|
|
227
|
+
if (!unlimited && ids.length >= maxResults) {
|
|
224
228
|
// Hit the caller's cap but the server has more. Flag it so
|
|
225
229
|
// reconcile-style callers can refuse to treat this as complete.
|
|
226
230
|
truncated = true;
|
|
@@ -299,19 +303,27 @@ export class GmailApiProvider {
|
|
|
299
303
|
async fetchSince(folder, sinceUid, options = {}) {
|
|
300
304
|
// Gmail message IDs are hash-derived, NOT monotonic — filtering by
|
|
301
305
|
// `uid > sinceUid` silently drops new messages whose hash happens to
|
|
302
|
-
// fall below the high-water mark. Fetch
|
|
303
|
-
//
|
|
304
|
-
// kept for interface compatibility but no longer used for filtering.
|
|
306
|
+
// fall below the high-water mark. Fetch by date (when caller supplied
|
|
307
|
+
// a `since` window) or fall back to the recent-200 page.
|
|
305
308
|
void sinceUid;
|
|
306
|
-
|
|
307
|
-
|
|
309
|
+
let query = `in:${this.folderToLabel(folder)}`;
|
|
310
|
+
if (options.since) {
|
|
311
|
+
// after: is inclusive at day granularity; filter precisely on the
|
|
312
|
+
// client side since Gmail's search is whole-day.
|
|
313
|
+
query += ` after:${this.formatDate(options.since)}`;
|
|
314
|
+
}
|
|
315
|
+
// When the caller bounded by date, let pagination run — the query
|
|
316
|
+
// is self-limiting. Otherwise cap at the recent 200.
|
|
317
|
+
const cap = options.since ? 0 : 200;
|
|
318
|
+
const ids = await this.listMessageIds(query, cap);
|
|
308
319
|
return this.batchFetch(ids, options);
|
|
309
320
|
}
|
|
310
321
|
async fetchByDate(folder, since, before, options = {}, onChunk) {
|
|
311
322
|
const afterDate = this.formatDate(since);
|
|
312
323
|
const beforeDate = this.formatDate(before);
|
|
313
324
|
const query = `in:${this.folderToLabel(folder)} after:${afterDate} before:${beforeDate}`;
|
|
314
|
-
|
|
325
|
+
// Date-bounded — let pagination drain the full range instead of stopping at 500.
|
|
326
|
+
const ids = await this.listMessageIds(query, 0);
|
|
315
327
|
return this.batchFetch(ids, options, onChunk);
|
|
316
328
|
}
|
|
317
329
|
async fetchByUids(folder, uids, options = {}) {
|
|
@@ -427,6 +439,61 @@ export class GmailApiProvider {
|
|
|
427
439
|
body: JSON.stringify({ addLabelIds, removeLabelIds }),
|
|
428
440
|
});
|
|
429
441
|
}
|
|
442
|
+
/** Move a message to the trash label. Gmail treats trash as a label, not
|
|
443
|
+
* as a destination folder — `POST /messages/{id}/trash` is the native
|
|
444
|
+
* path (equivalent to setting TRASH and removing INBOX in one op).
|
|
445
|
+
* Used by mailx's delete/trash path. */
|
|
446
|
+
async trashMessage(folder, uid) {
|
|
447
|
+
const query = `in:${this.folderToLabel(folder)}`;
|
|
448
|
+
const ids = await this.listMessageIds(query, 1000);
|
|
449
|
+
const id = ids.find(id => idToUid(id) === uid);
|
|
450
|
+
if (!id)
|
|
451
|
+
throw new Error(`Gmail trashMessage: UID ${uid} not found in ${folder}`);
|
|
452
|
+
await this.fetch(`/messages/${id}/trash`, { method: "POST" });
|
|
453
|
+
}
|
|
454
|
+
/** Move between "folders" == swap one label for another via modifyLabels.
|
|
455
|
+
* System labels (INBOX/SENT/TRASH/SPAM) are translated from the folder
|
|
456
|
+
* path; user labels use the folder path verbatim as the label id. */
|
|
457
|
+
async moveMessage(fromFolder, uid, toFolder) {
|
|
458
|
+
const query = `in:${this.folderToLabel(fromFolder)}`;
|
|
459
|
+
const ids = await this.listMessageIds(query, 1000);
|
|
460
|
+
const id = ids.find(id => idToUid(id) === uid);
|
|
461
|
+
if (!id)
|
|
462
|
+
throw new Error(`Gmail moveMessage: UID ${uid} not found in ${fromFolder}`);
|
|
463
|
+
// Map the folder path to the label id. System labels are uppercased
|
|
464
|
+
// aliases; user labels are passed through as-is (Gmail's label ids
|
|
465
|
+
// for user-created labels match the visible label name).
|
|
466
|
+
const toLabel = this.folderPathToLabelId(toFolder);
|
|
467
|
+
const fromLabel = this.folderPathToLabelId(fromFolder);
|
|
468
|
+
const addLabelIds = [];
|
|
469
|
+
const removeLabelIds = [];
|
|
470
|
+
if (toLabel)
|
|
471
|
+
addLabelIds.push(toLabel);
|
|
472
|
+
if (fromLabel && fromLabel !== toLabel)
|
|
473
|
+
removeLabelIds.push(fromLabel);
|
|
474
|
+
await this.fetch(`/messages/${id}/modify`, {
|
|
475
|
+
method: "POST",
|
|
476
|
+
body: JSON.stringify({ addLabelIds, removeLabelIds }),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/** Folder path → Gmail label id. System folders map to uppercase label
|
|
480
|
+
* constants; anything else is treated as a user label (identical name). */
|
|
481
|
+
folderPathToLabelId(path) {
|
|
482
|
+
const lower = path.toLowerCase();
|
|
483
|
+
if (lower === "inbox")
|
|
484
|
+
return "INBOX";
|
|
485
|
+
if (lower === "sent" || lower === "[gmail]/sent mail")
|
|
486
|
+
return "SENT";
|
|
487
|
+
if (lower === "drafts" || lower === "[gmail]/drafts")
|
|
488
|
+
return "DRAFT";
|
|
489
|
+
if (lower === "trash" || lower === "[gmail]/trash")
|
|
490
|
+
return "TRASH";
|
|
491
|
+
if (lower === "spam" || lower === "junk email" || lower === "[gmail]/spam")
|
|
492
|
+
return "SPAM";
|
|
493
|
+
if (lower === "archive" || lower === "[gmail]/all mail")
|
|
494
|
+
return ""; // no-op — archive is absence-of-INBOX
|
|
495
|
+
return path; // user label — name-as-id
|
|
496
|
+
}
|
|
430
497
|
async getUids(folder) {
|
|
431
498
|
const query = `in:${this.folderToLabel(folder)}`;
|
|
432
499
|
const ids = await this.listMessageIds(query, 10000);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Platform-agnostic mail provider implementations + sync orchestration. Single source of truth for Gmail/IMAP/Outlook protocol code, consumed by both desktop (Node) and Android (WebView) — eliminates the parallel mailx-imap/mailx-store-web Gmail providers that drifted in practice.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"author": "Bob Frankston",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
22
|
+
"@bobfrankston/iflow-direct": "^0.1.26",
|
|
23
23
|
"@bobfrankston/tcp-transport": "^0.1.4"
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
".transformedSnapshot": {
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
47
|
+
"@bobfrankston/iflow-direct": "^0.1.26",
|
|
48
48
|
"@bobfrankston/tcp-transport": "^0.1.4"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/types.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface ProviderMessage {
|
|
|
43
43
|
export interface FetchOptions {
|
|
44
44
|
source?: boolean;
|
|
45
45
|
providerId?: string;
|
|
46
|
+
/** Lower bound for "since" queries — bounds the result set by date so
|
|
47
|
+
* the default page-count cap doesn't silently truncate a large folder
|
|
48
|
+
* to the last ~200 messages. */
|
|
49
|
+
since?: Date;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* A mail provider that can list folders, fetch messages, and perform actions.
|