@bobfrankston/mailx 1.0.378 → 1.0.382
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/bin/mailx.js +18 -0
- package/client/app.js +107 -1
- package/client/components/calendar-sidebar.js +45 -79
- package/client/components/folder-tree.js +25 -0
- package/client/components/message-list.js +75 -1
- package/client/components/message-viewer.js +7 -0
- package/client/index.html +10 -5
- package/client/lib/api-client.js +31 -2
- package/client/lib/mailxapi.js +13 -0
- package/client/styles/components.css +124 -0
- package/client/styles/layout.css +34 -10
- package/package.json +3 -3
- package/packages/mailx-imap/index.js +31 -2
- package/packages/mailx-service/google-sync.d.ts +83 -0
- package/packages/mailx-service/google-sync.js +140 -0
- package/packages/mailx-service/index.d.ts +44 -2
- package/packages/mailx-service/index.js +218 -3
- package/packages/mailx-service/jsonrpc.js +25 -0
- package/packages/mailx-store/db.d.ts +49 -0
- package/packages/mailx-store/db.js +255 -6
- package/packages/mailx-store-web/db.js +54 -0
- package/packages/mailx-store-web/web-service.js +19 -10
- package/packages/mailx-types/index.d.ts +15 -3
- package/packages/mailx-types/index.js +90 -2
|
@@ -109,6 +109,60 @@ const SCHEMA = `
|
|
|
109
109
|
last_error TEXT,
|
|
110
110
|
UNIQUE(account_id, action, uid, folder_id)
|
|
111
111
|
);
|
|
112
|
+
|
|
113
|
+
-- Calendar events two-way cache (Android parity with desktop's
|
|
114
|
+
-- packages/mailx-store/db.ts calendar_events table). uuid = stable
|
|
115
|
+
-- local identity, provider_id = Google Calendar event id when known,
|
|
116
|
+
-- dirty = local edit not yet pushed, deleted = tombstone pending delete.
|
|
117
|
+
CREATE TABLE IF NOT EXISTS calendar_events (
|
|
118
|
+
uuid TEXT PRIMARY KEY,
|
|
119
|
+
account_id TEXT NOT NULL,
|
|
120
|
+
provider_id TEXT,
|
|
121
|
+
calendar_id TEXT DEFAULT 'primary',
|
|
122
|
+
title TEXT NOT NULL DEFAULT '',
|
|
123
|
+
start_ms INTEGER NOT NULL,
|
|
124
|
+
end_ms INTEGER NOT NULL,
|
|
125
|
+
all_day INTEGER DEFAULT 0,
|
|
126
|
+
location TEXT DEFAULT '',
|
|
127
|
+
notes TEXT DEFAULT '',
|
|
128
|
+
etag TEXT,
|
|
129
|
+
last_synced INTEGER DEFAULT 0,
|
|
130
|
+
dirty INTEGER DEFAULT 0,
|
|
131
|
+
deleted INTEGER DEFAULT 0,
|
|
132
|
+
updated_at INTEGER NOT NULL
|
|
133
|
+
);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_calendar_events_start ON calendar_events(account_id, start_ms);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
137
|
+
uuid TEXT PRIMARY KEY,
|
|
138
|
+
account_id TEXT NOT NULL,
|
|
139
|
+
provider_id TEXT,
|
|
140
|
+
list_id TEXT DEFAULT '@default',
|
|
141
|
+
title TEXT NOT NULL DEFAULT '',
|
|
142
|
+
notes TEXT DEFAULT '',
|
|
143
|
+
due_ms INTEGER,
|
|
144
|
+
completed_ms INTEGER,
|
|
145
|
+
etag TEXT,
|
|
146
|
+
last_synced INTEGER DEFAULT 0,
|
|
147
|
+
dirty INTEGER DEFAULT 0,
|
|
148
|
+
deleted INTEGER DEFAULT 0,
|
|
149
|
+
updated_at INTEGER NOT NULL
|
|
150
|
+
);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_account ON tasks(account_id);
|
|
152
|
+
|
|
153
|
+
-- Generic store-sync queue for non-message domains (calendar/tasks/contacts).
|
|
154
|
+
CREATE TABLE IF NOT EXISTS store_sync (
|
|
155
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
156
|
+
kind TEXT NOT NULL,
|
|
157
|
+
op TEXT NOT NULL,
|
|
158
|
+
account_id TEXT NOT NULL,
|
|
159
|
+
target_uuid TEXT NOT NULL,
|
|
160
|
+
payload TEXT,
|
|
161
|
+
attempts INTEGER DEFAULT 0,
|
|
162
|
+
last_error TEXT,
|
|
163
|
+
created_at INTEGER NOT NULL,
|
|
164
|
+
UNIQUE(kind, target_uuid, op)
|
|
165
|
+
);
|
|
112
166
|
`;
|
|
113
167
|
const IDB_NAME = "mailx-sqldb";
|
|
114
168
|
const IDB_STORE = "database";
|
|
@@ -311,21 +311,30 @@ export class WebMailxService {
|
|
|
311
311
|
}
|
|
312
312
|
async syncAccount(accountId) {
|
|
313
313
|
const folders = await this.syncManager.syncFolders(accountId);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
});
|
|
321
|
-
for (const folder of sorted) {
|
|
314
|
+
// INBOX-first: await INBOX so the UI re-renders new mail immediately,
|
|
315
|
+
// then fire the rest in the background so labels don't block the
|
|
316
|
+
// list. S57 (Android parity with desktop's local-first rule).
|
|
317
|
+
const inbox = folders.find(f => f.specialUse === "inbox");
|
|
318
|
+
const others = folders.filter(f => f.specialUse !== "inbox");
|
|
319
|
+
if (inbox) {
|
|
322
320
|
try {
|
|
323
|
-
await this.syncManager.syncFolder(accountId,
|
|
321
|
+
await this.syncManager.syncFolder(accountId, inbox.id);
|
|
324
322
|
}
|
|
325
323
|
catch (e) {
|
|
326
|
-
console.error(` Skipping
|
|
324
|
+
console.error(` Skipping INBOX ${inbox.path}: ${e.message}`);
|
|
327
325
|
}
|
|
328
326
|
}
|
|
327
|
+
// Background fan-out: don't await; errors log. UI already has INBOX.
|
|
328
|
+
(async () => {
|
|
329
|
+
for (const folder of others) {
|
|
330
|
+
try {
|
|
331
|
+
await this.syncManager.syncFolder(accountId, folder.id);
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
console.error(` Skipping folder ${folder.path}: ${e.message}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
})().catch(() => { });
|
|
329
338
|
}
|
|
330
339
|
async reauthenticate(accountId) {
|
|
331
340
|
return this.syncManager.reauthenticate(accountId);
|
|
@@ -278,10 +278,22 @@ export declare function sanitizeHtml(html: string): {
|
|
|
278
278
|
/** Encode text as RFC 2045 quoted-printable. */
|
|
279
279
|
export declare function encodeQuotedPrintable(text: string): string;
|
|
280
280
|
/** Parse search query into structured conditions.
|
|
281
|
-
* Supports qualifiers: from:, to:, subject
|
|
282
|
-
*
|
|
281
|
+
* Supports qualifiers: from:, to:, subject:, date:, has:attachment,
|
|
282
|
+
* is:flagged, is:unread, is:read. Unqualified terms search across subject /
|
|
283
|
+
* from / preview. Returns { conditions, params } for SQL WHERE clause with
|
|
284
|
+
* LIKE plus structured predicates (flags_json LIKE, has_attachments=1, date
|
|
285
|
+
* range comparisons).
|
|
286
|
+
*
|
|
287
|
+
* Date syntax (matches Gmail-ish conventions):
|
|
288
|
+
* - date:2026-04-22 exact day
|
|
289
|
+
* - date:2026-04 month
|
|
290
|
+
* - date:>2026-04-01 after
|
|
291
|
+
* - date:<2026-04-01 before
|
|
292
|
+
* - date:2026-04-01..2026-04-30 range
|
|
293
|
+
* - date:today / yesterday / last7 / last30
|
|
294
|
+
*/
|
|
283
295
|
export declare function parseSearchQuery(query: string): {
|
|
284
296
|
conditions: string[];
|
|
285
|
-
params: string[];
|
|
297
|
+
params: (string | number)[];
|
|
286
298
|
};
|
|
287
299
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -66,16 +66,68 @@ export function encodeQuotedPrintable(text) {
|
|
|
66
66
|
return result;
|
|
67
67
|
}
|
|
68
68
|
/** Parse search query into structured conditions.
|
|
69
|
-
* Supports qualifiers: from:, to:, subject
|
|
70
|
-
*
|
|
69
|
+
* Supports qualifiers: from:, to:, subject:, date:, has:attachment,
|
|
70
|
+
* is:flagged, is:unread, is:read. Unqualified terms search across subject /
|
|
71
|
+
* from / preview. Returns { conditions, params } for SQL WHERE clause with
|
|
72
|
+
* LIKE plus structured predicates (flags_json LIKE, has_attachments=1, date
|
|
73
|
+
* range comparisons).
|
|
74
|
+
*
|
|
75
|
+
* Date syntax (matches Gmail-ish conventions):
|
|
76
|
+
* - date:2026-04-22 exact day
|
|
77
|
+
* - date:2026-04 month
|
|
78
|
+
* - date:>2026-04-01 after
|
|
79
|
+
* - date:<2026-04-01 before
|
|
80
|
+
* - date:2026-04-01..2026-04-30 range
|
|
81
|
+
* - date:today / yesterday / last7 / last30
|
|
82
|
+
*/
|
|
71
83
|
export function parseSearchQuery(query) {
|
|
72
84
|
const parts = query.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
73
85
|
const conditions = [];
|
|
74
86
|
const params = [];
|
|
87
|
+
const dayStart = (y, m, d) => new Date(y, m - 1, d).getTime();
|
|
88
|
+
const parseDateSpec = (spec) => {
|
|
89
|
+
const now = new Date();
|
|
90
|
+
const today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
91
|
+
if (spec === "today")
|
|
92
|
+
return { from: today0, to: today0 + 86400_000 };
|
|
93
|
+
if (spec === "yesterday")
|
|
94
|
+
return { from: today0 - 86400_000, to: today0 };
|
|
95
|
+
const lastN = spec.match(/^last(\d+)$/i);
|
|
96
|
+
if (lastN)
|
|
97
|
+
return { from: today0 - parseInt(lastN[1]) * 86400_000 };
|
|
98
|
+
const rangeMatch = spec.match(/^(\d{4})-(\d{2})-(\d{2})\.\.(\d{4})-(\d{2})-(\d{2})$/);
|
|
99
|
+
if (rangeMatch)
|
|
100
|
+
return {
|
|
101
|
+
from: dayStart(+rangeMatch[1], +rangeMatch[2], +rangeMatch[3]),
|
|
102
|
+
to: dayStart(+rangeMatch[4], +rangeMatch[5], +rangeMatch[6]) + 86400_000,
|
|
103
|
+
};
|
|
104
|
+
const gtMatch = spec.match(/^>(\d{4})-(\d{2})-(\d{2})$/);
|
|
105
|
+
if (gtMatch)
|
|
106
|
+
return { from: dayStart(+gtMatch[1], +gtMatch[2], +gtMatch[3]) + 86400_000 };
|
|
107
|
+
const ltMatch = spec.match(/^<(\d{4})-(\d{2})-(\d{2})$/);
|
|
108
|
+
if (ltMatch)
|
|
109
|
+
return { to: dayStart(+ltMatch[1], +ltMatch[2], +ltMatch[3]) };
|
|
110
|
+
const monthMatch = spec.match(/^(\d{4})-(\d{2})$/);
|
|
111
|
+
if (monthMatch) {
|
|
112
|
+
const y = +monthMatch[1], m = +monthMatch[2];
|
|
113
|
+
const from = dayStart(y, m, 1);
|
|
114
|
+
const to = m === 12 ? dayStart(y + 1, 1, 1) : dayStart(y, m + 1, 1);
|
|
115
|
+
return { from, to };
|
|
116
|
+
}
|
|
117
|
+
const dayMatch = spec.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
118
|
+
if (dayMatch) {
|
|
119
|
+
const from = dayStart(+dayMatch[1], +dayMatch[2], +dayMatch[3]);
|
|
120
|
+
return { from, to: from + 86400_000 };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
75
124
|
for (const part of parts) {
|
|
76
125
|
const fromMatch = part.match(/^from:(.+)$/i);
|
|
77
126
|
const toMatch = part.match(/^to:(.+)$/i);
|
|
78
127
|
const subjectMatch = part.match(/^subject:(.+)$/i);
|
|
128
|
+
const hasMatch = part.match(/^has:(.+)$/i);
|
|
129
|
+
const isMatch = part.match(/^is:(.+)$/i);
|
|
130
|
+
const dateMatch = part.match(/^date:(.+)$/i);
|
|
79
131
|
if (fromMatch) {
|
|
80
132
|
const term = `%${fromMatch[1].replace(/"/g, "")}%`;
|
|
81
133
|
conditions.push("(from_name LIKE ? OR from_address LIKE ?)");
|
|
@@ -91,6 +143,42 @@ export function parseSearchQuery(query) {
|
|
|
91
143
|
conditions.push("subject LIKE ?");
|
|
92
144
|
params.push(term);
|
|
93
145
|
}
|
|
146
|
+
else if (hasMatch) {
|
|
147
|
+
const v = hasMatch[1].toLowerCase();
|
|
148
|
+
if (v === "attachment" || v === "attachments") {
|
|
149
|
+
conditions.push("has_attachments = 1");
|
|
150
|
+
}
|
|
151
|
+
// Unknown has: qualifier — silently drop; treating as a literal
|
|
152
|
+
// search term would be confusing.
|
|
153
|
+
}
|
|
154
|
+
else if (isMatch) {
|
|
155
|
+
const v = isMatch[1].toLowerCase();
|
|
156
|
+
if (v === "flagged" || v === "starred") {
|
|
157
|
+
conditions.push("flags_json LIKE ?");
|
|
158
|
+
params.push("%\\\\Flagged%");
|
|
159
|
+
}
|
|
160
|
+
else if (v === "unread") {
|
|
161
|
+
conditions.push("flags_json NOT LIKE ?");
|
|
162
|
+
params.push("%\\\\Seen%");
|
|
163
|
+
}
|
|
164
|
+
else if (v === "read") {
|
|
165
|
+
conditions.push("flags_json LIKE ?");
|
|
166
|
+
params.push("%\\\\Seen%");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (dateMatch) {
|
|
170
|
+
const spec = parseDateSpec(dateMatch[1]);
|
|
171
|
+
if (spec) {
|
|
172
|
+
if (spec.from !== undefined) {
|
|
173
|
+
conditions.push("date >= ?");
|
|
174
|
+
params.push(spec.from);
|
|
175
|
+
}
|
|
176
|
+
if (spec.to !== undefined) {
|
|
177
|
+
conditions.push("date < ?");
|
|
178
|
+
params.push(spec.to);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
94
182
|
else {
|
|
95
183
|
const term = `%${part}%`;
|
|
96
184
|
conditions.push("(subject LIKE ? OR from_name LIKE ? OR from_address LIKE ? OR preview LIKE ?)");
|