@bobfrankston/rmfmail 1.2.301 → 1.2.302
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 +49 -29
- package/client/android-bootstrap.bundle.js +60 -9
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/npmchanges.md +32 -0
- package/package.json +4 -4
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/web-service.d.ts +24 -2
- package/packages/mailx-store-web/web-service.d.ts.map +1 -1
- package/packages/mailx-store-web/web-service.js +81 -11
- package/packages/mailx-store-web/web-service.js.map +1 -1
- package/packages/mailx-store-web/web-service.ts +79 -11
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-69280 → node_modules.npmglobalize-stash-54400}/.package-lock.json +0 -0
package/.commitmsg
CHANGED
|
@@ -1,29 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
the
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
@@ -6841,13 +6841,13 @@ function parseMimeParts(body, boundary, topHeaders) {
|
|
|
6841
6841
|
}
|
|
6842
6842
|
if (disposition.includes("attachment") || partType.includes("application/") && !partType.includes("text/")) {
|
|
6843
6843
|
const filenameMatch = disposition.match(/filename="?([^";\r\n]+)"?/i) || partType.match(/name="?([^";\r\n]+)"?/i);
|
|
6844
|
-
const
|
|
6844
|
+
const content = decodeBodyBytes(partBody, partEncoding);
|
|
6845
6845
|
attachments.push({
|
|
6846
6846
|
filename: filenameMatch?.[1]?.trim() || `attachment-${attachments.length}`,
|
|
6847
6847
|
contentType: partType.split(";")[0].trim(),
|
|
6848
|
-
size:
|
|
6848
|
+
size: content.length,
|
|
6849
6849
|
contentId: (partHeaders.get("content-id") || "").replace(/[<>]/g, ""),
|
|
6850
|
-
content
|
|
6850
|
+
content
|
|
6851
6851
|
});
|
|
6852
6852
|
} else if (partType.includes("text/html")) {
|
|
6853
6853
|
const charsetMatch = partType.match(/charset="?([^";\s]+)"?/i);
|
|
@@ -6859,14 +6859,21 @@ function parseMimeParts(body, boundary, topHeaders) {
|
|
|
6859
6859
|
}
|
|
6860
6860
|
return { html, text, headers: topHeaders, attachments };
|
|
6861
6861
|
}
|
|
6862
|
-
function
|
|
6862
|
+
function base64FromBytes(bytes) {
|
|
6863
|
+
const CHUNK = 32768;
|
|
6864
|
+
let binary = "";
|
|
6865
|
+
for (let i = 0; i < bytes.length; i += CHUNK)
|
|
6866
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
6867
|
+
return btoa(binary);
|
|
6868
|
+
}
|
|
6869
|
+
function decodeBodyBytes(body, encoding) {
|
|
6863
6870
|
let bytes;
|
|
6864
6871
|
if (encoding === "base64") {
|
|
6865
6872
|
try {
|
|
6866
6873
|
const binary = atob(body.replace(/\s/g, ""));
|
|
6867
6874
|
bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
6868
6875
|
} catch {
|
|
6869
|
-
|
|
6876
|
+
bytes = Uint8Array.from(body, (c) => c.charCodeAt(0) & 255);
|
|
6870
6877
|
}
|
|
6871
6878
|
} else if (encoding === "quoted-printable") {
|
|
6872
6879
|
const cleaned = body.replace(/=\r?\n/g, "");
|
|
@@ -6884,8 +6891,14 @@ function decodeBody(body, encoding, charset = "utf-8") {
|
|
|
6884
6891
|
} else if (encoding === "7bit" || encoding === "8bit" || encoding === "" || encoding === "binary") {
|
|
6885
6892
|
bytes = Uint8Array.from(body, (c) => c.charCodeAt(0) & 255);
|
|
6886
6893
|
} else {
|
|
6887
|
-
|
|
6894
|
+
bytes = Uint8Array.from(body, (c) => c.charCodeAt(0) & 255);
|
|
6888
6895
|
}
|
|
6896
|
+
return bytes;
|
|
6897
|
+
}
|
|
6898
|
+
function decodeBody(body, encoding, charset = "utf-8") {
|
|
6899
|
+
if (!["base64", "quoted-printable", "7bit", "8bit", "", "binary"].includes(encoding))
|
|
6900
|
+
return body;
|
|
6901
|
+
const bytes = decodeBodyBytes(body, encoding);
|
|
6889
6902
|
try {
|
|
6890
6903
|
const normalized = charset.toLowerCase().replace("windows-", "windows-").replace("iso-", "iso-");
|
|
6891
6904
|
return new TextDecoder(normalized).decode(bytes);
|
|
@@ -7486,11 +7499,49 @@ var WebMailxService = class _WebMailxService {
|
|
|
7486
7499
|
async getMessageSource(_accountId, _uid, _folderId) {
|
|
7487
7500
|
return this.notImpl("getMessageSource");
|
|
7488
7501
|
}
|
|
7489
|
-
|
|
7490
|
-
|
|
7502
|
+
/** The bytes of one attachment, base64 for the wire — the same shape the
|
|
7503
|
+
* desktop jsonrpc `getAttachment` case returns, because the same viewer
|
|
7504
|
+
* code consumes both.
|
|
7505
|
+
*
|
|
7506
|
+
* 2026-09-04 — Claude Code (Opus 5), at Bob's direction ("I tried opening
|
|
7507
|
+
* a pdf attachment on Android and it said 'opening' but nothing
|
|
7508
|
+
* happened"). This was a `notImpl` stub sitting immediately above a
|
|
7509
|
+
* comment explaining that Android opens attachments through the native
|
|
7510
|
+
* bridge "— getAttachment + base64 hand-off covers it". Both statements
|
|
7511
|
+
* were in the file at once, and the second was describing this method.
|
|
7512
|
+
* The viewer's Android branch does what that comment says: it calls
|
|
7513
|
+
* getAttachment for the bytes and hands them to
|
|
7514
|
+
* `_nativeBridge.openAttachment`. The throw landed in the chip's catch,
|
|
7515
|
+
* which posts a banner and restores the label — so from the phone it read
|
|
7516
|
+
* as "Opening…" and then nothing. Log line, verbatim:
|
|
7517
|
+
* `Couldn't open "…rev159-PRIVATE-DRAFT.pdf": Not implemented on Android:
|
|
7518
|
+
* getAttachment`.
|
|
7519
|
+
*
|
|
7520
|
+
* `attachmentId` is the chip's id, which getMessage assigns as the index
|
|
7521
|
+
* into this same parser's attachment list — so the two must stay the same
|
|
7522
|
+
* parse. Re-reading the body rather than caching it keeps that true even
|
|
7523
|
+
* if the message is re-fetched between opening it and tapping the chip. */
|
|
7524
|
+
async getAttachment(accountId, uid, attachmentId, folderId) {
|
|
7525
|
+
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
7526
|
+
if (!envelope)
|
|
7527
|
+
throw new Error("Message not found");
|
|
7528
|
+
const raw = await this.syncManager.fetchMessageBody(accountId, envelope.folderId, envelope.uid);
|
|
7529
|
+
if (!raw)
|
|
7530
|
+
throw new Error("Message body isn't downloaded yet \u2014 open the message, then try the attachment again.");
|
|
7531
|
+
const parsed = parseEmailSource(new TextDecoder().decode(raw));
|
|
7532
|
+
const att = parsed.attachments?.[attachmentId];
|
|
7533
|
+
if (!att)
|
|
7534
|
+
throw new Error(`Attachment ${attachmentId} not found in this message`);
|
|
7535
|
+
return {
|
|
7536
|
+
content: base64FromBytes(att.content),
|
|
7537
|
+
contentType: att.contentType || "application/octet-stream",
|
|
7538
|
+
filename: (att.filename || "attachment").replace(/"/g, "")
|
|
7539
|
+
};
|
|
7491
7540
|
}
|
|
7492
7541
|
// Android opens attachments via the native bridge (_nativeBridge.openAttachment),
|
|
7493
|
-
//
|
|
7542
|
+
// which the viewer reaches directly with the bytes getAttachment returns
|
|
7543
|
+
// above; there is no service-side "save it and ask the OS to open it" on a
|
|
7544
|
+
// phone.
|
|
7494
7545
|
async openAttachment(_accountId, _uid, _attachmentId, _folderId) {
|
|
7495
7546
|
return this.notImpl("openAttachment");
|
|
7496
7547
|
}
|