@bobfrankston/mailx 1.0.179 → 1.0.180

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.
@@ -0,0 +1,231 @@
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).
5
+ *
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.
9
+ */
10
+ const API = "https://gmail.googleapis.com/gmail/v1/users/me";
11
+ /** Convert Gmail hex ID to integer UID (lower 48 bits) */
12
+ function idToUid(id) {
13
+ const hex = id.length > 12 ? id.slice(-12) : id;
14
+ return parseInt(hex, 16);
15
+ }
16
+ /** Map Gmail label to IMAP-style specialUse */
17
+ function labelSpecialUse(label) {
18
+ switch (label.id) {
19
+ case "INBOX": return "inbox";
20
+ case "SENT": return "sent";
21
+ case "DRAFT": return "drafts";
22
+ case "TRASH": return "trash";
23
+ case "SPAM": return "junk";
24
+ default: return "";
25
+ }
26
+ }
27
+ function getHeader(headers, name) {
28
+ return headers.find(h => h.name.toLowerCase() === name.toLowerCase())?.value || "";
29
+ }
30
+ function parseAddress(raw) {
31
+ const match = raw.match(/^"?([^"<]*?)"?\s*<([^>]+)>/);
32
+ if (match)
33
+ return { name: match[1].trim(), address: match[2].trim() };
34
+ return { address: raw.trim() };
35
+ }
36
+ function parseAddressList(raw) {
37
+ if (!raw)
38
+ return [];
39
+ return raw.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/).map(s => parseAddress(s.trim())).filter(a => a.address);
40
+ }
41
+ export class GmailApiWebProvider {
42
+ tokenProvider;
43
+ constructor(tokenProvider) {
44
+ this.tokenProvider = tokenProvider;
45
+ }
46
+ async apiFetch(path, options = {}) {
47
+ const token = await this.tokenProvider();
48
+ for (let attempt = 0; attempt < 3; attempt++) {
49
+ const res = await globalThis.fetch(`${API}${path}`, {
50
+ ...options,
51
+ headers: {
52
+ "Authorization": `Bearer ${token}`,
53
+ "Content-Type": "application/json",
54
+ ...options.headers,
55
+ },
56
+ });
57
+ if (res.status === 429) {
58
+ const delay = (attempt + 1) * 2000;
59
+ console.log(`[gmail] Rate limited, waiting ${delay / 1000}s...`);
60
+ await new Promise(r => setTimeout(r, delay));
61
+ continue;
62
+ }
63
+ if (!res.ok) {
64
+ const err = await res.text().catch(() => "");
65
+ throw new Error(`Gmail API ${res.status}: ${err.substring(0, 200)}`);
66
+ }
67
+ return res.json();
68
+ }
69
+ throw new Error("Gmail API: rate limited after 3 retries");
70
+ }
71
+ async listFolders() {
72
+ const data = await this.apiFetch("/labels");
73
+ const labels = data.labels || [];
74
+ const folders = [];
75
+ for (const label of labels) {
76
+ if (["UNREAD", "STARRED", "IMPORTANT", "CATEGORY_PERSONAL",
77
+ "CATEGORY_SOCIAL", "CATEGORY_PROMOTIONS", "CATEGORY_UPDATES",
78
+ "CATEGORY_FORUMS", "CHAT"].includes(label.id))
79
+ continue;
80
+ const specialUse = labelSpecialUse(label);
81
+ const path = label.name || label.id;
82
+ const name = path.includes("/") ? path.split("/").pop() : path;
83
+ folders.push({
84
+ path, name, delimiter: "/", specialUse,
85
+ flags: label.type === "system" ? ["\\Noselect"] : [],
86
+ });
87
+ }
88
+ return folders;
89
+ }
90
+ async listMessageIds(query, maxResults = 500) {
91
+ const ids = [];
92
+ let pageToken = "";
93
+ while (true) {
94
+ const params = new URLSearchParams({ q: query, maxResults: String(Math.min(maxResults - ids.length, 500)) });
95
+ if (pageToken)
96
+ params.set("pageToken", pageToken);
97
+ const data = await this.apiFetch(`/messages?${params}`);
98
+ for (const msg of data.messages || []) {
99
+ ids.push(msg.id);
100
+ }
101
+ if (!data.nextPageToken || ids.length >= maxResults)
102
+ break;
103
+ pageToken = data.nextPageToken;
104
+ }
105
+ return ids;
106
+ }
107
+ async batchFetch(ids, options = {}, onChunk) {
108
+ const all = [];
109
+ const chunkSize = options.source ? 10 : 50;
110
+ const format = options.source ? "raw" : "metadata";
111
+ for (let i = 0; i < ids.length; i += chunkSize) {
112
+ const chunk = ids.slice(i, i + chunkSize);
113
+ const messages = [];
114
+ for (const id of chunk) {
115
+ const params = new URLSearchParams({ format });
116
+ if (format === "metadata") {
117
+ for (const h of ["From", "To", "Cc", "Subject", "Message-ID", "Date"]) {
118
+ params.append("metadataHeaders", h);
119
+ }
120
+ }
121
+ messages.push(await this.apiFetch(`/messages/${id}?${params}`));
122
+ }
123
+ const parsed = messages.map(msg => this.parseMessage(msg, options));
124
+ all.push(...parsed);
125
+ if (onChunk)
126
+ onChunk(parsed);
127
+ }
128
+ return all;
129
+ }
130
+ parseMessage(msg, options = {}) {
131
+ const labels = msg.labelIds || [];
132
+ const headers = msg.payload?.headers || [];
133
+ let source = "";
134
+ if (options.source && msg.raw) {
135
+ // URL-safe base64 → standard base64 → decoded string
136
+ const base64 = msg.raw.replace(/-/g, "+").replace(/_/g, "/");
137
+ try {
138
+ source = atob(base64);
139
+ }
140
+ catch {
141
+ // Handle padding issues
142
+ const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
143
+ source = atob(padded);
144
+ }
145
+ }
146
+ const fromRaw = getHeader(headers, "From");
147
+ const toRaw = getHeader(headers, "To");
148
+ const ccRaw = getHeader(headers, "Cc");
149
+ const dateRaw = getHeader(headers, "Date") || "";
150
+ const subject = getHeader(headers, "Subject") || msg.snippet || "";
151
+ const messageId = getHeader(headers, "Message-ID") || "";
152
+ return {
153
+ uid: idToUid(msg.id),
154
+ messageId,
155
+ providerId: msg.id,
156
+ date: dateRaw ? new Date(dateRaw) : (msg.internalDate ? new Date(Number(msg.internalDate)) : null),
157
+ subject,
158
+ from: parseAddressList(fromRaw),
159
+ to: parseAddressList(toRaw),
160
+ cc: parseAddressList(ccRaw),
161
+ seen: !labels.includes("UNREAD"),
162
+ flagged: labels.includes("STARRED"),
163
+ answered: false,
164
+ draft: labels.includes("DRAFT"),
165
+ size: msg.sizeEstimate || 0,
166
+ source,
167
+ };
168
+ }
169
+ async fetchSince(folder, sinceUid, options = {}) {
170
+ const query = `in:${this.folderToLabel(folder)}`;
171
+ const ids = await this.listMessageIds(query, 200);
172
+ const messages = await this.batchFetch(ids, options);
173
+ return messages.filter(m => m.uid > sinceUid);
174
+ }
175
+ async fetchByDate(folder, since, before, options = {}, onChunk) {
176
+ const afterDate = this.formatDate(since);
177
+ const beforeDate = this.formatDate(before);
178
+ const query = `in:${this.folderToLabel(folder)} after:${afterDate} before:${beforeDate}`;
179
+ const ids = await this.listMessageIds(query);
180
+ return this.batchFetch(ids, options, onChunk);
181
+ }
182
+ async fetchByUids(folder, uids, options = {}) {
183
+ const query = `in:${this.folderToLabel(folder)}`;
184
+ const ids = await this.listMessageIds(query);
185
+ const uidSet = new Set(uids);
186
+ const matchingIds = ids.filter(id => uidSet.has(idToUid(id)));
187
+ return this.batchFetch(matchingIds, options);
188
+ }
189
+ async fetchOne(folder, uid, options = {}) {
190
+ const query = `in:${this.folderToLabel(folder)}`;
191
+ const ids = await this.listMessageIds(query, 1000);
192
+ const id = ids.find(id => idToUid(id) === uid);
193
+ if (!id)
194
+ return null;
195
+ const format = options.source ? "raw" : "metadata";
196
+ const params = new URLSearchParams({ format });
197
+ if (format === "metadata") {
198
+ for (const h of ["From", "To", "Cc", "Subject", "Message-ID", "Date"]) {
199
+ params.append("metadataHeaders", h);
200
+ }
201
+ }
202
+ const msg = await this.apiFetch(`/messages/${id}?${params}`);
203
+ return this.parseMessage(msg, options);
204
+ }
205
+ async getUids(folder) {
206
+ const query = `in:${this.folderToLabel(folder)}`;
207
+ const ids = await this.listMessageIds(query, 10000);
208
+ return ids.map(idToUid);
209
+ }
210
+ async close() { }
211
+ folderToLabel(path) {
212
+ const lower = path.toLowerCase();
213
+ if (lower === "inbox")
214
+ return "inbox";
215
+ if (lower === "sent" || lower === "[gmail]/sent mail")
216
+ return "sent";
217
+ if (lower === "drafts" || lower === "[gmail]/drafts")
218
+ return "drafts";
219
+ if (lower === "trash" || lower === "[gmail]/trash")
220
+ return "trash";
221
+ if (lower === "spam" || lower === "[gmail]/spam" || lower === "junk email")
222
+ return "spam";
223
+ if (lower === "archive" || lower === "[gmail]/all mail")
224
+ return "all";
225
+ return `"${path}"`;
226
+ }
227
+ formatDate(d) {
228
+ return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}`;
229
+ }
230
+ }
231
+ //# sourceMappingURL=gmail-api-web.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @bobfrankston/mailx-store-web
3
+ * WebAssembly SQLite + IndexedDB storage for Android/browser.
4
+ * Drop-in replacement for @bobfrankston/mailx-store in non-Node environments.
5
+ */
6
+ export { WebMailxDB } from "./db.js";
7
+ export { WebMessageStore } from "./web-message-store.js";
8
+ export { WebMailxService } from "./web-service.js";
9
+ export { dispatch } from "./web-jsonrpc.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @bobfrankston/mailx-store-web
3
+ * WebAssembly SQLite + IndexedDB storage for Android/browser.
4
+ * Drop-in replacement for @bobfrankston/mailx-store in non-Node environments.
5
+ */
6
+ export { WebMailxDB } from "./db.js";
7
+ export { WebMessageStore } from "./web-message-store.js";
8
+ export { WebMailxService } from "./web-service.js";
9
+ export { dispatch } from "./web-jsonrpc.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@bobfrankston/mailx-store-web",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc"
9
+ },
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@bobfrankston/mailx-types": "file:../mailx-types",
13
+ "sql.js": "^1.14.1"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/BobFrankston/mailx-store-web.git"
18
+ }
19
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Mail provider interface for web (browser/Android).
3
+ * Identical to @bobfrankston/mailx-imap/providers/types.ts.
4
+ * Duplicated here to avoid importing from mailx-imap (which depends on Node.js).
5
+ */
6
+ export interface ProviderFolder {
7
+ path: string;
8
+ name: string;
9
+ delimiter: string;
10
+ specialUse: string;
11
+ flags: string[];
12
+ }
13
+ export interface ProviderMessage {
14
+ uid: number;
15
+ messageId: string;
16
+ providerId: string;
17
+ date: Date | null;
18
+ subject: string;
19
+ from: {
20
+ name?: string;
21
+ address?: string;
22
+ }[];
23
+ to: {
24
+ name?: string;
25
+ address?: string;
26
+ }[];
27
+ cc: {
28
+ name?: string;
29
+ address?: string;
30
+ }[];
31
+ seen: boolean;
32
+ flagged: boolean;
33
+ answered: boolean;
34
+ draft: boolean;
35
+ size: number;
36
+ source: string;
37
+ }
38
+ export interface FetchOptions {
39
+ source?: boolean;
40
+ }
41
+ export interface MailProvider {
42
+ listFolders(): Promise<ProviderFolder[]>;
43
+ fetchSince(folder: string, sinceUid: number, options?: FetchOptions): Promise<ProviderMessage[]>;
44
+ fetchByDate(folder: string, since: Date, before: Date, options?: FetchOptions, onChunk?: (msgs: ProviderMessage[]) => void): Promise<ProviderMessage[]>;
45
+ fetchByUids(folder: string, uids: number[], options?: FetchOptions): Promise<ProviderMessage[]>;
46
+ fetchOne(folder: string, uid: number, options?: FetchOptions): Promise<ProviderMessage | null>;
47
+ getUids(folder: string): Promise<number[]>;
48
+ close(): Promise<void>;
49
+ }
50
+ //# sourceMappingURL=provider-types.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Mail provider interface for web (browser/Android).
3
+ * Identical to @bobfrankston/mailx-imap/providers/types.ts.
4
+ * Duplicated here to avoid importing from mailx-imap (which depends on Node.js).
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=provider-types.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Minimal type declarations for sql.js (WebAssembly SQLite).
3
+ * Only the API surface used by WebMailxDB is typed.
4
+ */
5
+
6
+ declare module "sql.js" {
7
+ interface SqlJsStatic {
8
+ Database: new (data?: ArrayLike<number>) => Database;
9
+ }
10
+
11
+ interface Database {
12
+ run(sql: string, params?: any[]): Database;
13
+ exec(sql: string, params?: any[]): QueryExecResult[];
14
+ export(): Uint8Array;
15
+ close(): void;
16
+ }
17
+
18
+ interface QueryExecResult {
19
+ columns: string[];
20
+ values: any[][];
21
+ }
22
+
23
+ function initSqlJs(config?: {
24
+ locateFile?: (file: string) => string;
25
+ }): Promise<SqlJsStatic>;
26
+
27
+ export default initSqlJs;
28
+ export type { Database, SqlJsStatic, QueryExecResult };
29
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * JSON-RPC dispatcher for WebMailxService.
3
+ * Maps mailxapi action names to WebMailxService method calls.
4
+ * Same dispatch table as desktop jsonrpc.ts but adapted for async settings.
5
+ */
6
+ type ServiceLike = Record<string, (...args: any[]) => any>;
7
+ export interface RpcRequest {
8
+ _action: string;
9
+ _cbid: string;
10
+ [key: string]: any;
11
+ }
12
+ export interface RpcResponse {
13
+ _cbid: string;
14
+ result?: any;
15
+ error?: string;
16
+ }
17
+ /** Dispatch an incoming mailxapi call to the appropriate WebMailxService method */
18
+ export declare function dispatch(svc: ServiceLike, req: RpcRequest): Promise<RpcResponse>;
19
+ export {};
20
+ //# sourceMappingURL=web-jsonrpc.d.ts.map
@@ -0,0 +1,94 @@
1
+ /**
2
+ * JSON-RPC dispatcher for WebMailxService.
3
+ * Maps mailxapi action names to WebMailxService method calls.
4
+ * Same dispatch table as desktop jsonrpc.ts but adapted for async settings.
5
+ */
6
+ /** Dispatch an incoming mailxapi call to the appropriate WebMailxService method */
7
+ export async function dispatch(svc, req) {
8
+ const { _action, _cbid, ...params } = req;
9
+ try {
10
+ const result = await dispatchAction(svc, _action, params);
11
+ return { _cbid, result };
12
+ }
13
+ catch (e) {
14
+ return { _cbid, error: e.message || String(e) };
15
+ }
16
+ }
17
+ async function dispatchAction(svc, action, p) {
18
+ switch (action) {
19
+ case "getAccounts":
20
+ return svc.getAccounts();
21
+ case "getFolders":
22
+ return svc.getFolders(p.accountId);
23
+ case "getMessages":
24
+ return svc.getMessages(p.accountId, p.folderId, p.page, p.pageSize);
25
+ case "getUnifiedInbox":
26
+ return svc.getUnifiedInbox(p.page || 1, p.pageSize || 50);
27
+ case "getMessage":
28
+ return svc.getMessage(p.accountId, p.uid, p.allowRemote, p.folderId);
29
+ case "updateFlags":
30
+ await svc.updateFlags(p.accountId, p.uid, p.flags);
31
+ return { ok: true };
32
+ case "deleteMessage":
33
+ await svc.deleteMessage(p.accountId, p.uid);
34
+ return { ok: true };
35
+ case "deleteMessages":
36
+ await svc.deleteMessages(p.accountId, p.uids);
37
+ return { ok: true, count: p.uids.length };
38
+ case "undeleteMessage":
39
+ await svc.undeleteMessage(p.accountId, p.uid, p.folderId);
40
+ return { ok: true };
41
+ case "moveMessage":
42
+ await svc.moveMessage(p.accountId, p.uid, p.targetFolderId, p.targetAccountId);
43
+ return { ok: true };
44
+ case "moveMessages":
45
+ await svc.moveMessages(p.accountId, p.uids, p.targetFolderId);
46
+ return { ok: true, count: p.uids.length };
47
+ case "markFolderRead":
48
+ svc.markFolderRead(p.folderId);
49
+ return { ok: true };
50
+ case "sendMessage":
51
+ await svc.send(p);
52
+ return { ok: true };
53
+ case "saveDraft":
54
+ return svc.saveDraft(p.accountId, p.subject, p.bodyHtml, p.bodyText, p.to, p.cc, p.previousDraftUid, p.draftId);
55
+ case "deleteDraft":
56
+ await svc.deleteDraft(p.accountId, p.draftUid);
57
+ return { ok: true };
58
+ case "syncAll":
59
+ await svc.syncAll();
60
+ return { ok: true };
61
+ case "syncAccount":
62
+ await svc.syncAccount(p.accountId);
63
+ return { ok: true };
64
+ case "getSyncPending":
65
+ return svc.getSyncPending();
66
+ case "reauthenticate":
67
+ return { ok: await svc.reauthenticate(p.accountId) };
68
+ case "searchMessages":
69
+ return svc.search(p.query, p.page, p.pageSize, p.scope, p.accountId, p.folderId);
70
+ case "searchContacts":
71
+ return svc.searchContacts(p.query);
72
+ case "getSettings":
73
+ return svc.getSettings();
74
+ case "saveSettingsData":
75
+ await svc.saveSettingsData(p);
76
+ return { ok: true };
77
+ case "allowRemoteContent":
78
+ await svc.allowRemoteContent(p.type, p.value);
79
+ return { ok: true };
80
+ case "getVersion":
81
+ return { version: "1.0.0-android", theme: "system", storage: svc.getStorageInfo(), platform: "android" };
82
+ case "getAutocompleteSettings":
83
+ return svc.getAutocompleteSettings();
84
+ case "saveAutocompleteSettings":
85
+ await svc.saveAutocompleteSettings(p);
86
+ return { ok: true };
87
+ case "resetStore":
88
+ await svc.resetStore();
89
+ return { ok: true };
90
+ default:
91
+ throw new Error(`Unknown action: ${action}`);
92
+ }
93
+ }
94
+ //# sourceMappingURL=web-jsonrpc.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * IndexedDB-based message body storage for Android/browser.
3
+ * Mirrors FileMessageStore API from @bobfrankston/mailx-store.
4
+ * Stores RFC 2822 message bodies as blobs keyed by accountId/folderId/uid.
5
+ */
6
+ export declare class WebMessageStore {
7
+ private dbPromise;
8
+ constructor();
9
+ putMessage(accountId: string, folderId: number, uid: number, raw: Uint8Array | ArrayBuffer): Promise<string>;
10
+ getMessage(accountId: string, folderId: number, uid: number): Promise<Uint8Array | null>;
11
+ deleteMessage(accountId: string, folderId: number, uid: number): Promise<void>;
12
+ hasMessage(accountId: string, folderId: number, uid: number): Promise<boolean>;
13
+ /** Clear all stored bodies — used for "Reset Store" */
14
+ clear(): Promise<void>;
15
+ }
16
+ //# sourceMappingURL=web-message-store.d.ts.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * IndexedDB-based message body storage for Android/browser.
3
+ * Mirrors FileMessageStore API from @bobfrankston/mailx-store.
4
+ * Stores RFC 2822 message bodies as blobs keyed by accountId/folderId/uid.
5
+ */
6
+ const DB_NAME = "mailx-bodies";
7
+ const DB_VERSION = 1;
8
+ const STORE_NAME = "messages";
9
+ function openDb() {
10
+ return new Promise((resolve, reject) => {
11
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
12
+ req.onupgradeneeded = () => {
13
+ const db = req.result;
14
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
15
+ db.createObjectStore(STORE_NAME);
16
+ }
17
+ };
18
+ req.onsuccess = () => resolve(req.result);
19
+ req.onerror = () => reject(req.error);
20
+ });
21
+ }
22
+ function makeKey(accountId, folderId, uid) {
23
+ return `${accountId}/${folderId}/${uid}`;
24
+ }
25
+ export class WebMessageStore {
26
+ dbPromise;
27
+ constructor() {
28
+ this.dbPromise = openDb();
29
+ }
30
+ async putMessage(accountId, folderId, uid, raw) {
31
+ const db = await this.dbPromise;
32
+ const key = makeKey(accountId, folderId, uid);
33
+ const data = raw instanceof Uint8Array ? raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength) : raw;
34
+ return new Promise((resolve, reject) => {
35
+ const tx = db.transaction(STORE_NAME, "readwrite");
36
+ tx.objectStore(STORE_NAME).put(data, key);
37
+ tx.oncomplete = () => resolve(key);
38
+ tx.onerror = () => reject(tx.error);
39
+ });
40
+ }
41
+ async getMessage(accountId, folderId, uid) {
42
+ const db = await this.dbPromise;
43
+ const key = makeKey(accountId, folderId, uid);
44
+ return new Promise((resolve, reject) => {
45
+ const tx = db.transaction(STORE_NAME, "readonly");
46
+ const req = tx.objectStore(STORE_NAME).get(key);
47
+ req.onsuccess = () => {
48
+ if (req.result) {
49
+ resolve(new Uint8Array(req.result));
50
+ }
51
+ else {
52
+ resolve(null);
53
+ }
54
+ };
55
+ req.onerror = () => reject(req.error);
56
+ });
57
+ }
58
+ async deleteMessage(accountId, folderId, uid) {
59
+ const db = await this.dbPromise;
60
+ const key = makeKey(accountId, folderId, uid);
61
+ return new Promise((resolve, reject) => {
62
+ const tx = db.transaction(STORE_NAME, "readwrite");
63
+ tx.objectStore(STORE_NAME).delete(key);
64
+ tx.oncomplete = () => resolve();
65
+ tx.onerror = () => reject(tx.error);
66
+ });
67
+ }
68
+ async hasMessage(accountId, folderId, uid) {
69
+ const db = await this.dbPromise;
70
+ const key = makeKey(accountId, folderId, uid);
71
+ return new Promise((resolve, reject) => {
72
+ const tx = db.transaction(STORE_NAME, "readonly");
73
+ const req = tx.objectStore(STORE_NAME).count(IDBKeyRange.only(key));
74
+ req.onsuccess = () => resolve(req.result > 0);
75
+ req.onerror = () => reject(req.error);
76
+ });
77
+ }
78
+ /** Clear all stored bodies — used for "Reset Store" */
79
+ async clear() {
80
+ const db = await this.dbPromise;
81
+ return new Promise((resolve, reject) => {
82
+ const tx = db.transaction(STORE_NAME, "readwrite");
83
+ tx.objectStore(STORE_NAME).clear();
84
+ tx.oncomplete = () => resolve();
85
+ tx.onerror = () => reject(tx.error);
86
+ });
87
+ }
88
+ }
89
+ //# sourceMappingURL=web-message-store.js.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Web-compatible MailxService for Android/browser.
3
+ * Replaces @bobfrankston/mailx-service which depends on Node.js (fs, dns, mailparser).
4
+ *
5
+ * Key differences from desktop:
6
+ * - Uses WebMailxDB (wa-sqlite) instead of MailxDB (node:sqlite)
7
+ * - Uses WebMessageStore (IndexedDB) instead of FileMessageStore (filesystem)
8
+ * - Uses postal-mime or manual header parsing instead of mailparser's simpleParser
9
+ * - Settings via IndexedDB + GDrive API instead of filesystem
10
+ * - No dns.resolveMx — provider detection is static (Gmail/Outlook/Yahoo/iCloud)
11
+ */
12
+ import type { WebMailxDB } from "./db.js";
13
+ import type { WebMessageStore } from "./web-message-store.js";
14
+ import type { Folder, AutocompleteSettings } from "@bobfrankston/mailx-types";
15
+ export declare function sanitizeHtml(html: string): {
16
+ html: string;
17
+ hasRemoteContent: boolean;
18
+ };
19
+ export interface WebSyncManager {
20
+ syncAll(): Promise<void>;
21
+ syncFolders(accountId: string): Promise<Folder[]>;
22
+ syncFolder(accountId: string, folderId: number): Promise<void>;
23
+ fetchMessageBody(accountId: string, folderId: number, uid: number): Promise<Uint8Array | null>;
24
+ updateFlagsLocal(accountId: string, uid: number, folderId: number, flags: string[]): Promise<void>;
25
+ trashMessage(accountId: string, folderId: number, uid: number): Promise<void>;
26
+ trashMessages(accountId: string, messages: {
27
+ uid: number;
28
+ folderId: number;
29
+ }[]): Promise<void>;
30
+ moveMessage(accountId: string, uid: number, folderId: number, targetFolderId: number): Promise<void>;
31
+ moveMessages(accountId: string, messages: {
32
+ uid: number;
33
+ folderId: number;
34
+ }[], targetFolderId: number): Promise<void>;
35
+ moveMessageCrossAccount(accountId: string, uid: number, folderId: number, targetAccountId: string, targetFolderId: number): Promise<void>;
36
+ undeleteMessage(accountId: string, uid: number, folderId: number): Promise<void>;
37
+ queueOutgoingLocal(accountId: string, rawMessage: string): void;
38
+ saveDraft(accountId: string, raw: string, previousDraftUid?: number, draftId?: string): Promise<number | null>;
39
+ deleteDraft(accountId: string, draftUid: number): Promise<void>;
40
+ reauthenticate(accountId: string): Promise<boolean>;
41
+ searchOnServer(accountId: string, folderPath: string, criteria: any): Promise<number[]>;
42
+ syncAllContacts(): Promise<void>;
43
+ addAccount(account: any): Promise<void>;
44
+ on(event: string, handler: (...args: any[]) => void): void;
45
+ emit(event: string, ...args: any[]): void;
46
+ }
47
+ export declare class WebMailxService {
48
+ private db;
49
+ private bodyStore;
50
+ private syncManager;
51
+ constructor(db: WebMailxDB, bodyStore: WebMessageStore, syncManager: WebSyncManager);
52
+ getAccounts(): Promise<any[]>;
53
+ getFolders(accountId: string): Folder[];
54
+ getUnifiedInbox(page?: number, pageSize?: number): any;
55
+ getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string): any;
56
+ getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise<any>;
57
+ updateFlags(accountId: string, uid: number, flags: string[]): Promise<void>;
58
+ allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise<void>;
59
+ search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number): Promise<any>;
60
+ getSyncPending(): {
61
+ pending: number;
62
+ };
63
+ syncAll(): Promise<void>;
64
+ syncAccount(accountId: string): Promise<void>;
65
+ reauthenticate(accountId: string): Promise<boolean>;
66
+ send(msg: any): Promise<void>;
67
+ deleteMessage(accountId: string, uid: number): Promise<void>;
68
+ deleteMessages(accountId: string, uids: number[]): Promise<void>;
69
+ moveMessage(accountId: string, uid: number, targetFolderId: number, targetAccountId?: string): Promise<void>;
70
+ moveMessages(accountId: string, uids: number[], targetFolderId: number): Promise<void>;
71
+ undeleteMessage(accountId: string, uid: number, folderId: number): Promise<void>;
72
+ saveDraft(accountId: string, subject: string, bodyHtml: string, bodyText: string, to?: string, cc?: string, previousDraftUid?: number, draftId?: string): Promise<{
73
+ uid: number | null;
74
+ draftId: string;
75
+ }>;
76
+ deleteDraft(accountId: string, draftUid: number): Promise<void>;
77
+ searchContacts(query: string): any[];
78
+ getSettings(): Promise<any>;
79
+ saveSettingsData(settings: any): Promise<void>;
80
+ getStorageInfo(): {
81
+ provider: string;
82
+ mode: string;
83
+ };
84
+ markFolderRead(folderId: number): void;
85
+ getAutocompleteSettings(): Promise<AutocompleteSettings>;
86
+ saveAutocompleteSettings(settings: AutocompleteSettings): Promise<void>;
87
+ autocomplete(_req: any): Promise<{
88
+ suggestion: string;
89
+ }>;
90
+ resetStore(): Promise<void>;
91
+ }
92
+ //# sourceMappingURL=web-service.d.ts.map