@bobfrankston/rmfmail 1.2.301 → 1.2.303
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/.commitmsg +43 -25
- package/client/android-bootstrap.bundle.js +85 -18
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/styles/components.css +6 -1
- package/npmchanges.md +84 -0
- package/package.json +4 -4
- package/packages/mailx-store-web/db.d.ts +1 -1
- package/packages/mailx-store-web/db.d.ts.map +1 -1
- package/packages/mailx-store-web/db.js +28 -4
- package/packages/mailx-store-web/db.js.map +1 -1
- package/packages/mailx-store-web/db.ts +28 -4
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/web-service.d.ts +26 -4
- package/packages/mailx-store-web/web-service.d.ts.map +1 -1
- package/packages/mailx-store-web/web-service.js +96 -16
- package/packages/mailx-store-web/web-service.js.map +1 -1
- package/packages/mailx-store-web/web-service.ts +93 -15
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-69280 → node_modules.npmglobalize-stash-42308}/.package-lock.json +0 -0
package/.commitmsg
CHANGED
|
@@ -1,29 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
The phone's star filter was never asked for, and the chip announcing it landed in a 28px column
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
"it then says no flagged messages. but there are message flagged" (Bob
|
|
4
|
+
2026-09-04, on Android, with the screenshot). There are 260 — 257 in bobma's
|
|
5
|
+
INBOX alone, and the desktop's own SQL finds every one of them.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
implementation
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
it lands after anything applyInit does synchronously afterwards. Quill's
|
|
15
|
-
`setSelection` focuses as well. Whatever order the two lines were written in,
|
|
16
|
-
the editor was always going to win.
|
|
7
|
+
`WebMailxService.getUnifiedInbox` took two parameters. api-client has always
|
|
8
|
+
called it with four: `(page, pageSize, flaggedOnly, dateBasis)`. `getMessages`
|
|
9
|
+
took seven and is called with nine, `flaggedOnly` at position 8. The extras
|
|
10
|
+
fell off the end and JavaScript said nothing — and neither did TypeScript,
|
|
11
|
+
because an implementation with FEWER parameters satisfies an interface
|
|
12
|
+
declaring more. Both dropped arguments are on `MessageQuery`, documented, and
|
|
13
|
+
honoured by the desktop store.
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
What was left on the phone was the client's `.flagged-only` CSS rule, which
|
|
16
|
+
hides unflagged rows in the loaded page. That is precisely the arrangement the
|
|
17
|
+
desktop comment has warned about since 2026-06-05: a paginated list whose
|
|
18
|
+
newest 50 messages happen to carry no star shows nothing at all, and every
|
|
19
|
+
starred message deeper in the mailbox is unreachable. The overlay then counted
|
|
20
|
+
the rows it was hiding and reported the truth from where it stood — "A 'flagged
|
|
21
|
+
only' filter is hiding 50 messages" — while the filter that could have found
|
|
22
|
+
the other 260 had never run.
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
Both parameters are now threaded through, and the `\Flagged` predicate lives in
|
|
25
|
+
one named constant matching desktop's character for character: two backslashes
|
|
26
|
+
in the SQL literal, because `flags_json` is JSON and the flag is stored
|
|
27
|
+
escaped. `dateBasis` was dropped by the same omission on the same line, so the
|
|
28
|
+
phone has been ordering by arrival time while the desktop ordered by the Date:
|
|
29
|
+
header; it now honours what it is handed. That is a visible change to list
|
|
30
|
+
order on the phone, and it is the order the client has been asking for all
|
|
31
|
+
along — say so if you want it left as it was.
|
|
32
|
+
|
|
33
|
+
Three tests assert the threading, which is where the bug was. They use a spy
|
|
34
|
+
db, since the failure was an argument that never arrived, not SQL that got it
|
|
35
|
+
wrong.
|
|
36
|
+
|
|
37
|
+
**The chip, separately.** `#ml-filter-chip` said `display: block; width: 100%`
|
|
38
|
+
and never said `grid-column: 1 / -1`. `.message-list` is a five-column grid,
|
|
39
|
+
every other full-width child declares the span, and this one auto-placed into
|
|
40
|
+
column 1 — the 28px avatar track — so "Showing flagged (★) messages only —
|
|
41
|
+
click to show everything" came out one word per line down the left edge of the
|
|
42
|
+
header. `width: 100%` cannot help there; inside a grid it means 100% of a 28px
|
|
43
|
+
track.
|
|
44
|
+
|
|
45
|
+
Not changed: the ★ button sits in the search row, next to Server and
|
|
46
|
+
Trash/Spam, which is why reaching for search found a list filter. That is a
|
|
47
|
+
placement judgement, not a bug, so it is yours to call.
|
|
@@ -5855,6 +5855,7 @@ var sql_wasm_default = __toBinary("AGFzbQEAAAABnwRFYAJ/fwF/YAF/AX9gA39/fwBgA39/f
|
|
|
5855
5855
|
|
|
5856
5856
|
// packages/mailx-store-web/db.js
|
|
5857
5857
|
init_mailx_types();
|
|
5858
|
+
var FLAGGED_SQL = " AND flags_json LIKE '%\\\\Flagged%'";
|
|
5858
5859
|
var JUNK_LOCAL_RE = new RegExp(CONTACT_RULES.junk.localExact, "i");
|
|
5859
5860
|
var JUNK_LOCAL_SUFFIX_RE = new RegExp(CONTACT_RULES.junk.localSuffix, "i");
|
|
5860
5861
|
var JUNK_LOCAL_PREFIX_RE = new RegExp(CONTACT_RULES.junk.localPrefix, "i");
|
|
@@ -6304,7 +6305,8 @@ var WebMailxDB = class {
|
|
|
6304
6305
|
const page = query.page || 1;
|
|
6305
6306
|
const pageSize = query.pageSize || 50;
|
|
6306
6307
|
const offset = (page - 1) * pageSize;
|
|
6307
|
-
const
|
|
6308
|
+
const dateCol = query.dateBasis === "received" ? "date" : "COALESCE(sent_date, date)";
|
|
6309
|
+
const sortCol = query.sort === "from" ? "from_name" : query.sort === "subject" ? "subject" : dateCol;
|
|
6308
6310
|
const sortDir = query.sortDir || "desc";
|
|
6309
6311
|
let where = "account_id = ? AND folder_id = ?";
|
|
6310
6312
|
const params = [query.accountId, query.folderId];
|
|
@@ -6313,21 +6315,25 @@ var WebMailxDB = class {
|
|
|
6313
6315
|
const term = `%${query.search}%`;
|
|
6314
6316
|
params.push(term, term, term);
|
|
6315
6317
|
}
|
|
6318
|
+
if (query.flaggedOnly)
|
|
6319
|
+
where += FLAGGED_SQL;
|
|
6316
6320
|
const countRow = this.get(`SELECT COUNT(*) as cnt FROM messages WHERE ${where}`, params);
|
|
6317
6321
|
const total = countRow?.cnt || 0;
|
|
6318
6322
|
const rows = this.all(`SELECT * FROM messages WHERE ${where} ORDER BY ${sortCol} ${sortDir} LIMIT ? OFFSET ?`, [...params, pageSize, offset]);
|
|
6319
6323
|
return { items: rows.map((r) => this.rowToEnvelope(r)), total, page, pageSize };
|
|
6320
6324
|
}
|
|
6321
|
-
getUnifiedInbox(page = 1, pageSize = 50) {
|
|
6325
|
+
getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false, dateBasis = "sent") {
|
|
6322
6326
|
const offset = (page - 1) * pageSize;
|
|
6323
6327
|
const inboxRows = this.all("SELECT id FROM folders WHERE special_use = 'inbox'");
|
|
6324
6328
|
if (inboxRows.length === 0)
|
|
6325
6329
|
return { items: [], total: 0, page, pageSize };
|
|
6326
6330
|
const ids = inboxRows.map((r) => r.id);
|
|
6327
6331
|
const placeholders = ids.map(() => "?").join(",");
|
|
6328
|
-
const
|
|
6332
|
+
const flagFilter = flaggedOnly ? FLAGGED_SQL : "";
|
|
6333
|
+
const dateCol = dateBasis === "received" ? "date" : "COALESCE(sent_date, date)";
|
|
6334
|
+
const countRow = this.get(`SELECT COUNT(*) as cnt FROM messages WHERE folder_id IN (${placeholders})${flagFilter}`, ids);
|
|
6329
6335
|
const total = countRow?.cnt || 0;
|
|
6330
|
-
const rows = this.all(`SELECT * FROM messages WHERE folder_id IN (${placeholders}) ORDER BY
|
|
6336
|
+
const rows = this.all(`SELECT * FROM messages WHERE folder_id IN (${placeholders})${flagFilter} ORDER BY ${dateCol} DESC LIMIT ? OFFSET ?`, [...ids, pageSize, offset]);
|
|
6331
6337
|
return { items: rows.map((r) => this.rowToEnvelope(r)), total, page, pageSize };
|
|
6332
6338
|
}
|
|
6333
6339
|
getMessageByUid(accountId, uid, folderId) {
|
|
@@ -6841,13 +6847,13 @@ function parseMimeParts(body, boundary, topHeaders) {
|
|
|
6841
6847
|
}
|
|
6842
6848
|
if (disposition.includes("attachment") || partType.includes("application/") && !partType.includes("text/")) {
|
|
6843
6849
|
const filenameMatch = disposition.match(/filename="?([^";\r\n]+)"?/i) || partType.match(/name="?([^";\r\n]+)"?/i);
|
|
6844
|
-
const
|
|
6850
|
+
const content = decodeBodyBytes(partBody, partEncoding);
|
|
6845
6851
|
attachments.push({
|
|
6846
6852
|
filename: filenameMatch?.[1]?.trim() || `attachment-${attachments.length}`,
|
|
6847
6853
|
contentType: partType.split(";")[0].trim(),
|
|
6848
|
-
size:
|
|
6854
|
+
size: content.length,
|
|
6849
6855
|
contentId: (partHeaders.get("content-id") || "").replace(/[<>]/g, ""),
|
|
6850
|
-
content
|
|
6856
|
+
content
|
|
6851
6857
|
});
|
|
6852
6858
|
} else if (partType.includes("text/html")) {
|
|
6853
6859
|
const charsetMatch = partType.match(/charset="?([^";\s]+)"?/i);
|
|
@@ -6859,14 +6865,21 @@ function parseMimeParts(body, boundary, topHeaders) {
|
|
|
6859
6865
|
}
|
|
6860
6866
|
return { html, text, headers: topHeaders, attachments };
|
|
6861
6867
|
}
|
|
6862
|
-
function
|
|
6868
|
+
function base64FromBytes(bytes) {
|
|
6869
|
+
const CHUNK = 32768;
|
|
6870
|
+
let binary = "";
|
|
6871
|
+
for (let i = 0; i < bytes.length; i += CHUNK)
|
|
6872
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
6873
|
+
return btoa(binary);
|
|
6874
|
+
}
|
|
6875
|
+
function decodeBodyBytes(body, encoding) {
|
|
6863
6876
|
let bytes;
|
|
6864
6877
|
if (encoding === "base64") {
|
|
6865
6878
|
try {
|
|
6866
6879
|
const binary = atob(body.replace(/\s/g, ""));
|
|
6867
6880
|
bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
6868
6881
|
} catch {
|
|
6869
|
-
|
|
6882
|
+
bytes = Uint8Array.from(body, (c) => c.charCodeAt(0) & 255);
|
|
6870
6883
|
}
|
|
6871
6884
|
} else if (encoding === "quoted-printable") {
|
|
6872
6885
|
const cleaned = body.replace(/=\r?\n/g, "");
|
|
@@ -6884,8 +6897,14 @@ function decodeBody(body, encoding, charset = "utf-8") {
|
|
|
6884
6897
|
} else if (encoding === "7bit" || encoding === "8bit" || encoding === "" || encoding === "binary") {
|
|
6885
6898
|
bytes = Uint8Array.from(body, (c) => c.charCodeAt(0) & 255);
|
|
6886
6899
|
} else {
|
|
6887
|
-
|
|
6900
|
+
bytes = Uint8Array.from(body, (c) => c.charCodeAt(0) & 255);
|
|
6888
6901
|
}
|
|
6902
|
+
return bytes;
|
|
6903
|
+
}
|
|
6904
|
+
function decodeBody(body, encoding, charset = "utf-8") {
|
|
6905
|
+
if (!["base64", "quoted-printable", "7bit", "8bit", "", "binary"].includes(encoding))
|
|
6906
|
+
return body;
|
|
6907
|
+
const bytes = decodeBodyBytes(body, encoding);
|
|
6889
6908
|
try {
|
|
6890
6909
|
const normalized = charset.toLowerCase().replace("windows-", "windows-").replace("iso-", "iso-");
|
|
6891
6910
|
return new TextDecoder(normalized).decode(bytes);
|
|
@@ -6923,11 +6942,21 @@ var WebMailxService = class _WebMailxService {
|
|
|
6923
6942
|
return this.db.getFolders(accountId);
|
|
6924
6943
|
}
|
|
6925
6944
|
// ── Messages ──
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6945
|
+
// 2026-09-04 — Claude Code (Opus 5), at Bob's direction ("it then says no
|
|
6946
|
+
// flagged messages. but there are message flagged" — 260 of them). Both of
|
|
6947
|
+
// these took two or seven parameters while api-client has always CALLED
|
|
6948
|
+
// them with four and nine: `flaggedOnly` and `dateBasis` fell off the end
|
|
6949
|
+
// and JavaScript said nothing. The star filter therefore existed on the
|
|
6950
|
+
// phone only as the client's `.flagged-only` CSS rule, which can hide
|
|
6951
|
+
// unflagged rows in the loaded page but cannot reach a starred message on
|
|
6952
|
+
// page 4 — so a list whose newest 50 carry no star reads as "no flagged
|
|
6953
|
+
// messages". Keep these signatures in step with api-client's calls; the
|
|
6954
|
+
// language will not do it for you.
|
|
6955
|
+
getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false, dateBasis = "sent") {
|
|
6956
|
+
return this.db.getUnifiedInbox(page, pageSize, flaggedOnly, dateBasis);
|
|
6957
|
+
}
|
|
6958
|
+
getMessages(accountId, folderId, page = 1, pageSize = 50, sort = "date", sortDir = "desc", search, flaggedOnly = false, dateBasis = "sent") {
|
|
6959
|
+
return this.db.getMessages({ accountId, folderId, page, pageSize, sort, sortDir, search, flaggedOnly, dateBasis });
|
|
6931
6960
|
}
|
|
6932
6961
|
async getMessage(accountId, uid, allowRemote = false, folderId) {
|
|
6933
6962
|
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
@@ -7486,11 +7515,49 @@ var WebMailxService = class _WebMailxService {
|
|
|
7486
7515
|
async getMessageSource(_accountId, _uid, _folderId) {
|
|
7487
7516
|
return this.notImpl("getMessageSource");
|
|
7488
7517
|
}
|
|
7489
|
-
|
|
7490
|
-
|
|
7518
|
+
/** The bytes of one attachment, base64 for the wire — the same shape the
|
|
7519
|
+
* desktop jsonrpc `getAttachment` case returns, because the same viewer
|
|
7520
|
+
* code consumes both.
|
|
7521
|
+
*
|
|
7522
|
+
* 2026-09-04 — Claude Code (Opus 5), at Bob's direction ("I tried opening
|
|
7523
|
+
* a pdf attachment on Android and it said 'opening' but nothing
|
|
7524
|
+
* happened"). This was a `notImpl` stub sitting immediately above a
|
|
7525
|
+
* comment explaining that Android opens attachments through the native
|
|
7526
|
+
* bridge "— getAttachment + base64 hand-off covers it". Both statements
|
|
7527
|
+
* were in the file at once, and the second was describing this method.
|
|
7528
|
+
* The viewer's Android branch does what that comment says: it calls
|
|
7529
|
+
* getAttachment for the bytes and hands them to
|
|
7530
|
+
* `_nativeBridge.openAttachment`. The throw landed in the chip's catch,
|
|
7531
|
+
* which posts a banner and restores the label — so from the phone it read
|
|
7532
|
+
* as "Opening…" and then nothing. Log line, verbatim:
|
|
7533
|
+
* `Couldn't open "…rev159-PRIVATE-DRAFT.pdf": Not implemented on Android:
|
|
7534
|
+
* getAttachment`.
|
|
7535
|
+
*
|
|
7536
|
+
* `attachmentId` is the chip's id, which getMessage assigns as the index
|
|
7537
|
+
* into this same parser's attachment list — so the two must stay the same
|
|
7538
|
+
* parse. Re-reading the body rather than caching it keeps that true even
|
|
7539
|
+
* if the message is re-fetched between opening it and tapping the chip. */
|
|
7540
|
+
async getAttachment(accountId, uid, attachmentId, folderId) {
|
|
7541
|
+
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
7542
|
+
if (!envelope)
|
|
7543
|
+
throw new Error("Message not found");
|
|
7544
|
+
const raw = await this.syncManager.fetchMessageBody(accountId, envelope.folderId, envelope.uid);
|
|
7545
|
+
if (!raw)
|
|
7546
|
+
throw new Error("Message body isn't downloaded yet \u2014 open the message, then try the attachment again.");
|
|
7547
|
+
const parsed = parseEmailSource(new TextDecoder().decode(raw));
|
|
7548
|
+
const att = parsed.attachments?.[attachmentId];
|
|
7549
|
+
if (!att)
|
|
7550
|
+
throw new Error(`Attachment ${attachmentId} not found in this message`);
|
|
7551
|
+
return {
|
|
7552
|
+
content: base64FromBytes(att.content),
|
|
7553
|
+
contentType: att.contentType || "application/octet-stream",
|
|
7554
|
+
filename: (att.filename || "attachment").replace(/"/g, "")
|
|
7555
|
+
};
|
|
7491
7556
|
}
|
|
7492
7557
|
// Android opens attachments via the native bridge (_nativeBridge.openAttachment),
|
|
7493
|
-
//
|
|
7558
|
+
// which the viewer reaches directly with the bytes getAttachment returns
|
|
7559
|
+
// above; there is no service-side "save it and ask the OS to open it" on a
|
|
7560
|
+
// phone.
|
|
7494
7561
|
async openAttachment(_accountId, _uid, _attachmentId, _folderId) {
|
|
7495
7562
|
return this.notImpl("openAttachment");
|
|
7496
7563
|
}
|