@bobfrankston/mailx 1.0.395 → 1.0.399
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/client/android.html +11 -1
- package/client/app.js +29 -4
- package/client/components/alarms.js +286 -0
- package/client/components/calendar-sidebar.js +43 -7
- package/client/components/message-list.js +159 -16
- package/client/compose/compose.js +24 -0
- package/client/index.html +11 -1
- package/client/lib/api-client.js +3 -0
- package/client/lib/mailxapi.js +1 -0
- package/client/styles/components.css +183 -0
- package/package.json +1 -1
- package/packages/mailx-service/index.d.ts +4 -0
- package/packages/mailx-service/index.js +6 -0
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-store/db.d.ts +8 -0
- package/packages/mailx-store/db.js +26 -0
- package/packages/mailx-store-web/android-bootstrap.js +60 -0
- package/packages/mailx-store-web/db.d.ts +4 -0
- package/packages/mailx-store-web/db.js +25 -0
- package/packages/mailx-store-web/sync-manager.d.ts +7 -0
- package/packages/mailx-store-web/sync-manager.js +55 -0
- package/packages/mailx-store-web/web-service.d.ts +4 -0
- package/packages/mailx-store-web/web-service.js +7 -0
- package/tdview.cmd +1 -0
|
@@ -447,6 +447,58 @@ class AndroidSyncManager {
|
|
|
447
447
|
async undeleteMessage(accountId, uid, folderId) {
|
|
448
448
|
this.db.queueSyncAction(accountId, "undelete", uid, folderId);
|
|
449
449
|
}
|
|
450
|
+
/** Q112: drain queued move/flag/trash actions to the provider. Android is
|
|
451
|
+
* standalone — it pushes state changes directly to Gmail (or other
|
|
452
|
+
* provider) the same way desktop does. Called from the periodic 2-min
|
|
453
|
+
* tick above. `send` actions drain separately via `processSendQueue`. */
|
|
454
|
+
async processSyncActions(accountId) {
|
|
455
|
+
const provider = this.providers.get(accountId);
|
|
456
|
+
if (!provider)
|
|
457
|
+
return;
|
|
458
|
+
const pending = this.db.getPendingSyncActions(accountId)
|
|
459
|
+
.filter((a) => a.action !== "send");
|
|
460
|
+
if (pending.length === 0)
|
|
461
|
+
return;
|
|
462
|
+
const folders = this.db.getFolders(accountId);
|
|
463
|
+
const folderPath = (id) => {
|
|
464
|
+
const f = folders.find((x) => x.id === id);
|
|
465
|
+
return f?.path || null;
|
|
466
|
+
};
|
|
467
|
+
for (const p of pending) {
|
|
468
|
+
const path = folderPath(p.folderId);
|
|
469
|
+
if (!path) {
|
|
470
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown folder ${p.folderId}`);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
if (p.action === "flags" && typeof provider.setFlags === "function") {
|
|
475
|
+
await provider.setFlags(path, p.uid, Array.isArray(p.flags) ? p.flags : (p.flags ? [p.flags] : []));
|
|
476
|
+
}
|
|
477
|
+
else if (p.action === "trash" && typeof provider.trashMessage === "function") {
|
|
478
|
+
await provider.trashMessage(path, p.uid);
|
|
479
|
+
}
|
|
480
|
+
else if (p.action === "move" && typeof provider.moveMessage === "function") {
|
|
481
|
+
const toId = p.targetFolderId;
|
|
482
|
+
const toPath = folderPath(toId);
|
|
483
|
+
if (!toPath) {
|
|
484
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown target folder ${toId}`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
await provider.moveMessage(path, p.uid, toPath);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, `provider does not support ${p.action}`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
this.db.completeSyncActionByUid(accountId, p.action, p.uid);
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
const msg = e?.message || String(e);
|
|
497
|
+
console.error(`[sync-action] ${accountId} ${p.action} uid=${p.uid}: ${msg}`);
|
|
498
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, msg);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
450
502
|
queueOutgoingLocal(accountId, rawMessage) {
|
|
451
503
|
// Local-first: PERSIST to sync_actions before attempting the network
|
|
452
504
|
// send, so a crash / offline / process kill between now and SMTP ACK
|
|
@@ -919,11 +971,16 @@ export async function initAndroid() {
|
|
|
919
971
|
// Drain any stranded send-queue entries BEFORE first sync. A message
|
|
920
972
|
// queued in a prior session (offline, crashed mid-send, process killed)
|
|
921
973
|
// gets a retry as soon as we have accounts registered. Desktop parity.
|
|
974
|
+
// Q112 (2026-04-24): also drain move/flag/trash actions here — Android
|
|
975
|
+
// is standalone, not desktop-dependent, so it pushes state changes
|
|
976
|
+
// directly to the server the same way desktop does.
|
|
922
977
|
for (const account of accounts) {
|
|
923
978
|
if (!account.enabled)
|
|
924
979
|
continue;
|
|
925
980
|
syncManager.processSendQueue(account.id)
|
|
926
981
|
.catch(e => console.error(`[android] processSendQueue ${account.id}: ${e.message}`));
|
|
982
|
+
syncManager.processSyncActions(account.id)
|
|
983
|
+
.catch(e => console.error(`[android] processSyncActions ${account.id}: ${e.message}`));
|
|
927
984
|
}
|
|
928
985
|
setTimeout(() => {
|
|
929
986
|
syncManager.syncAll().catch(e => console.error(`[android] Sync error: ${e.message}`));
|
|
@@ -937,6 +994,8 @@ export async function initAndroid() {
|
|
|
937
994
|
for (const account of db.getAccounts()) {
|
|
938
995
|
syncManager.processSendQueue(account.id)
|
|
939
996
|
.catch(e => console.error(`[android] retry ${account.id}: ${e.message}`));
|
|
997
|
+
syncManager.processSyncActions(account.id)
|
|
998
|
+
.catch(e => console.error(`[android] processSyncActions ${account.id}: ${e.message}`));
|
|
940
999
|
}
|
|
941
1000
|
syncManager.syncAll().catch(e => console.error(`[android] Periodic sync error: ${e.message}`));
|
|
942
1001
|
}, SYNC_INTERVAL_MS);
|
|
@@ -998,6 +1057,7 @@ function installBridge() {
|
|
|
998
1057
|
},
|
|
999
1058
|
searchMessages: (query, page, pageSize) => service.search(query, page, pageSize),
|
|
1000
1059
|
searchContacts: (query) => service.searchContacts(query),
|
|
1060
|
+
hasCcHistoryTo: (email) => ({ hasCc: service.hasCcHistoryTo?.(email) ?? false }),
|
|
1001
1061
|
syncAll: async () => { await service.syncAll(); return { ok: true }; },
|
|
1002
1062
|
syncAccount: async (accountId) => { await service.syncAccount(accountId); return { ok: true }; },
|
|
1003
1063
|
getSyncPending: () => service.getSyncPending(),
|
|
@@ -94,6 +94,10 @@ export declare class WebMailxDB {
|
|
|
94
94
|
source: string;
|
|
95
95
|
useCount: number;
|
|
96
96
|
}[];
|
|
97
|
+
/** Q49 heuristic: has the user ever sent to `recipientEmail` with a
|
|
98
|
+
* non-empty Cc? Scans Sent folder(s). Used by compose to auto-expand
|
|
99
|
+
* the Cc row on reply to a frequent-Cc'd recipient. */
|
|
100
|
+
hasCcHistoryTo(recipientEmail: string): boolean;
|
|
97
101
|
searchMessages(query: string, page?: number, pageSize?: number, accountId?: string, folderId?: number): PagedResult<MessageEnvelope>;
|
|
98
102
|
queueSyncAction(accountId: string, action: string, uid: number, folderId: number, extra?: {
|
|
99
103
|
targetFolderId?: number;
|
|
@@ -483,11 +483,36 @@ export class WebMailxDB {
|
|
|
483
483
|
return added;
|
|
484
484
|
}
|
|
485
485
|
searchContacts(query, limit = 10) {
|
|
486
|
+
query = (query || "").trim();
|
|
487
|
+
if (!query)
|
|
488
|
+
return [];
|
|
486
489
|
const q = `%${query}%`;
|
|
487
490
|
return this.all(`SELECT name, email, source, use_count as useCount FROM contacts
|
|
488
491
|
WHERE email LIKE ? OR name LIKE ?
|
|
489
492
|
ORDER BY use_count DESC, last_used DESC LIMIT ?`, [q, q, limit]);
|
|
490
493
|
}
|
|
494
|
+
/** Q49 heuristic: has the user ever sent to `recipientEmail` with a
|
|
495
|
+
* non-empty Cc? Scans Sent folder(s). Used by compose to auto-expand
|
|
496
|
+
* the Cc row on reply to a frequent-Cc'd recipient. */
|
|
497
|
+
hasCcHistoryTo(recipientEmail) {
|
|
498
|
+
const email = (recipientEmail || "").trim().toLowerCase();
|
|
499
|
+
if (!email)
|
|
500
|
+
return false;
|
|
501
|
+
try {
|
|
502
|
+
const row = this.get(`
|
|
503
|
+
SELECT 1 FROM messages m
|
|
504
|
+
JOIN folders f ON m.folder_id = f.id
|
|
505
|
+
WHERE f.special_use = 'sent'
|
|
506
|
+
AND lower(m.to_json) LIKE ?
|
|
507
|
+
AND m.cc_json IS NOT NULL AND m.cc_json != '[]' AND m.cc_json != ''
|
|
508
|
+
LIMIT 1
|
|
509
|
+
`, [`%"${email}"%`]);
|
|
510
|
+
return !!row;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
491
516
|
// ── Search ──
|
|
492
517
|
searchMessages(query, page = 1, pageSize = 50, accountId, folderId) {
|
|
493
518
|
const offset = (page - 1) * pageSize;
|
|
@@ -48,6 +48,13 @@ export declare class SyncManager implements WebSyncManager {
|
|
|
48
48
|
}[], targetFolderId: number): Promise<void>;
|
|
49
49
|
moveMessageCrossAccount(): Promise<void>;
|
|
50
50
|
undeleteMessage(accountId: string, uid: number, folderId: number): Promise<void>;
|
|
51
|
+
/** Q112: drain queued move/flag/trash actions to the provider. Android is
|
|
52
|
+
* standalone — it pushes state changes to Gmail (or other provider) the
|
|
53
|
+
* same way desktop does, so local actions propagate without needing a
|
|
54
|
+
* desktop to relay them. Called from android-bootstrap on startup and
|
|
55
|
+
* every 2-min sync tick. `send` actions are drained separately by
|
|
56
|
+
* processSendQueue. */
|
|
57
|
+
processSyncActions(accountId: string): Promise<void>;
|
|
51
58
|
markFolderRead(folderId: number): Promise<void>;
|
|
52
59
|
emptyFolder(accountId: string, folderId: number): Promise<void>;
|
|
53
60
|
queueOutgoingLocal(accountId: string, rawMessage: string): void;
|
|
@@ -329,6 +329,61 @@ export class SyncManager {
|
|
|
329
329
|
async undeleteMessage(accountId, uid, folderId) {
|
|
330
330
|
this.db.queueSyncAction(accountId, "undelete", uid, folderId);
|
|
331
331
|
}
|
|
332
|
+
/** Q112: drain queued move/flag/trash actions to the provider. Android is
|
|
333
|
+
* standalone — it pushes state changes to Gmail (or other provider) the
|
|
334
|
+
* same way desktop does, so local actions propagate without needing a
|
|
335
|
+
* desktop to relay them. Called from android-bootstrap on startup and
|
|
336
|
+
* every 2-min sync tick. `send` actions are drained separately by
|
|
337
|
+
* processSendQueue. */
|
|
338
|
+
async processSyncActions(accountId) {
|
|
339
|
+
const provider = this.getProvider(accountId);
|
|
340
|
+
if (!provider)
|
|
341
|
+
return;
|
|
342
|
+
const pending = this.db.getPendingSyncActions(accountId)
|
|
343
|
+
.filter((a) => a.action !== "send");
|
|
344
|
+
if (pending.length === 0)
|
|
345
|
+
return;
|
|
346
|
+
const folders = this.db.getFolders(accountId);
|
|
347
|
+
const folderPath = (id) => {
|
|
348
|
+
const f = folders.find((x) => x.id === id);
|
|
349
|
+
return f?.path || null;
|
|
350
|
+
};
|
|
351
|
+
for (const p of pending) {
|
|
352
|
+
const path = folderPath(p.folderId);
|
|
353
|
+
if (!path) {
|
|
354
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown folder ${p.folderId}`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
if (p.action === "flags" && typeof provider.setFlags === "function") {
|
|
359
|
+
await provider.setFlags(path, p.uid, Array.isArray(p.flags) ? p.flags : (p.flags ? [p.flags] : []));
|
|
360
|
+
}
|
|
361
|
+
else if (p.action === "trash" && typeof provider.trashMessage === "function") {
|
|
362
|
+
await provider.trashMessage(path, p.uid);
|
|
363
|
+
}
|
|
364
|
+
else if (p.action === "move" && typeof provider.moveMessage === "function") {
|
|
365
|
+
const toId = p.targetFolderId;
|
|
366
|
+
const toPath = folderPath(toId);
|
|
367
|
+
if (!toPath) {
|
|
368
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown target folder ${toId}`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
await provider.moveMessage(path, p.uid, toPath);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// Unsupported action for this provider — don't loop forever.
|
|
375
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, `provider does not support ${p.action}`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
this.db.completeSyncActionByUid(accountId, p.action, p.uid);
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
const msg = e?.message || String(e);
|
|
382
|
+
console.error(`[sync-action] ${accountId} ${p.action} uid=${p.uid}: ${msg}`);
|
|
383
|
+
this.db.failSyncActionByUid(accountId, p.action, p.uid, msg);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
332
387
|
async markFolderRead(folderId) {
|
|
333
388
|
this.db.markFolderRead(folderId);
|
|
334
389
|
}
|
|
@@ -71,6 +71,10 @@ export declare class WebMailxService {
|
|
|
71
71
|
}>;
|
|
72
72
|
deleteDraft(accountId: string, draftUid: number): Promise<void>;
|
|
73
73
|
searchContacts(query: string): any[];
|
|
74
|
+
/** Q49 heuristic mirror: true if the user has ever sent a message to
|
|
75
|
+
* `recipientEmail` that had a non-empty Cc field. Compose uses this to
|
|
76
|
+
* decide whether to auto-expand the Cc row on reply. */
|
|
77
|
+
hasCcHistoryTo(recipientEmail: string): boolean;
|
|
74
78
|
getSettings(): Promise<any>;
|
|
75
79
|
saveSettingsData(settings: any): Promise<void>;
|
|
76
80
|
getStorageInfo(): {
|
|
@@ -437,10 +437,17 @@ export class WebMailxService {
|
|
|
437
437
|
}
|
|
438
438
|
// ── Contacts ──
|
|
439
439
|
searchContacts(query) {
|
|
440
|
+
query = (query || "").trim();
|
|
440
441
|
if (query.length < 1)
|
|
441
442
|
return [];
|
|
442
443
|
return this.db.searchContacts(query);
|
|
443
444
|
}
|
|
445
|
+
/** Q49 heuristic mirror: true if the user has ever sent a message to
|
|
446
|
+
* `recipientEmail` that had a non-empty Cc field. Compose uses this to
|
|
447
|
+
* decide whether to auto-expand the Cc row on reply. */
|
|
448
|
+
hasCcHistoryTo(recipientEmail) {
|
|
449
|
+
return this.db.hasCcHistoryTo?.(recipientEmail) ?? false;
|
|
450
|
+
}
|
|
444
451
|
// ── Settings ──
|
|
445
452
|
async getSettings() {
|
|
446
453
|
return loadSettings();
|
package/tdview.cmd
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
call mdview todo.md -pos 100,100,1 -size 900,1400
|