@bobfrankston/rmfmail 1.2.302 → 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 CHANGED
@@ -1,49 +1,47 @@
1
- Opening a PDF on the phone: a stub, and bytes laundered through UTF-8
2
-
3
- "I tried opening a pdf attachment on Android and it said 'opening' but nothing
4
- happened" (Bob 2026-09-04). In the Android log, verbatim:
5
-
6
- Couldn't open "260901-THE-AI-COMPUTE-RACE-…-PRIVATE-DRAFT.pdf":
7
- Not implemented on Android: getAttachment
8
-
9
- Two independent bugs, and only the first was reachable.
10
-
11
- **getAttachment was a `notImpl` stub.** It sat one line above a comment that
12
- read "Android opens attachments via the native bridge getAttachment + base64
13
- hand-off covers it": a correct description of the design, immediately below the
14
- throw that made the design impossible. The viewer's Android branch does exactly
15
- what the comment says fetch the bytes, hand them to
16
- `_nativeBridge.openAttachment` so the throw landed in the chip's catch, which
17
- posts a banner and restores the label after 600ms. From the phone that is
18
- "Opening…", and then the chip goes back to normal. It is now implemented: read
19
- the body through the sync manager, parse, index the attachment the same way
20
- getMessage numbered the chips, return base64 in the shape the desktop jsonrpc
21
- returns.
22
-
23
- **The parser was destroying the bytes anyway.** The attachment branch called
24
- `decodeBody()` which decodes the transfer encoding to bytes and then decodes
25
- THOSE as UTF-8 text and re-encoded the result with `TextEncoder`. Every byte
26
- sequence that is not valid UTF-8 became U+FFFD and came back as the three bytes
27
- EF BF BD. A PDF, an image or a zip would have arrived corrupt and LONGER than
28
- it started, with `size` wrong to match, the moment the stub was filled in. The
29
- transfer-encoding step is now `decodeBodyBytes()` and stops at bytes;
30
- `decodeBody` is that plus the charset step, so the encodings still have one
31
- implementation. Attachments take the bytes. Text parts are unaffected.
32
-
33
- Verified against the actual message: the web parser and mailparser now produce
34
- byte-identical output for that PDF 3,307,996 bytes, `%PDF-` to `%%EOF`.
35
- tests/attachment-bytes.test.ts covers it with a fixture built from bytes no
36
- UTF-8 decoder can round-trip, asserting the whole buffer rather than a prefix,
37
- because the broken version also started with "%PDF".
38
-
39
- **One more thing that had never run.** `app/openattachment` in MainPage.xaml.cs
40
- is the only place in the bridge that reads a RETURN VALUE from JS; every other
41
- path pushes C#→JS. Since the JS threw first, that code has never once executed.
42
- Android's evaluateJavascript hands back a JSON-encoded value, so the base64
43
- arrives quoted and with `=` liable to appear as = — undone before decode,
44
- and a FormatException now logs the length and first characters of what actually
45
- arrived instead of dying as "invalid base64".
46
-
47
- Untested from here: the 3.3 MB hand-off itself. The bytes are correct and the
48
- call is wired; whether a payload that size survives one EvaluateJavaScriptAsync
49
- can only be answered on the device. If it does not, the new log line names it.
1
+ The phone's star filter was never asked for, and the chip announcing it landed in a 28px column
2
+
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
+
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.
14
+
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.
23
+
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 sortCol = query.sort === "from" ? "from_name" : query.sort === "subject" ? "subject" : "date";
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 countRow = this.get(`SELECT COUNT(*) as cnt FROM messages WHERE folder_id IN (${placeholders})`, ids);
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 date DESC LIMIT ? OFFSET ?`, [...ids, pageSize, offset]);
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) {
@@ -6936,11 +6942,21 @@ var WebMailxService = class _WebMailxService {
6936
6942
  return this.db.getFolders(accountId);
6937
6943
  }
6938
6944
  // ── Messages ──
6939
- getUnifiedInbox(page = 1, pageSize = 50) {
6940
- return this.db.getUnifiedInbox(page, pageSize);
6941
- }
6942
- getMessages(accountId, folderId, page = 1, pageSize = 50, sort = "date", sortDir = "desc", search) {
6943
- return this.db.getMessages({ accountId, folderId, page, pageSize, sort, sortDir, search });
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 });
6944
6960
  }
6945
6961
  async getMessage(accountId, uid, allowRemote = false, folderId) {
6946
6962
  const envelope = this.db.getMessageByUid(accountId, uid, folderId);