@bobfrankston/mailx-service 0.1.47
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/ai-usage.d.ts +32 -0
- package/ai-usage.d.ts.map +1 -0
- package/ai-usage.js +98 -0
- package/ai-usage.js.map +1 -0
- package/charset.d.ts +15 -0
- package/charset.d.ts.map +1 -0
- package/charset.js +61 -0
- package/charset.js.map +1 -0
- package/db-worker-client.d.ts +32 -0
- package/db-worker-client.d.ts.map +1 -0
- package/db-worker-client.js +66 -0
- package/db-worker-client.js.map +1 -0
- package/db-worker.d.ts +39 -0
- package/db-worker.d.ts.map +1 -0
- package/db-worker.js +115 -0
- package/db-worker.js.map +1 -0
- package/google-sync.d.ts +151 -0
- package/google-sync.d.ts.map +1 -0
- package/google-sync.js +259 -0
- package/google-sync.js.map +1 -0
- package/html-to-docx.d.ts +1 -0
- package/index.d.ts +1102 -0
- package/index.d.ts.map +1 -0
- package/index.js +5476 -0
- package/index.js.map +1 -0
- package/jsonrpc.d.ts +29 -0
- package/jsonrpc.d.ts.map +1 -0
- package/jsonrpc.js +481 -0
- package/jsonrpc.js.map +1 -0
- package/local-store.d.ts +10 -0
- package/local-store.d.ts.map +1 -0
- package/local-store.js +9 -0
- package/local-store.js.map +1 -0
- package/package.json +53 -0
- package/reconciler.d.ts +90 -0
- package/reconciler.d.ts.map +1 -0
- package/reconciler.js +277 -0
- package/reconciler.js.map +1 -0
- package/sync-queue.d.ts +127 -0
- package/sync-queue.d.ts.map +1 -0
- package/sync-queue.js +271 -0
- package/sync-queue.js.map +1 -0
- package/sync-worker-client.d.ts +33 -0
- package/sync-worker-client.d.ts.map +1 -0
- package/sync-worker-client.js +89 -0
- package/sync-worker-client.js.map +1 -0
- package/sync-worker.d.ts +33 -0
- package/sync-worker.d.ts.map +1 -0
- package/sync-worker.js +143 -0
- package/sync-worker.js.map +1 -0
- package/word-html.d.ts +41 -0
- package/word-html.d.ts.map +1 -0
- package/word-html.js +122 -0
- package/word-html.js.map +1 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bobfrankston/mailx-service
|
|
3
|
+
* Pure business logic — no HTTP, no Express.
|
|
4
|
+
* Both the Express API (mailx-api) and the Android bridge call these functions.
|
|
5
|
+
*/
|
|
6
|
+
interface MimeLeafPart {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
filename: string;
|
|
10
|
+
contentType: string;
|
|
11
|
+
decoded: Buffer | null;
|
|
12
|
+
}
|
|
13
|
+
export declare function enumerateMimeLeafParts(raw: string): MimeLeafPart[];
|
|
14
|
+
import { ImapManager } from "@bobfrankston/mailx-imap";
|
|
15
|
+
import { Store } from "@bobfrankston/mailx-store";
|
|
16
|
+
import { type AiUsageSummary } from "./ai-usage.js";
|
|
17
|
+
export { spawnSyncWorker, type SpawnedSyncWorker } from "./sync-worker-client.js";
|
|
18
|
+
import type { Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings, AiTransformRequest, AiTransformResponse, MailxApi } from "@bobfrankston/mailx-types";
|
|
19
|
+
import { type TrustListType } from "@bobfrankston/mailx-types";
|
|
20
|
+
interface ReputationResult {
|
|
21
|
+
flagged: boolean;
|
|
22
|
+
listedCount: number;
|
|
23
|
+
checkedCount: number;
|
|
24
|
+
sources: Array<{
|
|
25
|
+
service: string;
|
|
26
|
+
flagged: boolean;
|
|
27
|
+
verdict: string;
|
|
28
|
+
}>;
|
|
29
|
+
verdict: string;
|
|
30
|
+
service: string;
|
|
31
|
+
/** Services that answered something that is NOT a listing verdict —
|
|
32
|
+
* "query came through a public resolver", "volume limit exceeded",
|
|
33
|
+
* SERVFAIL. Kept out of listedCount/checkedCount so a refusal can never
|
|
34
|
+
* read as a conviction, and carried to the UI/log so the feature can say
|
|
35
|
+
* it is blind rather than silently reporting "clean". */
|
|
36
|
+
unavailable: Array<{
|
|
37
|
+
service: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
export type DnsblOutcome = {
|
|
42
|
+
service: string;
|
|
43
|
+
status: "listed";
|
|
44
|
+
verdict: string;
|
|
45
|
+
} | {
|
|
46
|
+
service: string;
|
|
47
|
+
status: "clean";
|
|
48
|
+
} | {
|
|
49
|
+
service: string;
|
|
50
|
+
status: "unavailable";
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
53
|
+
/** Interpret one A record from a DNSBL. Returns a verdict for a documented
|
|
54
|
+
* listing code, or the reason the answer cannot be read as one. */
|
|
55
|
+
export type DnsblInterpret = (ip: string) => {
|
|
56
|
+
verdict: string;
|
|
57
|
+
} | {
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
60
|
+
export declare const interpretSpamhausDbl: DnsblInterpret;
|
|
61
|
+
/** URIBL multi: 2 black, 4 grey, 8 red. */
|
|
62
|
+
export declare const interpretUribl: DnsblInterpret;
|
|
63
|
+
/** SURBL multi: 8 phishing, 16 malware, 32 abuse, 64 cracked. */
|
|
64
|
+
export declare const interpretSurbl: DnsblInterpret;
|
|
65
|
+
/** Injected popup function — wraps mailx-host's showMessageBox so the
|
|
66
|
+
* service can pop OS-level always-on-top reminder windows without
|
|
67
|
+
* itself depending on mailx-host (keeps the host abstraction clean).
|
|
68
|
+
* Set via setPopupFn in bin/mailx.ts after construction. Returns the
|
|
69
|
+
* label of the button the user clicked, or "closed" / "dismissed" /
|
|
70
|
+
* "" when the window was closed without picking one. */
|
|
71
|
+
export type PopupFn = (opts: any) => Promise<{
|
|
72
|
+
button?: string;
|
|
73
|
+
closed?: boolean;
|
|
74
|
+
dismissed?: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
/** Injected popout-window function — bin/mailx.ts wraps mailx-host's
|
|
77
|
+
* showMessageBoxEx pointed at the loopback popout server, so "open this
|
|
78
|
+
* message in its own OS window" spawns a native msger WebView window
|
|
79
|
+
* instead of falling back to the OS browser. Injection (not import)
|
|
80
|
+
* keeps mailx-service host-agnostic, same as PopupFn. */
|
|
81
|
+
export type PopoutWindowFn = (opts: {
|
|
82
|
+
accountId: string;
|
|
83
|
+
uid: number;
|
|
84
|
+
folderId?: number;
|
|
85
|
+
subject?: string;
|
|
86
|
+
}) => Promise<{
|
|
87
|
+
ok: boolean;
|
|
88
|
+
reason?: string;
|
|
89
|
+
}>;
|
|
90
|
+
/** Injected compose-popout functions — bin/mailx.ts wraps mailx-host's
|
|
91
|
+
* showMessageBoxEx pointed at the popout server's static compose page, so
|
|
92
|
+
* "take this compose into its own OS window" spawns a native msger window.
|
|
93
|
+
* `open` receives the stash id the page will consume; `close` tears the
|
|
94
|
+
* window down after send/discard. Injection keeps mailx-service host-agnostic. */
|
|
95
|
+
export type ComposePopoutFns = {
|
|
96
|
+
open: (opts: {
|
|
97
|
+
id: string;
|
|
98
|
+
subject?: string;
|
|
99
|
+
}) => Promise<{
|
|
100
|
+
ok: boolean;
|
|
101
|
+
reason?: string;
|
|
102
|
+
}>;
|
|
103
|
+
close: (id: string) => void;
|
|
104
|
+
};
|
|
105
|
+
export declare class MailxService implements MailxApi {
|
|
106
|
+
/** The Store is the nexus — owns DB + .eml files + bus + operations.
|
|
107
|
+
* MailxService is the IPC adapter on top: it routes UI requests to
|
|
108
|
+
* Store reads/writes and to the SyncQueue/Reconciler for server-
|
|
109
|
+
* mirror work. The raw `db` getter below is a transitional shim so
|
|
110
|
+
* the ~50 `this.db.X(...)` callsites in this file don't all need
|
|
111
|
+
* touching at once; all writes should migrate to `this.localStore.X(...)`. */
|
|
112
|
+
private store;
|
|
113
|
+
private imapManager;
|
|
114
|
+
private _accountsCache;
|
|
115
|
+
private _contactsCorpusSeeded;
|
|
116
|
+
private _contactsRetries;
|
|
117
|
+
private _settingsCache;
|
|
118
|
+
private popupFn;
|
|
119
|
+
private popoutWindowFn;
|
|
120
|
+
/** Inject the popup implementation. Called once at startup from
|
|
121
|
+
* bin/mailx.ts with mailx-host's showMessageBox. Without this,
|
|
122
|
+
* showReminderPopup returns a "no host" reason. */
|
|
123
|
+
setPopupFn(fn: PopupFn): void;
|
|
124
|
+
/** Inject the popup CLOSER — closes an open reminder popup by its
|
|
125
|
+
* occurrence key (bin keeps the handle registry). Used to retract a
|
|
126
|
+
* popup when the cross-device state sync says another machine already
|
|
127
|
+
* dismissed/snoozed the same reminder. */
|
|
128
|
+
private popupCloserFn;
|
|
129
|
+
setPopupCloserFn(fn: (key: string) => boolean): void;
|
|
130
|
+
closeReminderPopup(key: string): {
|
|
131
|
+
ok: boolean;
|
|
132
|
+
};
|
|
133
|
+
/** Inject the popout-window implementation (native msger window per
|
|
134
|
+
* message). Without this, popoutWindow returns a "no host" reason and
|
|
135
|
+
* the client falls back to its in-window overlay. */
|
|
136
|
+
setPopoutWindowFn(fn: PopoutWindowFn): void;
|
|
137
|
+
private mailtoRegisterFn;
|
|
138
|
+
/** Inject the OS mailto:-handler registration (Settings → "Make rmfmail
|
|
139
|
+
* the default email app"). The registry/xdg work lives in bin/mailx.ts —
|
|
140
|
+
* platform-specific code stays out of this package. */
|
|
141
|
+
setMailtoRegisterFn(fn: () => Promise<{
|
|
142
|
+
ok: boolean;
|
|
143
|
+
message: string;
|
|
144
|
+
}> | {
|
|
145
|
+
ok: boolean;
|
|
146
|
+
message: string;
|
|
147
|
+
}): void;
|
|
148
|
+
registerMailto(): Promise<{
|
|
149
|
+
ok: boolean;
|
|
150
|
+
message: string;
|
|
151
|
+
}>;
|
|
152
|
+
/** Open a message in its own native OS window (msger WebView). The
|
|
153
|
+
* client's 🗗 button lands here via jsonrpc. */
|
|
154
|
+
popoutWindow(accountId: string, uid: number, folderId?: number, subject?: string): Promise<{
|
|
155
|
+
ok: boolean;
|
|
156
|
+
reason?: string;
|
|
157
|
+
}>;
|
|
158
|
+
private composePopoutFns;
|
|
159
|
+
/** Compose-popout init stash, keyed by the id embedded in the popout
|
|
160
|
+
* window's URL. One-shot: consumePopoutComposeInit deletes on read. */
|
|
161
|
+
private composePopoutInits;
|
|
162
|
+
setComposePopoutFns(fns: ComposePopoutFns): void;
|
|
163
|
+
/** Take a live compose into its own native OS window. The overlay hands
|
|
164
|
+
* us its full state (fields, body, attachments, draft identity); we
|
|
165
|
+
* stash it under a one-shot id and spawn the window pointed at the
|
|
166
|
+
* popout server's compose page with that id in the URL. */
|
|
167
|
+
popoutCompose(init: any): Promise<{
|
|
168
|
+
ok: boolean;
|
|
169
|
+
reason?: string;
|
|
170
|
+
}>;
|
|
171
|
+
/** One-shot read of a stashed compose-popout init (popout page boot). */
|
|
172
|
+
consumePopoutComposeInit(id: string): {
|
|
173
|
+
init: any | null;
|
|
174
|
+
};
|
|
175
|
+
/** Fetch a remote image server-side and return it as a data: URI. Used
|
|
176
|
+
* by compose paste: clipboard HTML often references remote images that
|
|
177
|
+
* the editor cannot load (hotlink protection, blocked remote content,
|
|
178
|
+
* auth cookies) — Bob 2026-07-12 pasted a GIF and got the broken-image
|
|
179
|
+
* alt text. Inlining also makes the outgoing mail self-contained. */
|
|
180
|
+
fetchImageAsDataUri(url: string): Promise<{
|
|
181
|
+
dataUri?: string;
|
|
182
|
+
error?: string;
|
|
183
|
+
}>;
|
|
184
|
+
/** Close a compose-popout window (after send / discard in the popout). */
|
|
185
|
+
closeComposePopout(id: string): {
|
|
186
|
+
ok: boolean;
|
|
187
|
+
};
|
|
188
|
+
private mainPopoutFn;
|
|
189
|
+
setMainPopoutFn(fn: (target?: {
|
|
190
|
+
accountId?: string;
|
|
191
|
+
folderId?: number;
|
|
192
|
+
name?: string;
|
|
193
|
+
}) => Promise<{
|
|
194
|
+
ok: boolean;
|
|
195
|
+
reason?: string;
|
|
196
|
+
}>): void;
|
|
197
|
+
/** Open ANOTHER full mailx window (View → New window) — e.g. Drafts in
|
|
198
|
+
* one OS window, Sent in another. Served by the popout server with the
|
|
199
|
+
* HTTP bridge, so all request/response IPC works; push events do not
|
|
200
|
+
* reach secondary windows (no live new-mail update — known limit). */
|
|
201
|
+
popoutMainWindow(target?: {
|
|
202
|
+
accountId?: string;
|
|
203
|
+
folderId?: number;
|
|
204
|
+
name?: string;
|
|
205
|
+
}): Promise<{
|
|
206
|
+
ok: boolean;
|
|
207
|
+
reason?: string;
|
|
208
|
+
}>;
|
|
209
|
+
/** Local-first read/write facade. Every UI IPC handler that touches the
|
|
210
|
+
* local DB or body store goes through this — no awaiting IMAP, no
|
|
211
|
+
* awaiting Gmail API, no awaiting SMTP. See docs/local-first-plan.md. */
|
|
212
|
+
private localStore;
|
|
213
|
+
/** Phase 0 read isolation (docs/clean-architecture.md §5). The DB read
|
|
214
|
+
* surface runs on its OWN thread with its own read-only SQLite handle, so
|
|
215
|
+
* UI list/search/open reads can NEVER be head-of-line-blocked behind a
|
|
216
|
+
* synchronous sync write or a hung IMAP op on the main event loop. Null
|
|
217
|
+
* until the worker has finished its init handshake (a few ms at startup)
|
|
218
|
+
* and null forever if spawning failed — in both cases reads fall back to
|
|
219
|
+
* the in-process `localStore`, same code, same answers. `readReady`
|
|
220
|
+
* resolves once the spawn attempt settles (either outcome). */
|
|
221
|
+
private dbWorker;
|
|
222
|
+
private readReady;
|
|
223
|
+
/** Persistent (and in-memory body-fetch) queue. UI handlers commit
|
|
224
|
+
* locally, then enqueue a server-mirror task here. */
|
|
225
|
+
private syncQueue;
|
|
226
|
+
/** Background loop: drains body-fetch tasks, retries failed message
|
|
227
|
+
* actions, emits sync-state events for the status pill. */
|
|
228
|
+
private reconciler;
|
|
229
|
+
constructor(
|
|
230
|
+
/** The Store is the nexus — owns DB + .eml files + bus + operations.
|
|
231
|
+
* MailxService is the IPC adapter on top: it routes UI requests to
|
|
232
|
+
* Store reads/writes and to the SyncQueue/Reconciler for server-
|
|
233
|
+
* mirror work. The raw `db` getter below is a transitional shim so
|
|
234
|
+
* the ~50 `this.db.X(...)` callsites in this file don't all need
|
|
235
|
+
* touching at once; all writes should migrate to `this.localStore.X(...)`. */
|
|
236
|
+
store: Store, imapManager: ImapManager);
|
|
237
|
+
private _walCheckpointTimer;
|
|
238
|
+
/** Bring up the read-worker. Resolves whether it succeeded or not — on
|
|
239
|
+
* failure `dbWorker` stays null and every read uses `localStore`. */
|
|
240
|
+
private spawnReadWorker;
|
|
241
|
+
/** In-flight read coalescing. A slow daemon makes the UI re-fire the same
|
|
242
|
+
* read (scroll loadMore retries, poll loops, re-render storms): the log
|
|
243
|
+
* showed the IDENTICAL getUnifiedInbox queued dozens of times and all
|
|
244
|
+
* draining at once after a 78s stall. Keyed by method+args, a duplicate
|
|
245
|
+
* request that arrives while one is already running shares that single
|
|
246
|
+
* promise instead of enqueuing another worker job. Same args ⇒ same
|
|
247
|
+
* result, so this is always safe; different args (other pages) key
|
|
248
|
+
* separately. This caps the worker queue at one job per distinct read. */
|
|
249
|
+
private inflightReads;
|
|
250
|
+
/** Route a read through the worker bus if it's up, else run it in-process.
|
|
251
|
+
* A worker-side error (or a dead worker) transparently falls back so a
|
|
252
|
+
* read never fails just because the isolation layer hiccuped. Identical
|
|
253
|
+
* concurrent reads are coalesced (see inflightReads). */
|
|
254
|
+
private read;
|
|
255
|
+
/** Transitional getter — direct DB access from MailxService for the
|
|
256
|
+
* ~50 callsites that haven't yet migrated to Store methods. Future:
|
|
257
|
+
* every mutation routes through `this.localStore.X(...)`. */
|
|
258
|
+
private get db();
|
|
259
|
+
private _contactsFlushTimer;
|
|
260
|
+
private _contactsFlushInFlight;
|
|
261
|
+
private readonly CONTACTS_FLUSH_DEBOUNCE_MS;
|
|
262
|
+
/** Schedule a debounced flush of the local contacts state to GDrive.
|
|
263
|
+
* Multiple changes within the debounce window collapse to one write. */
|
|
264
|
+
markContactsDirty(): void;
|
|
265
|
+
/** Serializes read-modify-write cycles on the cloud contacts.jsonc within
|
|
266
|
+
* this process. Without it, the debounced flush can read the file, an
|
|
267
|
+
* addToDenylist/setPriority write can land, and the flush write-back then
|
|
268
|
+
* clobbers it — the "never suggest didn't stick" bug. Cross-device
|
|
269
|
+
* overlap is handled separately by flushContactsConfig merging instead
|
|
270
|
+
* of overwriting. */
|
|
271
|
+
private _contactsCloudChain;
|
|
272
|
+
private contactsCloudSerial;
|
|
273
|
+
/** Write current DB contacts state to GDrive contacts.jsonc. Called via
|
|
274
|
+
* the debounced timer; also exposed for force-flush on shutdown or
|
|
275
|
+
* after a manual seed. Idempotent — safe to call multiple times. */
|
|
276
|
+
flushContactsConfig(): Promise<void>;
|
|
277
|
+
private flushContactsConfigNow;
|
|
278
|
+
/** Read contacts.jsonc from cloud + apply (preferred + denylist + discovered)
|
|
279
|
+
* into the DB. On first run with no file, seed from message corpus and
|
|
280
|
+
* write a fresh contacts.jsonc to GDrive — that auto-bootstrap is what
|
|
281
|
+
* makes a new device useful immediately on a shared GDrive setup. */
|
|
282
|
+
loadContactsConfig(): Promise<{
|
|
283
|
+
preferred: number;
|
|
284
|
+
discovered: number;
|
|
285
|
+
purged: number;
|
|
286
|
+
conflicts: string[];
|
|
287
|
+
} | null>;
|
|
288
|
+
/** Append an entry to contacts.jsonc#preferred[] and write back to cloud,
|
|
289
|
+
* then re-apply. Mutates the file in place — preserves existing entries
|
|
290
|
+
* and the user's hand-formatting where the parser permits. */
|
|
291
|
+
addPreferredContact(entry: {
|
|
292
|
+
name: string;
|
|
293
|
+
email: string;
|
|
294
|
+
source?: string;
|
|
295
|
+
organization?: string;
|
|
296
|
+
}): Promise<void>;
|
|
297
|
+
/** Return the priority sender / domain index — derived from the most
|
|
298
|
+
* recently loaded contacts.jsonc. Used by the client to flag list
|
|
299
|
+
* rows visually. */
|
|
300
|
+
getPriorityLists(): {
|
|
301
|
+
senders: string[];
|
|
302
|
+
domains: string[];
|
|
303
|
+
};
|
|
304
|
+
/** Mark / unmark a sender as priority. Finds the matching preferred[]
|
|
305
|
+
* entry (by lowercased email), sets `priority: true|false`, and writes
|
|
306
|
+
* contacts.jsonc back to cloud. If no preferred entry exists, one is
|
|
307
|
+
* inserted with the given name. Reloads the in-memory index. */
|
|
308
|
+
setPrioritySender(email: string, value: boolean, name?: string): Promise<void>;
|
|
309
|
+
/** Mark / unmark a domain as priority. Maintained in
|
|
310
|
+
* contacts.jsonc → priorityDomains[] (separate from preferred[]
|
|
311
|
+
* because per-address contacts don't have a domain shape). */
|
|
312
|
+
setPriorityDomain(domain: string, value: boolean): Promise<void>;
|
|
313
|
+
/** Append an email to contacts.jsonc#denylist[] and write back to cloud,
|
|
314
|
+
* then re-apply (which purges any matching discovered rows). */
|
|
315
|
+
addToDenylist(email: string): Promise<void>;
|
|
316
|
+
/** Return accounts from cache — load once, reuse until configChanged. */
|
|
317
|
+
private getCachedAccounts;
|
|
318
|
+
/** Return full settings from cache — load once, reuse until
|
|
319
|
+
* configChanged invalidates. Hot-path callers (getMessage on every
|
|
320
|
+
* preview click, refreshCalendarEvents on every poll) MUST go
|
|
321
|
+
* through here; calling raw `loadSettings()` blocks the event loop
|
|
322
|
+
* with a sync GDrive-mounted readFileSync. See `_settingsCache`
|
|
323
|
+
* comment for the wedge-28-seconds story. */
|
|
324
|
+
private getCachedSettings;
|
|
325
|
+
/** Resolve the reminder sound for the client when an alarm fires.
|
|
326
|
+
* Returns `{mute:true}` for "none", the raw sound bytes (base64 + mime)
|
|
327
|
+
* for a custom file, or `{}` to mean "play the built-in chime" (the
|
|
328
|
+
* default, AND the fallback when a configured file can't be read). The
|
|
329
|
+
* custom path is resolved relative to the config: the LOCAL config dir
|
|
330
|
+
* (~/.rmfmail/<name>, binary-safe, subpaths OK) first, then the shared
|
|
331
|
+
* cloud config folder on Drive (flat, basename only) — so the sound can
|
|
332
|
+
* live next to the JSONC config and follow the user across machines. */
|
|
333
|
+
getReminderSound(): Promise<{
|
|
334
|
+
mute?: boolean;
|
|
335
|
+
dataBase64?: string;
|
|
336
|
+
mime?: string;
|
|
337
|
+
}>;
|
|
338
|
+
private reminderStateCache;
|
|
339
|
+
getReminderState(): Promise<any>;
|
|
340
|
+
/** Merge a device's local state into the shared copy; returns the merged
|
|
341
|
+
* result (push + pull in one round-trip — the client adopts it). */
|
|
342
|
+
mergeReminderState(patch: any): Promise<any>;
|
|
343
|
+
getAccounts(): any[];
|
|
344
|
+
getFolders(accountId: string): Folder[] | Promise<Folder[]>;
|
|
345
|
+
getUnifiedInbox(page?: number, pageSize?: number, flaggedOnly?: boolean, dateBasis?: "sent" | "received"): any;
|
|
346
|
+
getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string, flaggedOnly?: boolean, dateBasis?: "sent" | "received"): any;
|
|
347
|
+
/** UI body read — local-first. Returns immediately from the local cache:
|
|
348
|
+
* `cached: true` with the full parsed body when on disk, or `cached: false`
|
|
349
|
+
* with envelope-only when not. In the cache-miss case we kick off a
|
|
350
|
+
* fire-and-forget IMAP fetch in the background and emit `bodyAvailable`
|
|
351
|
+
* when the body lands; the UI listens for that event and re-requests.
|
|
352
|
+
*
|
|
353
|
+
* No `await imap*` in the click → render path. The 60s body-fetch race
|
|
354
|
+
* and structured `bodyError` shape that lived here previously are gone —
|
|
355
|
+
* step 1 of the local-first refactor (docs/local-first-plan.md). */
|
|
356
|
+
getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise<any>;
|
|
357
|
+
/** Diagnostic accessor — exposed for the sync-status pill / debug UI.
|
|
358
|
+
* Returns the current queue counts so the UI can render
|
|
359
|
+
* "Sync OK / Syncing N items" without polling per-account state. */
|
|
360
|
+
getSyncStatus(): {
|
|
361
|
+
messageActions: number;
|
|
362
|
+
bodyFetches: number;
|
|
363
|
+
};
|
|
364
|
+
/** RFC 8058 one-click unsubscribe: POST `List-Unsubscribe=One-Click` to the
|
|
365
|
+
* HTTPS URL the message's List-Unsubscribe header advertised. Done server-
|
|
366
|
+
* side because the unsubscribe endpoint usually doesn't set CORS headers,
|
|
367
|
+
* so a browser-side fetch would be blocked. */
|
|
368
|
+
unsubscribeOneClick(url: string): Promise<{
|
|
369
|
+
ok: boolean;
|
|
370
|
+
status: number;
|
|
371
|
+
statusText: string;
|
|
372
|
+
}>;
|
|
373
|
+
/** AI spend since `sinceMs` (default: start of this month). Reads the
|
|
374
|
+
* per-call log written by aiTransform. `unpricedCalls` are excluded from
|
|
375
|
+
* `costUsd` — a model with no published price contributes tokens, not a
|
|
376
|
+
* made-up dollar figure. */
|
|
377
|
+
getAiUsage(sinceMs?: number): AiUsageSummary;
|
|
378
|
+
/** Fetch a remote image so the WebView can put it on the clipboard.
|
|
379
|
+
*
|
|
380
|
+
* The client cannot do this itself: an image hosted by a sender is
|
|
381
|
+
* cross-origin and virtually never carries `Access-Control-Allow-Origin`,
|
|
382
|
+
* so `fetch` rejects and a canvas draw of the rendered <img> is tainted.
|
|
383
|
+
* Node has no same-origin policy, so the daemon fetches the bytes and
|
|
384
|
+
* hands them back base64. Only reachable from the viewer's "Copy image"
|
|
385
|
+
* action, and only for a URL the message already rendered.
|
|
386
|
+
*
|
|
387
|
+
* Note this is a deliberate remote request: the caller has already shown
|
|
388
|
+
* the image (remote content allowed for that message), so nothing new is
|
|
389
|
+
* disclosed to the sender beyond the load that already happened. */
|
|
390
|
+
fetchRemoteImage(url: string): Promise<{
|
|
391
|
+
base64: string;
|
|
392
|
+
contentType: string;
|
|
393
|
+
}>;
|
|
394
|
+
/** Per-session map: editId → temp file path, which editor got it, and
|
|
395
|
+
* watcher cleanup. Lives in memory only — cleared when the user closes
|
|
396
|
+
* compose or sends. */
|
|
397
|
+
private wordEdits;
|
|
398
|
+
/** In-flight opens, keyed by editId. Converting a body to .docx and
|
|
399
|
+
* launching Word takes seconds, during which the button looks dead — so
|
|
400
|
+
* the user clicks it again, and again. Each of those clicks used to
|
|
401
|
+
* convert its own timestamped file and spawn its own Word window, and
|
|
402
|
+
* because each open re-armed the watcher on ITS file, only the LAST
|
|
403
|
+
* window's saves came back: editing in one of the earlier windows was
|
|
404
|
+
* silently discarded (Bob 2026-08-09: "it shouldn't spawn multiple
|
|
405
|
+
* instances but just ignore extras"). Extra clicks now join the first
|
|
406
|
+
* open's promise instead of starting a second one. */
|
|
407
|
+
private wordEditOpens;
|
|
408
|
+
/** Hand the current compose body off to Microsoft Word for editing. Writes
|
|
409
|
+
* the HTML to `~/.mailx/external-edit/<editId>.html`, opens it via the
|
|
410
|
+
* default OS handler (Word on Windows when .html is associated; otherwise
|
|
411
|
+
* the user's chosen editor for HTML), and starts an fs.watch that emits
|
|
412
|
+
* `wordEditUpdated` when Word saves. The compose UI listens for that
|
|
413
|
+
* event and reloads the editor.
|
|
414
|
+
*
|
|
415
|
+
* Windows-only by current default — on Mac/Linux there's no equivalent
|
|
416
|
+
* reliable round-trip. The compose toolbar should hide the button on
|
|
417
|
+
* non-win32 platforms. */
|
|
418
|
+
openInWord(editId: string, html: string, popoutId?: string): Promise<{
|
|
419
|
+
ok: boolean;
|
|
420
|
+
path: string;
|
|
421
|
+
opener: string;
|
|
422
|
+
reused?: boolean;
|
|
423
|
+
superseded?: boolean;
|
|
424
|
+
}>;
|
|
425
|
+
/** Re-launch an already-written external-edit file. Same launch commands
|
|
426
|
+
* as the first open; the OS activates the existing window when the
|
|
427
|
+
* document is already loaded. Failure is not fatal — the file is there
|
|
428
|
+
* and the watcher is still armed. */
|
|
429
|
+
private relaunchExternal;
|
|
430
|
+
/** Age out the external-edit scratch directory.
|
|
431
|
+
*
|
|
432
|
+
* Every Edit-in-Word writes a fresh timestamped .docx (the filename is
|
|
433
|
+
* unique per open so a still-locked previous file cannot cause EBUSY),
|
|
434
|
+
* plus Word's ~$ lock files and the autosave sidecar's .ps1. Nothing
|
|
435
|
+
* ever deleted them, so the directory accumulated every document the
|
|
436
|
+
* user had ever edited — copies of real letters, sitting in the clear
|
|
437
|
+
* indefinitely (Bob 2026-08-10: "clean up external edits more than a
|
|
438
|
+
* week old"). A week is comfortably longer than any live editing session
|
|
439
|
+
* and short enough that the folder stops being an archive.
|
|
440
|
+
*
|
|
441
|
+
* Files belonging to a LIVE session are skipped regardless of age — a
|
|
442
|
+
* document open in Word for eight days is still in use. */
|
|
443
|
+
private sweepExternalEdits;
|
|
444
|
+
private openInWordUncached;
|
|
445
|
+
/** Fallback path used when html-to-docx fails. Reproduces the old
|
|
446
|
+
* .html-based flow: writes <editId>.html, watches it, emits the body
|
|
447
|
+
* content on save. User has to Save-As-Web-Page in Word for the file
|
|
448
|
+
* to update — but at least the edit path works. */
|
|
449
|
+
private openExternalHtmlFallback;
|
|
450
|
+
/** Show an OS-level always-on-top reminder popup. Spawns a separate
|
|
451
|
+
* msger window (via the injected popupFn) with the supplied HTML
|
|
452
|
+
* and button list; the chosen button label is returned to the
|
|
453
|
+
* client which then snoozes/dismisses/opens the underlying event.
|
|
454
|
+
* No-op (returns reason) when no popupFn was injected — keeps the
|
|
455
|
+
* service usable in test harnesses without a UI host. */
|
|
456
|
+
showReminderPopup(opts: {
|
|
457
|
+
title: string;
|
|
458
|
+
html: string;
|
|
459
|
+
buttons: string[];
|
|
460
|
+
size?: {
|
|
461
|
+
width: number;
|
|
462
|
+
height: number;
|
|
463
|
+
};
|
|
464
|
+
pos?: {
|
|
465
|
+
x: number;
|
|
466
|
+
y: number;
|
|
467
|
+
};
|
|
468
|
+
/** Occurrence key for the retraction registry (see setPopupCloserFn). */
|
|
469
|
+
popupKey?: string;
|
|
470
|
+
}): Promise<{
|
|
471
|
+
button: string;
|
|
472
|
+
form?: any;
|
|
473
|
+
reason?: string;
|
|
474
|
+
}>;
|
|
475
|
+
/** Read + delete the pending mailto: file (P115). Client calls this
|
|
476
|
+
* on startup so a `mailx --mailto <url>` invocation that spawned us
|
|
477
|
+
* doesn't lose its compose payload to the daemon-fires-before-app-
|
|
478
|
+
* registers race window. Returns null when no pending file is present.
|
|
479
|
+
* The file lives at `~/.rmfmail/pending-mailto.json` and is one-shot —
|
|
480
|
+
* reading it deletes it. */
|
|
481
|
+
consumePendingMailto(): {
|
|
482
|
+
to: string[];
|
|
483
|
+
cc: string[];
|
|
484
|
+
bcc: string[];
|
|
485
|
+
subject: string;
|
|
486
|
+
body: string;
|
|
487
|
+
inReplyTo: string;
|
|
488
|
+
} | null;
|
|
489
|
+
/** Read + delete the pending Windows-share file (C46). Client calls
|
|
490
|
+
* this on startup so a share that spawned the daemon (rmfshare.exe →
|
|
491
|
+
* `node mailx.js`) isn't lost to the fires-before-app-registers race —
|
|
492
|
+
* same contract as consumePendingMailto above. Staged files are
|
|
493
|
+
* returned base64-encoded (the WebView can't read disk) and their
|
|
494
|
+
* staging dirs reaped afterwards. Mirrors bin/share-target.ts, which
|
|
495
|
+
* serves the daemon's live fs.watch path — packages/ can't import
|
|
496
|
+
* bin/, so the two implementations stay in lockstep by hand. */
|
|
497
|
+
consumePendingShare(): {
|
|
498
|
+
title: string;
|
|
499
|
+
text: string;
|
|
500
|
+
url: string;
|
|
501
|
+
attachments: {
|
|
502
|
+
filename: string;
|
|
503
|
+
mimeType: string;
|
|
504
|
+
dataBase64: string;
|
|
505
|
+
}[];
|
|
506
|
+
skipped: {
|
|
507
|
+
name: string;
|
|
508
|
+
reason: string;
|
|
509
|
+
}[];
|
|
510
|
+
} | null;
|
|
511
|
+
/** End external editing. Stops the watcher, removes the temp file.
|
|
512
|
+
* Caller is the compose UI when the user closes the window or sends. */
|
|
513
|
+
closeWordEdit(editId: string): Promise<void>;
|
|
514
|
+
/** Toggle flags. Local DB write + Store event + server-mirror enqueue.
|
|
515
|
+
* No IMAP code path runs in the click→ack window — the queued drain
|
|
516
|
+
* fires 1s later, coalescing rapid toggles. */
|
|
517
|
+
updateFlags(accountId: string, uid: number, flags: string[], folderId?: number): Promise<void>;
|
|
518
|
+
allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise<void>;
|
|
519
|
+
/**
|
|
520
|
+
* Silence the spam note for a sender WITHOUT loading their images.
|
|
521
|
+
*
|
|
522
|
+
* Bob 2026-08-28, on a newsletter he subscribed to: "I don't need the spam
|
|
523
|
+
* reminder but I don't want to say show remote content in order to prevent
|
|
524
|
+
* tracking." Those are two different questions and the allowlist was
|
|
525
|
+
* answering both with one list — the only way to say "this correspondent
|
|
526
|
+
* is fine" was the remote-content banner, which buys a tracking pixel with
|
|
527
|
+
* every vote of confidence.
|
|
528
|
+
*
|
|
529
|
+
* So: `senders`/`domains` mean "load their images, and I trust them";
|
|
530
|
+
* `trustedSenders`/`trustedDomains` mean only the second half. Both feed
|
|
531
|
+
* `senderTrusted` in the store; only the first feeds `allowRemote`.
|
|
532
|
+
* Toggles, so a mistake is one click to undo.
|
|
533
|
+
*/
|
|
534
|
+
trustSenderOrDomain(type: TrustListType, value: string): Promise<{
|
|
535
|
+
trusted: boolean;
|
|
536
|
+
}>;
|
|
537
|
+
/**
|
|
538
|
+
* Read the shared allowlist so the UI can ask "is this sender approved?"
|
|
539
|
+
*
|
|
540
|
+
* The write side (allowRemoteContent / flagSenderOrDomain) already
|
|
541
|
+
* existed; nothing could read it back, so the message list had no way to
|
|
542
|
+
* know a sender had been vouched for. Returns plain arrays — the caller
|
|
543
|
+
* builds whatever index it wants.
|
|
544
|
+
*/
|
|
545
|
+
getAllowlist(): Promise<{
|
|
546
|
+
senders: string[];
|
|
547
|
+
domains: string[];
|
|
548
|
+
recipients: string[];
|
|
549
|
+
flaggedSenders: string[];
|
|
550
|
+
flaggedDomains: string[];
|
|
551
|
+
trustedSenders: string[];
|
|
552
|
+
trustedDomains: string[];
|
|
553
|
+
trustedIntermediaries: string[];
|
|
554
|
+
trustedLists: string[];
|
|
555
|
+
trustedSendersVia: string[];
|
|
556
|
+
}>;
|
|
557
|
+
getUserDict(): Promise<string[]>;
|
|
558
|
+
addUserDictWord(word: string): Promise<string[]>;
|
|
559
|
+
/** Bulk add — used to reconcile a client's localStorage cache up to the
|
|
560
|
+
* cloud file (e.g. words added before the cloud round-trip existed). */
|
|
561
|
+
addUserDictWords(words: string[]): Promise<string[]>;
|
|
562
|
+
removeUserDictWord(word: string): Promise<string[]>;
|
|
563
|
+
/** Domain-reputation cache. Lookups are fast (~50ms each, three in
|
|
564
|
+
* parallel) but we still don't want to redo them on every render of
|
|
565
|
+
* the same sender's mail. Five-minute TTL — long enough that scrolling
|
|
566
|
+
* a folder fans out one query set, short enough that a newly-listed
|
|
567
|
+
* domain surfaces within minutes. */
|
|
568
|
+
private reputationCache;
|
|
569
|
+
private static readonly REPUTATION_TTL_MS;
|
|
570
|
+
private static readonly REPUTATION_TIMEOUT_MS;
|
|
571
|
+
/** Check a domain against three free no-key DNS blocklists in parallel:
|
|
572
|
+
*
|
|
573
|
+
* Spamhaus DBL — `<d>.dbl.spamhaus.org` spam/phish/malware
|
|
574
|
+
* SURBL multi — `<d>.multi.surbl.org` mixed (ph/mw/abuse)
|
|
575
|
+
* URIBL multi — `<d>.multi.uribl.com` black/grey/red lists
|
|
576
|
+
*
|
|
577
|
+
* A DNSBL answers in 127.0.0.0/8, and only SOME of those addresses mean
|
|
578
|
+
* "listed" — the rest are STATUS codes. Treating any A record as a
|
|
579
|
+
* listing is the bug that flagged e.nytimes.com as spam on 2 of 2
|
|
580
|
+
* services (Bob 2026-08-13). From a consumer connection every DBL query
|
|
581
|
+
* comes back `127.255.255.254` ("query via a public/open resolver"),
|
|
582
|
+
* including for google.com and for Spamhaus's own dbltest.com, and
|
|
583
|
+
* URIBL answers `127.0.0.1` ("query refused") the same way — so the old
|
|
584
|
+
* code convicted every domain it looked at. Each service now gets an
|
|
585
|
+
* interpreter that returns a verdict ONLY for a documented listing code;
|
|
586
|
+
* anything else is "unavailable" and never reaches the banner.
|
|
587
|
+
*
|
|
588
|
+
* Each lookup is bounded at 500 ms; missing/slow services are treated
|
|
589
|
+
* as "unknown" (don't poison the cache). Returns the aggregate plus
|
|
590
|
+
* the per-service detail so the UI can show "N of 3 services flag
|
|
591
|
+
* this domain" with the contributing source list.
|
|
592
|
+
*
|
|
593
|
+
* Spamhaus DQS: a free personal key (https://www.spamhaus.com/free-trial/)
|
|
594
|
+
* makes DBL answer from any resolver — queries go to
|
|
595
|
+
* `<domain>.<key>.dbl.dq.spamhaus.net` instead. Set `spamhausDqsKey` in
|
|
596
|
+
* Settings; without it, DBL is simply reported as unavailable rather
|
|
597
|
+
* than guessed at.
|
|
598
|
+
*
|
|
599
|
+
* Privacy: each query leaks the bare domain to that DNSBL's
|
|
600
|
+
* infrastructure plus the user's local resolver. Opt-in via Settings. */
|
|
601
|
+
checkDomainReputation(domain: string): Promise<ReputationResult | null>;
|
|
602
|
+
/** Mark a sender or domain as suspect. Surfaced in the remote-content
|
|
603
|
+
* banner as a red warning on subsequent messages. Toggle: calling with
|
|
604
|
+
* the same value removes it. Returns the new state for UI feedback. */
|
|
605
|
+
flagSenderOrDomain(type: "sender" | "domain", value: string): Promise<{
|
|
606
|
+
flagged: boolean;
|
|
607
|
+
}>;
|
|
608
|
+
/** Monotonic generation for in-flight server searches. A new server
|
|
609
|
+
* search or a cancelServerSearch() call bumps it; the server-search
|
|
610
|
+
* loop checks it between folder batches and bails when superseded. */
|
|
611
|
+
private serverSearchGen;
|
|
612
|
+
/** Abort any in-flight server search. The client calls this when the
|
|
613
|
+
* search box is edited or cleared so a 90-folder IMAP sweep doesn't
|
|
614
|
+
* keep churning for a query the user has already moved on from. */
|
|
615
|
+
cancelServerSearch(): void;
|
|
616
|
+
search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number, includeTrashSpam?: boolean): Promise<any>;
|
|
617
|
+
rebuildSearchIndex(): number;
|
|
618
|
+
getSyncPending(): {
|
|
619
|
+
pending: number;
|
|
620
|
+
};
|
|
621
|
+
/** Outbox queue depth + retry status for the UI status bar. Cheap to call. */
|
|
622
|
+
getOutboxStatus(): any;
|
|
623
|
+
/** Per-account health snapshot: inactivity-timeout count, conn-cap hits,
|
|
624
|
+
* last failed IMAP command. Drives the diagnostics ⚠ badge in the UI. */
|
|
625
|
+
getDiagnostics(): any;
|
|
626
|
+
/** Return the account that supplies `feature` data (calendar / tasks /
|
|
627
|
+
* contacts). Resolution order:
|
|
628
|
+
* 1. Any account with `primary<Feature>: true` (per-feature override)
|
|
629
|
+
* 2. Any account with `primary: true` (catch-all default)
|
|
630
|
+
* 3. First account (fallback)
|
|
631
|
+
* Called without `feature` it returns the catch-all primary — same
|
|
632
|
+
* semantics as the original single-flag version for back-compat. */
|
|
633
|
+
getPrimaryAccount(feature?: string): any;
|
|
634
|
+
/** Feature names that have already emitted authScopeError this session.
|
|
635
|
+
* Stops the "banner flashing on and off continually" loop where every
|
|
636
|
+
* 5-min poll / sidebar nav re-fired the event and the client re-rendered
|
|
637
|
+
* the red banner. Cleared when the user hits Re-authenticate. */
|
|
638
|
+
private scopeErrorEmitted;
|
|
639
|
+
/** Quota cooldown — feature → epoch-ms when the next API call is allowed.
|
|
640
|
+
* Set when Google returns 429 (rate limit / daily-quota exceeded). While
|
|
641
|
+
* cooldown is in effect, getCalendarEvents/getTasks return local DB rows
|
|
642
|
+
* without firing a refresh. Heuristic cooldown is one hour; the daily
|
|
643
|
+
* Google Tasks quota actually resets at Pacific midnight, but a one-hour
|
|
644
|
+
* short-circuit keeps the log clean and avoids hammering after a burst. */
|
|
645
|
+
private quotaCooldown;
|
|
646
|
+
/** Sticky "quota exceeded" emit guard — same shape as scopeErrorEmitted. */
|
|
647
|
+
private quotaErrorEmitted;
|
|
648
|
+
/** In-flight refresh promises keyed by feature, so concurrent UI calls
|
|
649
|
+
* share one Google round-trip instead of stacking N parallel fetches.
|
|
650
|
+
* The fire-and-forget loop where `tasksUpdated` re-triggers `getTasks`
|
|
651
|
+
* used to spawn a new refresh on every event RTT — this dedupes them. */
|
|
652
|
+
private refreshingCalendar;
|
|
653
|
+
private refreshingTasks;
|
|
654
|
+
/** Wall-clock of the last Google refresh per account, for calendar and
|
|
655
|
+
* tasks. The alarm poll calls getCalendarEvents/getTasks every 30 s;
|
|
656
|
+
* without a throttle that hammered Google's APIs and exhausted the
|
|
657
|
+
* Tasks "Queries per day" project quota (Bob 2026-05-16). These gate
|
|
658
|
+
* the actual network refresh to GOOGLE_REFRESH_MIN_INTERVAL_MS — the
|
|
659
|
+
* call still returns local rows instantly, it just doesn't re-pull. */
|
|
660
|
+
private lastCalendarRefresh;
|
|
661
|
+
private lastTasksRefresh;
|
|
662
|
+
/** Delete the cached Google OAuth token (the one used for Calendar / Tasks
|
|
663
|
+
* / Contacts scopes — NOT the IMAP token which `reauthenticate()` handles)
|
|
664
|
+
* and clear the sticky auth-error state so a subsequent refresh can
|
|
665
|
+
* re-trigger browser consent with the current scope set. Equivalent of
|
|
666
|
+
* `mailx -reauth` but callable from the UI. Returns `{ cleared: N }` so
|
|
667
|
+
* the caller can tell the user what happened. */
|
|
668
|
+
reauthGoogleScopes(): {
|
|
669
|
+
cleared: number;
|
|
670
|
+
};
|
|
671
|
+
private primaryTokenProvider;
|
|
672
|
+
/** Return cal events visible in [fromMs..toMs), refreshing from Google
|
|
673
|
+
* in the background. Caller displays local results immediately; after
|
|
674
|
+
* the refresh completes the service emits `calendarUpdated` so the UI
|
|
675
|
+
* re-renders with pulled-in rows. Fire-and-forget-with-event, not
|
|
676
|
+
* fire-and-forget-and-pray. */
|
|
677
|
+
getCalendarEvents(fromMs: number, toMs: number): Promise<any[]>;
|
|
678
|
+
/** Pull from Google NOW, ignoring the 5-minute throttle once. The New
|
|
679
|
+
* event dialog hands the event to Google Calendar in the browser and
|
|
680
|
+
* the reader saves it there; until now the sidebar showed it only when
|
|
681
|
+
* the next throttled refresh happened to run (Bob 2026-09-10: "creating
|
|
682
|
+
* an event should trigger an immediate update for the calendar view").
|
|
683
|
+
* Emits calendarUpdated through the usual path when anything changed.
|
|
684
|
+
* A 20 s floor of its own keeps a focus-flapping window from becoming
|
|
685
|
+
* a Google call per flap — the throttle exists because the alarm poll
|
|
686
|
+
* once burned the Tasks quota, and this must not reopen that door.
|
|
687
|
+
* Returns whether a refresh was actually started. (Claude Code) */
|
|
688
|
+
refreshCalendarNow(): Promise<{
|
|
689
|
+
ok: boolean;
|
|
690
|
+
reason?: string;
|
|
691
|
+
}>;
|
|
692
|
+
/** List the user's *selected* Google calendars (id, display name, color,
|
|
693
|
+
* primary flag) so the sidebar can render one checkbox + icon per
|
|
694
|
+
* calendar. The user curates the set by selecting calendars in Google;
|
|
695
|
+
* mailx only reflects it. Returns [] on quota cooldown / auth failure
|
|
696
|
+
* — the sidebar then just shows no per-calendar controls. */
|
|
697
|
+
getCalendars(): Promise<Array<{
|
|
698
|
+
id: string;
|
|
699
|
+
name: string;
|
|
700
|
+
color: string;
|
|
701
|
+
primary: boolean;
|
|
702
|
+
}>>;
|
|
703
|
+
/** Returns true if the feature is currently in a quota-exceeded cooldown. */
|
|
704
|
+
private inQuotaCooldown;
|
|
705
|
+
/** Single error-handling path for Google refresh failures.
|
|
706
|
+
* Distinguishes 429 (quota) from 401/403 (scope) so each gets the right
|
|
707
|
+
* cooldown + sticky-emit treatment without duplicating the regex blocks. */
|
|
708
|
+
private handleGoogleRefreshError;
|
|
709
|
+
/** Pull events in [fromMs..toMs) from Google, upsert locally, reconcile
|
|
710
|
+
* server-side deletions. Returns true if anything changed so callers
|
|
711
|
+
* can decide whether to emit a refresh event. `changed` is only true
|
|
712
|
+
* when at least one row's data actually differs — without this guard
|
|
713
|
+
* the UI's `calendarUpdated` listener re-triggers `getCalendarEvents`,
|
|
714
|
+
* which fires another `refreshCalendarEvents`, which emits again, etc.
|
|
715
|
+
* Tight loop = 429 quota burn. */
|
|
716
|
+
private refreshCalendarEvents;
|
|
717
|
+
createCalendarEventLocal(ev: {
|
|
718
|
+
title: string;
|
|
719
|
+
startMs: number;
|
|
720
|
+
endMs: number;
|
|
721
|
+
allDay?: boolean;
|
|
722
|
+
location?: string;
|
|
723
|
+
notes?: string;
|
|
724
|
+
/** Google birthday event (all-day, yearly) / not-busy event. Not DB
|
|
725
|
+
* columns: they ride in the store_sync payload, which is what
|
|
726
|
+
* localToCalendarEvent builds the POST from (2026-09-21). */
|
|
727
|
+
birthday?: boolean;
|
|
728
|
+
free?: boolean;
|
|
729
|
+
/** Google calendar id to create on; "primary" when absent. A shared
|
|
730
|
+
* calendar's id is its owner's address or a group id — the sidebar
|
|
731
|
+
* offers the ones Google says the reader can write to (2026-09-10). */
|
|
732
|
+
calendarId?: string;
|
|
733
|
+
}): Promise<string>;
|
|
734
|
+
updateCalendarEventLocal(uuid: string, patch: {
|
|
735
|
+
title?: string;
|
|
736
|
+
startMs?: number;
|
|
737
|
+
endMs?: number;
|
|
738
|
+
allDay?: boolean;
|
|
739
|
+
location?: string;
|
|
740
|
+
notes?: string;
|
|
741
|
+
}): Promise<void>;
|
|
742
|
+
deleteCalendarEventLocal(uuid: string): Promise<void>;
|
|
743
|
+
getTasks(includeCompleted?: boolean): Promise<any[]>;
|
|
744
|
+
private refreshTasks;
|
|
745
|
+
createTaskLocal(t: {
|
|
746
|
+
title: string;
|
|
747
|
+
notes?: string;
|
|
748
|
+
dueMs?: number;
|
|
749
|
+
}): Promise<string>;
|
|
750
|
+
updateTaskLocal(uuid: string, patch: {
|
|
751
|
+
title?: string;
|
|
752
|
+
notes?: string;
|
|
753
|
+
dueMs?: number;
|
|
754
|
+
completedMs?: number;
|
|
755
|
+
}): Promise<void>;
|
|
756
|
+
deleteTaskLocal(uuid: string): Promise<void>;
|
|
757
|
+
/** Drain the store_sync queue — calendar / tasks / contacts push-to-server.
|
|
758
|
+
* Called on every local edit, and on a periodic tick from the outbox worker. */
|
|
759
|
+
drainStoreSync(): Promise<void>;
|
|
760
|
+
/** List queued outgoing messages with parsed envelope headers so the UI
|
|
761
|
+
* can render a pink-row "pending" view before IMAP APPEND succeeds. */
|
|
762
|
+
listQueuedOutgoing(): any[];
|
|
763
|
+
/** Manually drop a queued message (not yet sent). Removes the .ltr file. */
|
|
764
|
+
cancelQueuedOutgoing(filePath: string): {
|
|
765
|
+
ok: true;
|
|
766
|
+
};
|
|
767
|
+
syncAll(): Promise<void>;
|
|
768
|
+
syncAccount(accountId: string): Promise<void>;
|
|
769
|
+
/** Sync ONE folder now. Backs the lazy-folder-sync model (Bob 2026-05-28):
|
|
770
|
+
* the client fires this when the user opens a folder, so an on-demand
|
|
771
|
+
* folder is fresh without the app full-sweeping all 79 folders every
|
|
772
|
+
* 5 minutes. Gmail folders are API-synced; IMAP folders go through
|
|
773
|
+
* syncFolder. Fire-and-forget from the caller's view — the folderSynced
|
|
774
|
+
* event refreshes the list when it lands. */
|
|
775
|
+
syncFolderNow(accountId: string, folderId: number): Promise<void>;
|
|
776
|
+
/** Force re-authentication for an account (deletes token, opens browser consent) */
|
|
777
|
+
reauthenticate(accountId: string): Promise<boolean>;
|
|
778
|
+
send(msg: any): Promise<void>;
|
|
779
|
+
/** Trash a single message. Local-first: local row moves to Trash (or
|
|
780
|
+
* expunges if already there); the server mirror is queued. */
|
|
781
|
+
deleteMessage(accountId: string, uid: number, folderId?: number): Promise<void>;
|
|
782
|
+
/** Bulk trash. Same shape as deleteMessage but loops; each row gets its
|
|
783
|
+
* own pending-delete flag, local move/expunge, and queue entry. */
|
|
784
|
+
deleteMessages(accountId: string, uids: number[], folderIds?: number[]): Promise<void>;
|
|
785
|
+
/** Move a message to another folder. Same-account moves go through the
|
|
786
|
+
* Store + queue. Cross-account moves still synchronously bridge two
|
|
787
|
+
* IMAP connections (rare; local-first violation noted as future work). */
|
|
788
|
+
moveMessage(accountId: string, uid: number, targetFolderId: number, targetAccountId?: string): Promise<void>;
|
|
789
|
+
/** Bulk COPY: the messages stay where they are; a copy is queued to the
|
|
790
|
+
* target folder (server APPEND via the sync queue). No local row is
|
|
791
|
+
* moved/removed — the target copy is imported when its folder syncs
|
|
792
|
+
* (the copy action triggers that). folderIds[] disambiguates each uid to
|
|
793
|
+
* its source folder, index-aligned with uids (same contract as
|
|
794
|
+
* deleteMessages). */
|
|
795
|
+
copyMessages(accountId: string, uids: number[], folderIds: number[] | undefined, targetFolderId: number): Promise<void>;
|
|
796
|
+
/** Bulk move. Loops the single-move pattern under a withBatch so the
|
|
797
|
+
* one folder-counts re-render fires once per affected folder, not per
|
|
798
|
+
* message. */
|
|
799
|
+
moveMessages(accountId: string, uids: number[], targetFolderId: number, folderIds?: number[]): Promise<void>;
|
|
800
|
+
/** Move messages to the account's configured spam folder (accounts.jsonc "spam" path).
|
|
801
|
+
* Throws if the account has no spam folder configured or the folder doesn't exist locally. */
|
|
802
|
+
markAsSpamMessages(accountId: string, uids: number[], folderIds?: number[]): Promise<{
|
|
803
|
+
targetFolderId: number;
|
|
804
|
+
moved: number;
|
|
805
|
+
}>;
|
|
806
|
+
/** Append a spam report row to `~/.mailx/spam.csv` — placeholder mechanism
|
|
807
|
+
* per user 2026-04-23 ("let's make it smart later; no auto-delete until
|
|
808
|
+
* safety issues are addressed"). One row per click. Columns: timestamp
|
|
809
|
+
* (ms since epoch), ISO date, ISO time, accountId, Delivered-To, From
|
|
810
|
+
* address, Subject, eml file path. CSV fields RFC 4180-quoted so commas
|
|
811
|
+
* and quotes in subjects survive. No move, no flag change, no server
|
|
812
|
+
* hit — just the log. Useful as training data for a future classifier.
|
|
813
|
+
*/
|
|
814
|
+
recordSpamReport(accountId: string, uid: number, folderId: number): Promise<{
|
|
815
|
+
ok: true;
|
|
816
|
+
row: string;
|
|
817
|
+
}>;
|
|
818
|
+
/** Restore from Trash. Local-first: move the row back, clear tombstone
|
|
819
|
+
* and pending-delete flag, then either retract the still-queued MOVE
|
|
820
|
+
* to trash or queue a counter-MOVE (trash → original). */
|
|
821
|
+
undeleteMessage(accountId: string, uid: number, folderId: number): Promise<void>;
|
|
822
|
+
deleteOnServer(accountId: string, folderPath: string, uid: number): Promise<void>;
|
|
823
|
+
createFolder(accountId: string, parentPath: string, name: string): Promise<void>;
|
|
824
|
+
/** Rename a folder in place, or — when `newParentId` is given — reparent it.
|
|
825
|
+
* Both are a single server-side mailbox rename (no copy-and-delete). Path
|
|
826
|
+
* computation, guard rails (special-use refusal, name validation, collision
|
|
827
|
+
* / self-descendant checks), the API-vs-IMAP branch, and the local
|
|
828
|
+
* folder-row + descendant-prefix rewrite all live in ImapManager (on the
|
|
829
|
+
* sync worker) so this stays a thin delegate. */
|
|
830
|
+
renameFolder(accountId: string, folderId: number, newName: string, newParentId?: number): Promise<void>;
|
|
831
|
+
deleteFolder(accountId: string, folderId: number): Promise<void>;
|
|
832
|
+
/** Move a folder into the account's Trash. Default delete action; user
|
|
833
|
+
* has to opt in to permanent removal via Shift+Delete (which routes
|
|
834
|
+
* to `deleteFolder` above).
|
|
835
|
+
*
|
|
836
|
+
* Strategy:
|
|
837
|
+
* - IMAP RENAME `<path>` → `<trashPath><delim><name>`. Most servers
|
|
838
|
+
* bring the message contents and any subfolders along automatically.
|
|
839
|
+
* - Name collision in Trash → append " (YYYY-MM-DD)", then a counter
|
|
840
|
+
* " (YYYY-MM-DD 2)", ... up to a sane cap.
|
|
841
|
+
* - RENAME rejected (server forbids subfolders under Trash, e.g. some
|
|
842
|
+
* Dovecot configs flag Trash \Noinferiors) → fall back to moving the
|
|
843
|
+
* folder's messages into Trash root via the existing trash path,
|
|
844
|
+
* then `mailboxDelete` on the now-empty folder. Children lost in
|
|
845
|
+
* that fallback get the same treatment recursively.
|
|
846
|
+
* - Folder is itself Trash, or already inside Trash → throw (caller
|
|
847
|
+
* should detect and route to `deleteFolder` instead).
|
|
848
|
+
*/
|
|
849
|
+
moveFolderToTrash(accountId: string, folderId: number): Promise<void>;
|
|
850
|
+
markFolderRead(folderId: number): void;
|
|
851
|
+
emptyFolder(accountId: string, folderId: number): Promise<void>;
|
|
852
|
+
getAttachment(accountId: string, uid: number, attachmentId: number, folderId?: number): Promise<{
|
|
853
|
+
content: Buffer;
|
|
854
|
+
contentType: string;
|
|
855
|
+
filename: string;
|
|
856
|
+
}>;
|
|
857
|
+
/** Delete attachment(s) from a RECEIVED message — local-first message
|
|
858
|
+
* surgery. The raw .eml is rewritten without the selected MIME parts
|
|
859
|
+
* (original kept next to it as .pre-strip.bak), the DB row updates, and
|
|
860
|
+
* a `replaceMessage` sync action queues the server-side swap, which
|
|
861
|
+
* appends the modified copy and only deletes the original after the
|
|
862
|
+
* server confirms the append (never the other order). IMAP accounts
|
|
863
|
+
* only for now — Gmail's REST path needs its own insert+trash pass.
|
|
864
|
+
* `ids` are the viewer's chip ids (= mailparser attachment indexes);
|
|
865
|
+
* "all" means every chip-visible attachment. Every strip is verified by
|
|
866
|
+
* re-parsing the result before anything is written. */
|
|
867
|
+
deleteAttachments(accountId: string, uid: number, folderId: number | undefined, ids: number[] | "all"): Promise<{
|
|
868
|
+
ok: boolean;
|
|
869
|
+
removed: number;
|
|
870
|
+
remaining: number;
|
|
871
|
+
}>;
|
|
872
|
+
/** Raw RFC 822 source of a message — for "Save message as .eml". Reads the
|
|
873
|
+
* on-disk body file (same resolution as getAttachment), falling back to a
|
|
874
|
+
* server fetch. Returned base64 so it survives the IPC JSON channel. */
|
|
875
|
+
getMessageSource(accountId: string, uid: number, folderId?: number): Promise<{
|
|
876
|
+
dataBase64: string;
|
|
877
|
+
filename: string;
|
|
878
|
+
}>;
|
|
879
|
+
/** Save an attachment to a local temp dir and open it with the OS default
|
|
880
|
+
* application. The desktop UI uses this instead of a browser download —
|
|
881
|
+
* a programmatic `<a download>` click is silently dropped inside msger's
|
|
882
|
+
* WebView2, so the open must happen in the Node process (same pattern as
|
|
883
|
+
* openInWord / openLocalPath). Cross-platform: start / open / xdg-open. */
|
|
884
|
+
openAttachment(accountId: string, uid: number, attachmentId: number, folderId?: number, filename?: string): Promise<{
|
|
885
|
+
ok: boolean;
|
|
886
|
+
path: string;
|
|
887
|
+
}>;
|
|
888
|
+
/** Open a file the user is composing WITH — an attachment sitting in the
|
|
889
|
+
* compose window that has never been in a message. The bytes come from
|
|
890
|
+
* the client (it already holds them for the send), so there is nothing
|
|
891
|
+
* to fetch; everything after that is identical to opening a received
|
|
892
|
+
* attachment, which is why both go through stageAndOpenFile. */
|
|
893
|
+
openPendingAttachment(filename: string, mimeType: string, dataBase64: string): Promise<{
|
|
894
|
+
ok: boolean;
|
|
895
|
+
path: string;
|
|
896
|
+
}>;
|
|
897
|
+
/** Stage bytes to the attachment temp dir and hand them to the OS opener.
|
|
898
|
+
* Shared by received attachments and compose attachments — the collision
|
|
899
|
+
* handling and Mark-of-the-Web tagging below are subtle enough that a
|
|
900
|
+
* second copy of them would drift. */
|
|
901
|
+
private stageAndOpenFile;
|
|
902
|
+
/** Open an http/https/mailto URL in the OS default browser/handler from the
|
|
903
|
+
* Node process. The UI used to rely on `window.open(url, "_blank")`, but
|
|
904
|
+
* inside msger's WebView2 that opens a local in-app window (or no-ops)
|
|
905
|
+
* rather than the system browser — so clicking a link in a message, or an
|
|
906
|
+
* unsubscribe "click here", "stayed local" (Bob 2026-06-13, "old bug
|
|
907
|
+
* back"). `mailxapi.openExternal` now routes here.
|
|
908
|
+
*
|
|
909
|
+
* Security: the URL comes from untrusted email. We (1) parse it and allow
|
|
910
|
+
* ONLY http/https/mailto — no file:, javascript:, data:, etc. — and (2)
|
|
911
|
+
* pass it as a single spawn ARG with no shell, and on Windows use rundll32
|
|
912
|
+
* (not `cmd /c start`, which re-parses `&`/`^` in query strings and is
|
|
913
|
+
* injection-prone). Same cross-platform spawn pattern as openAttachment. */
|
|
914
|
+
openExternal(url: string): Promise<{
|
|
915
|
+
ok: boolean;
|
|
916
|
+
reason?: string;
|
|
917
|
+
}>;
|
|
918
|
+
saveDraft(accountId: string, subject: string, bodyHtml: string, bodyText: string, to?: string, cc?: string, previousDraftUid?: number, draftId?: string, bcc?: string, from?: string, attachments?: {
|
|
919
|
+
filename: string;
|
|
920
|
+
mimeType: string;
|
|
921
|
+
dataBase64: string;
|
|
922
|
+
}[]): Promise<{
|
|
923
|
+
draftUid: number | null;
|
|
924
|
+
draftId: string;
|
|
925
|
+
}>;
|
|
926
|
+
deleteDraft(accountId: string, draftUid: number, draftId?: string): Promise<void>;
|
|
927
|
+
/** Compose-open probe: is there a NEWER copy of this draft on the server
|
|
928
|
+
* (edited on another machine) than the row the user just opened? Best
|
|
929
|
+
* effort — network/auth failures return null and compose proceeds with
|
|
930
|
+
* the local copy (local-first: this never blocks the compose window
|
|
931
|
+
* from opening; the client fires it in the background). */
|
|
932
|
+
checkDraftNewer(accountId: string, draftId: string, sinceUid: number): Promise<{
|
|
933
|
+
uid: number;
|
|
934
|
+
folderId: number;
|
|
935
|
+
} | null>;
|
|
936
|
+
searchContacts(query: string): any[];
|
|
937
|
+
/** Q49: boolean hint for compose to auto-expand Cc when replying to this
|
|
938
|
+
* address. True when at least one past sent message to the same recipient
|
|
939
|
+
* had a non-empty Cc field. */
|
|
940
|
+
hasCcHistoryTo(email: string): boolean;
|
|
941
|
+
/** Q49: same shape, for Bcc. Sent folder is the only place Bcc appears,
|
|
942
|
+
* so the signal is local-only but still reflects the user's habit. */
|
|
943
|
+
hasBccHistoryTo(email: string): boolean;
|
|
944
|
+
syncGoogleContacts(): Promise<void>;
|
|
945
|
+
seedContacts(): Promise<number>;
|
|
946
|
+
/** Explicit add to address book — used by the right-click "Add to contacts"
|
|
947
|
+
* action on From/To/Cc addresses in the message viewer. Just calls the same
|
|
948
|
+
* validated upsert path as recordSentAddress. */
|
|
949
|
+
addContact(name: string, email: string): boolean;
|
|
950
|
+
/** Address-book listing — paginated, filterable. */
|
|
951
|
+
listContacts(query: string, page?: number, pageSize?: number): any;
|
|
952
|
+
/** Upsert a contact from the address book UI (edit name). Two-way cache:
|
|
953
|
+
* commits locally, queues a Google People push. */
|
|
954
|
+
upsertContact(name: string, email: string, fields?: Record<string, string>): {
|
|
955
|
+
ok: true;
|
|
956
|
+
};
|
|
957
|
+
/** Delete a contact from the address book. Also pushes the deletion to
|
|
958
|
+
* Google People if the contact had a resourceName (i.e. was synced). */
|
|
959
|
+
deleteContact(email: string): {
|
|
960
|
+
ok: true;
|
|
961
|
+
};
|
|
962
|
+
/** Open a configured local path in the OS file explorer. Whitelisted to
|
|
963
|
+
* avoid the UI poking at arbitrary paths. */
|
|
964
|
+
/** Open an absolute file path in the OS default *text* editor.
|
|
965
|
+
* Distinct from "open with the file's associated app" — for a .eml
|
|
966
|
+
* that would usually be Outlook / a mail client; the user wants a
|
|
967
|
+
* plain-text viewer like Notepad / TextEdit / xdg's text handler.
|
|
968
|
+
*
|
|
969
|
+
* Cross-platform strategy:
|
|
970
|
+
* - Windows: `notepad.exe <path>`. Always present since Windows 95.
|
|
971
|
+
* Notepad reads any file regardless of extension.
|
|
972
|
+
* - macOS: `open -t <path>` — the `-t` flag explicitly routes to the
|
|
973
|
+
* user's default text editor (TextEdit or whatever they've set).
|
|
974
|
+
* - Linux: try `$VISUAL` / `$EDITOR` first (user's stated preference),
|
|
975
|
+
* then fall back to `xdg-open` with a `.txt` symlink so xdg picks
|
|
976
|
+
* the text MIME handler rather than the .eml handler.
|
|
977
|
+
*
|
|
978
|
+
* Path whitelist: must live under the configured store path or
|
|
979
|
+
* ~/.rmfmail, so the IPC can't be coerced into opening arbitrary
|
|
980
|
+
* files (e.g., system passwords).
|
|
981
|
+
*/
|
|
982
|
+
openInTextEditor(filePath: string): Promise<{
|
|
983
|
+
ok: boolean;
|
|
984
|
+
opener: string;
|
|
985
|
+
reason?: string;
|
|
986
|
+
}>;
|
|
987
|
+
openLocalPath(which: "config" | "log"): Promise<{
|
|
988
|
+
ok: true;
|
|
989
|
+
path: string;
|
|
990
|
+
}>;
|
|
991
|
+
/** Get all messages in a thread (across folders) for an account. */
|
|
992
|
+
getThreadMessages(accountId: string, threadId: string): any;
|
|
993
|
+
/** Read a JSONC config file from the shared cloud dir or local ~/.mailx.
|
|
994
|
+
* Names are whitelisted so the UI can't read arbitrary files.
|
|
995
|
+
* `config.jsonc` is the local per-machine config (not cloud-synced). */
|
|
996
|
+
readJsoncFile(name: string): Promise<string | null>;
|
|
997
|
+
formatJsonc(content: string): Promise<string>;
|
|
998
|
+
/** Return the help markdown for a named config file. Prefers a per-file
|
|
999
|
+
* doc (`<name>.md` minus the `.jsonc` suffix — e.g. `contacts.md` for
|
|
1000
|
+
* `contacts.jsonc`) shipped in the package's `docs/` dir; falls back to
|
|
1001
|
+
* the legacy `config-help.md`-with-sections format if the per-file
|
|
1002
|
+
* isn't available. */
|
|
1003
|
+
readConfigHelp(name: string): Promise<string>;
|
|
1004
|
+
/** Write a JSONC config file. Validates that the content parses as JSONC
|
|
1005
|
+
* (loosely — strips comments/trailing commas) before writing.
|
|
1006
|
+
* Saves the prior content to a dated backup file first — manual edits
|
|
1007
|
+
* occasionally have typos that survive validation (semantically wrong
|
|
1008
|
+
* but syntactically OK), and a one-key undo isn't enough; the user
|
|
1009
|
+
* asked to be able to recover yesterday's accounts.jsonc. Automatic
|
|
1010
|
+
* saveAccounts/saveAllowlist paths skip backups (they're driven by
|
|
1011
|
+
* trusted code, not the JSONC editor). */
|
|
1012
|
+
writeJsoncFile(name: string, content: string): Promise<void>;
|
|
1013
|
+
/** Read the current content of a config file (cloud or local) so it can
|
|
1014
|
+
* be saved as a backup before being overwritten. Returns null if the
|
|
1015
|
+
* file doesn't exist yet (first save — nothing to back up). */
|
|
1016
|
+
private readJsoncForBackup;
|
|
1017
|
+
/** Write the prior content to `<configDir>/backup/<name>.<ts>.bak` and
|
|
1018
|
+
* prune so at most 10 backups per file remain AND none are older than 7
|
|
1019
|
+
* days. Skipped when previous content is null (first write) or
|
|
1020
|
+
* identical to the new content (no-op save). */
|
|
1021
|
+
private backupJsoncIfChanged;
|
|
1022
|
+
getSettings(): any;
|
|
1023
|
+
saveSettings(settings: any): Promise<void>;
|
|
1024
|
+
getStorageInfo(): {
|
|
1025
|
+
provider: string;
|
|
1026
|
+
mode: string;
|
|
1027
|
+
cloudPath?: string;
|
|
1028
|
+
folderName?: string;
|
|
1029
|
+
folderId?: string;
|
|
1030
|
+
configDir?: string;
|
|
1031
|
+
cloudError?: string;
|
|
1032
|
+
};
|
|
1033
|
+
/** True when the writer DB handle came up read-only (the OS refused
|
|
1034
|
+
* read-write at open and SQLite silently downgraded it — see
|
|
1035
|
+
* MailxDB.openWritable). Nothing the user does will be saved, so the
|
|
1036
|
+
* client turns this into a sticky fatal banner with a Restart button
|
|
1037
|
+
* rather than letting every action fail one confusing error at a time. */
|
|
1038
|
+
isDbWriteDenied(): boolean;
|
|
1039
|
+
setupAccount(name: string, email: string, password?: string): Promise<{
|
|
1040
|
+
ok: boolean;
|
|
1041
|
+
error?: string;
|
|
1042
|
+
message?: string;
|
|
1043
|
+
}>;
|
|
1044
|
+
repairAccounts(): Promise<{
|
|
1045
|
+
ok: boolean;
|
|
1046
|
+
error?: string;
|
|
1047
|
+
message?: string;
|
|
1048
|
+
}>;
|
|
1049
|
+
getAutocompleteSettings(): AutocompleteSettings;
|
|
1050
|
+
saveAutocompleteSettings(settings: AutocompleteSettings): void;
|
|
1051
|
+
autocomplete(req: AutocompleteRequest): Promise<AutocompleteResponse>;
|
|
1052
|
+
/** Generic AI text transform — translate / proofread / summarize.
|
|
1053
|
+
* Shares the autocomplete provider config (provider, key, model). Each
|
|
1054
|
+
* feature has its own opt-in toggle (translateEnabled / proofreadEnabled),
|
|
1055
|
+
* default false. Returns empty text + reason when disabled or on error. */
|
|
1056
|
+
aiTransform(req: AiTransformRequest): Promise<AiTransformResponse>;
|
|
1057
|
+
private harperInit;
|
|
1058
|
+
private static readonly EMPTY_LINTS;
|
|
1059
|
+
grammarLint(blocks: string[]): Promise<{
|
|
1060
|
+
blocks: {
|
|
1061
|
+
start: number;
|
|
1062
|
+
end: number;
|
|
1063
|
+
kind: string;
|
|
1064
|
+
message: string;
|
|
1065
|
+
suggestions: string[];
|
|
1066
|
+
}[][];
|
|
1067
|
+
}>;
|
|
1068
|
+
/** Event extraction via the interface `gcal add -clip` uses — keep the
|
|
1069
|
+
* prompt and model IN SYNC with Y:/dev/utils/cardx/gcal/glib/aihelper.ts
|
|
1070
|
+
* (EVENT_EXTRACTION_PROMPT / extractEventsFromText). Key sources, in
|
|
1071
|
+
* order: accounts.jsonc `keys.anthropic`, ANTHROPIC_API_KEY env, the
|
|
1072
|
+
* gcards shared key file (%APPDATA%/gcards/keys.env — same file gcal
|
|
1073
|
+
* and gcards read). Returns null ONLY when no key is available, which
|
|
1074
|
+
* lets aiTransform fall through to the configured provider. */
|
|
1075
|
+
private extractEventGcalStyle;
|
|
1076
|
+
sendMessage(msg: any): Promise<void>;
|
|
1077
|
+
searchMessages(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number, includeTrashSpam?: boolean): Promise<any>;
|
|
1078
|
+
createCalendarEvent(ev: any): Promise<{
|
|
1079
|
+
uuid: string;
|
|
1080
|
+
}>;
|
|
1081
|
+
updateCalendarEvent(uuid: string, patch: any): Promise<{
|
|
1082
|
+
ok: true;
|
|
1083
|
+
}>;
|
|
1084
|
+
deleteCalendarEvent(uuid: string): Promise<{
|
|
1085
|
+
ok: true;
|
|
1086
|
+
}>;
|
|
1087
|
+
createTask(t: {
|
|
1088
|
+
title: string;
|
|
1089
|
+
notes?: string;
|
|
1090
|
+
dueMs?: number;
|
|
1091
|
+
}): Promise<{
|
|
1092
|
+
uuid: string;
|
|
1093
|
+
}>;
|
|
1094
|
+
updateTask(uuid: string, patch: any): Promise<{
|
|
1095
|
+
ok: true;
|
|
1096
|
+
}>;
|
|
1097
|
+
deleteTask(uuid: string): Promise<{
|
|
1098
|
+
ok: true;
|
|
1099
|
+
}>;
|
|
1100
|
+
saveSettingsData(settings: any): Promise<void>;
|
|
1101
|
+
}
|
|
1102
|
+
//# sourceMappingURL=index.d.ts.map
|