/** * Web-compatible MailxService for Android/browser. * Replaces @bobfrankston/mailx-service which depends on Node.js (fs, dns, mailparser). * * Key differences from desktop: * - Uses WebMailxDB (wa-sqlite) instead of MailxDB (node:sqlite) * - Uses WebMessageStore (IndexedDB) instead of FileMessageStore (filesystem) * - Uses postal-mime or manual header parsing instead of mailparser's simpleParser * - Settings via IndexedDB + GDrive API instead of filesystem * - No dns.resolveMx — provider detection is static (Gmail/Outlook/Yahoo/iCloud) */ import type { WebMailxDB } from "./db.js"; import type { WebMessageStore } from "./web-message-store.js"; import type { Folder, AutocompleteSettings, MailxApi } from "@bobfrankston/mailx-types"; import { type ReminderState } from "@bobfrankston/mailx-types"; export interface WebSyncManager { syncAll(): Promise; syncFolders(accountId: string): Promise; syncFolder(accountId: string, folderId: number): Promise; fetchMessageBody(accountId: string, folderId: number, uid: number): Promise; updateFlagsLocal(accountId: string, uid: number, folderId: number, flags: string[]): Promise; trashMessage(accountId: string, folderId: number, uid: number): Promise; trashMessages(accountId: string, messages: { uid: number; folderId: number; }[]): Promise; moveMessage(accountId: string, uid: number, folderId: number, targetFolderId: number): Promise; moveMessages(accountId: string, messages: { uid: number; folderId: number; }[], targetFolderId: number): Promise; moveMessageCrossAccount(accountId: string, uid: number, folderId: number, targetAccountId: string, targetFolderId: number): Promise; undeleteMessage(accountId: string, uid: number, folderId: number): Promise; queueOutgoingLocal(accountId: string, rawMessage: string): void | Promise; saveDraft(accountId: string, raw: string, previousDraftUid?: number, draftId?: string): Promise; deleteDraft(accountId: string, draftUid: number): Promise; reauthenticate(accountId: string): Promise; searchOnServer(accountId: string, folderPath: string, criteria: any): Promise; syncAllContacts(): Promise; addAccount(account: any): Promise; on(event: string, handler: (...args: any[]) => void): void; emit(event: string, ...args: any[]): void; } export declare class WebMailxService implements MailxApi { private db; private bodyStore; private syncManager; constructor(db: WebMailxDB, bodyStore: WebMessageStore, syncManager: WebSyncManager); getAccounts(): Promise; getFolders(accountId: string): Folder[]; getUnifiedInbox(page?: number, pageSize?: number): any; getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string): any; getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise; /** Flag (or unflag) a sender / domain. Mirrors the desktop service so * the viewer's right-click "Flag sender" / "Flag domain" buttons work * on phone — was a no-op on Android before. Toggles membership in * flaggedSenders / flaggedDomains in allowlist.jsonc, which syncs * back to the cloud copy. */ flagSenderOrDomain(type: "sender" | "domain", value: string): Promise<{ flagged: boolean; }>; updateFlags(accountId: string, uid: number, flags: string[]): Promise; /** Read side of the shared allowlist, so the UI can ask whether a sender * has been vouched for. Mirrors MailxService.getAllowlist — the desktop * and Android halves must answer the same question the same way, or the * display-name warning would differ between them. */ getAllowlist(): Promise<{ senders: string[]; domains: string[]; recipients: string[]; flaggedSenders: string[]; flaggedDomains: string[]; }>; allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise; search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number): Promise; getSyncPending(): { pending: number; }; syncAll(): Promise; syncAccount(accountId: string): Promise; reauthenticate(accountId: string): Promise; send(msg: any): Promise; deleteMessage(accountId: string, uid: number): Promise; deleteMessages(accountId: string, uids: number[]): Promise; moveMessage(accountId: string, uid: number, targetFolderId: number, targetAccountId?: string): Promise; moveMessages(accountId: string, uids: number[], targetFolderId: number): Promise; undeleteMessage(accountId: string, uid: number, folderId: number): Promise; saveDraft(accountId: string, subject: string, bodyHtml: string, bodyText: string, to?: string, cc?: string, previousDraftUid?: number, draftId?: string, bcc?: string, from?: string, attachments?: { filename: string; mimeType: string; dataBase64: string; }[]): Promise<{ draftUid: number | null; draftId: string; }>; deleteDraft(accountId: string, draftUid: number): Promise; searchContacts(query: string): any[]; /** Re-pull contacts.jsonc only when the Drive copy actually changed. * One metadata request (cloudStat) throttled to a few minutes; the * full download + reload runs only on a new modifiedTime. Kicked from * autocomplete, the periodic sync tick, and app-resume. */ private _contactsStatAt; private _contactsModifiedTime; private static readonly CONTACTS_STAT_MS; maybeRefreshContactsConfig(): Promise; /** Address-book listing — paginated, filterable. Mirrors mailx-service's * signature so the same client-side address-book modal works on Android * without an "ipc(...).listContacts is not a function" crash. */ listContacts(query: string, page?: number, pageSize?: number): { items: any[]; total: number; page: number; pageSize: number; }; /** Manual upsert from the address-book UI. The desktop path queues a * Google People sync; Android relies on the desktop pushing changes * back, so this is local-only for now. */ upsertContact(name: string, email: string): { ok: true; }; deleteContact(email: string): { ok: true; }; addContact(name: string, email: string): boolean; /** Q49 heuristic mirror: true if the user has ever sent a message to * `recipientEmail` that had a non-empty Cc field. Compose uses this to * decide whether to auto-expand the Cc row on reply. */ hasCcHistoryTo(recipientEmail: string): boolean; getSettings(): Promise; saveSettingsData(settings: any): Promise; getStorageInfo(): { provider: string; mode: string; folderId?: string; folderName?: string; folderPath?: string; folderOwner?: string; }; markFolderRead(folderId: number): void; getAutocompleteSettings(): Promise; saveAutocompleteSettings(settings: AutocompleteSettings): Promise; autocomplete(_req: any): Promise<{ suggestion: string; }>; resetStore(): Promise; readJsoncFile(name: string): Promise; writeJsoncFile(name: string, content: string): Promise; formatJsonc(content: string): Promise; readConfigHelp(_name: string): Promise; /** Compute and return the priority sender / domain index from the * current contacts.jsonc. Reads from cloud (or local cache) every * call — small file, simpler than maintaining a long-lived cache. */ getPriorityLists(): Promise<{ senders: string[]; domains: string[]; }>; setPrioritySender(email: string, value: boolean, name?: string): Promise; setPriorityDomain(domain: string, value: boolean): Promise; sendMessage(msg: any): Promise; searchMessages(query: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number, _includeTrashSpam?: boolean): Promise; private notImpl; getThreadMessages(_accountId: string, _threadId: string): any; getMessageSource(_accountId: string, _uid: number, _folderId?: number): Promise<{ dataBase64: string; filename: string; }>; getAttachment(_accountId: string, _uid: number, _attachmentId: number, _folderId?: number): Promise<{ content: any; contentType: string; filename: string; }>; openAttachment(_accountId: string, _uid: number, _attachmentId: number, _folderId?: number): Promise<{ ok: boolean; path: string; }>; /** Mark as spam = move to the account's \Junk folder, exactly as desktop's * MailxService.markAsSpamMessages does. This was a notImpl() stub, which * is worse than it sounds: the client optimistically removes the rows * from the list BEFORE calling this, then only writes the rejection to * the status bar. So on Android the spam button looked like it worked, * nothing moved on the server, and the next sync brought every message * straight back (Bob 2026-08-07: "attempts to delete spam but they keep * coming back"). moveMessages was implemented all along — spam just never * got wired to it. */ markAsSpamMessages(accountId: string, uids: number[]): Promise<{ targetFolderId: number; moved: number; }>; recordSpamReport(_accountId: string, _uid: number, _folderId: number): Promise; createFolder(_accountId: string, _parentPath: string, _name: string): Promise; renameFolder(_accountId: string, _folderId: number, _newName: string): Promise; deleteFolder(_accountId: string, _folderId: number): Promise; moveFolderToTrash(_accountId: string, _folderId: number): Promise; emptyFolder(_accountId: string, _folderId: number): Promise; getOutboxStatus(): any; listQueuedOutgoing(): any[]; cancelQueuedOutgoing(_filePath: string): { ok: true; }; drainStoreSync(): Promise; reauthGoogleScopes(): { cleared: number; }; /** No per-folder sync lane on Android — a folder "sync now" refreshes the * whole account (cheap: Gmail incremental sync / one IMAP pass). */ syncFolderNow(accountId: string, _folderId: number): Promise; cancelServerSearch(): void; copyMessages(_accountId: string, _uids: number[], _folderIds: number[] | undefined, _targetFolderId: number): Promise; /** `{}` = play the built-in chime — no custom sound files on Android. */ getReminderSound(): Promise<{ mute?: boolean; dataBase64?: string; mime?: string; }>; addPreferredContact(entry: { name: string; email: string; source?: string; organization?: string; }): Promise; addToDenylist(email: string): Promise; hasBccHistoryTo(_email: string): boolean; /** Pull the shared GDrive `contacts.jsonc` and merge its entries into the * local contacts table. Desktop flushes the full union there (everything * it discovered from its mailbox corpus + Google contacts + preferred); * this is how an Android device gets contacts it never saw in its own * (partial) on-device corpus. Called at startup AND on a periodic tick / * app-resume / after a ⊘ so a denylist edit made on another device * reaches a running phone (Bob 2026-07-07: GDrive edit was still being * offered). The denylist refresh runs every call; the contact IMPORT * runs once per session — recordSentAddress bumps `use_count`, so * re-importing on every reload would inflate autocomplete rank. */ private _contactsImported; loadContactsConfig(): Promise; getCalendarEvents(_fromMs: number, _toMs: number): Promise; getCalendars(): Promise>; createCalendarEvent(_ev: any): Promise<{ uuid: string; }>; updateCalendarEvent(_uuid: string, _patch: any): Promise<{ ok: true; }>; deleteCalendarEvent(_uuid: string): Promise<{ ok: true; }>; getTasks(_includeCompleted?: boolean): Promise; createTask(_t: { title: string; notes?: string; dueMs?: number; }): Promise<{ uuid: string; }>; updateTask(_uuid: string, _patch: any): Promise<{ ok: true; }>; deleteTask(_uuid: string): Promise<{ ok: true; }>; private reminderStateCache; getReminderState(): Promise; mergeReminderState(patch: any): Promise; getUserDict(): Promise; addUserDictWord(_word: string): Promise; addUserDictWords(_words: string[]): Promise; removeUserDictWord(_word: string): Promise; getDiagnostics(): any; getPrimaryAccount(_feature?: string): any; getVersion(): any; consumePendingMailto(): any; logClientEvent(..._args: any[]): void; setupAccount(_name: string, _email: string, _password?: string): Promise<{ ok: boolean; error?: string; message?: string; }>; repairAccounts(): Promise<{ ok: boolean; error?: string; message?: string; }>; unsubscribeOneClick(_url: string): Promise<{ ok: boolean; status: number; statusText: string; }>; aiTransform(_req: any): Promise; } //# sourceMappingURL=web-service.d.ts.map