@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 CHANGED
@@ -1,29 +1,47 @@
1
- Placing the caret in the body is the same act as taking focus
1
+ The phone's star filter was never asked for, and the chip announcing it landed in a 28px column
2
2
 
3
- Ctrl+N opens compose with the cursor in the message body instead of the To
4
- field (Bob 2026-09-03: "^n (and forward?) still start me in the body rather
5
- than the to field" and yes, forward too).
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
- applyInit made the decision twice. First it seeded the body and called
8
- `editor.setCursor(0)`; twenty lines later it picked the first empty field and
9
- focused it. Reading top to bottom the second one wins, and it does not:
10
- `setCursor` is not a caret-placement call, it is a focus call. rmf-tiny's
11
- implementation ends with `editor.focus()` and then re-runs the whole placement
12
- inside `requestAnimationFrame` deliberately, to survive the nested-iframe
13
- focus race that produced the old "have to click in and try again" symptom — so
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
- So the two are now one decision, made once and up front: `focusTarget` is the
19
- first field the user still has to fill, To Subject body, and `setCursor` is
20
- asked for only when the answer is the body. On a reply that is still the body —
21
- To and Subject are filled, and the call also scrolls the quote out of view so
22
- the empty line is what's on screen, which is the behaviour it exists for. On a
23
- forward and on new mail it is the To field, untouched.
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
- Forward has been doing this since it grew a quoted body. Ctrl+N joined it in
26
- v1.2.298, three hours ago: before the account signature reached compose,
27
- `bodyToRender` was empty for new mail and the branch holding `setCursor` never
28
- ran at all. The first message anyone composed with a signature was also the
29
- first one that opened with the cursor in the wrong place.
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) {
@@ -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 decoded = decodeBody(partBody, partEncoding);
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: decoded.length,
6854
+ size: content.length,
6849
6855
  contentId: (partHeaders.get("content-id") || "").replace(/[<>]/g, ""),
6850
- content: new TextEncoder().encode(decoded)
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 decodeBody(body, encoding, charset = "utf-8") {
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
- return body;
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
- return body;
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
- getUnifiedInbox(page = 1, pageSize = 50) {
6927
- return this.db.getUnifiedInbox(page, pageSize);
6928
- }
6929
- getMessages(accountId, folderId, page = 1, pageSize = 50, sort = "date", sortDir = "desc", search) {
6930
- 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 });
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
- async getAttachment(_accountId, _uid, _attachmentId, _folderId) {
7490
- return this.notImpl("getAttachment");
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
- // not this service path getAttachment + base64 hand-off covers it.
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
  }