From 368c90ce3e1fb3c314ea5fda8bc97a05efa16b2f Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:37:04 +0100 Subject: [PATCH] fix: stop crafted .eml headers from stalling the parser by moving to postal-mime 4.2 --- package-lock.json | 13 ++- package.json | 2 +- patches/postal-mime+3.0.0.patch | 82 --------------- patches/postal-mime+4.2.0.patch | 128 +++++++++++++++++++++++ src/lib/__tests__/postal-mime-rn.test.ts | 2 +- src/lib/licences.json | 2 +- 6 files changed, 139 insertions(+), 90 deletions(-) delete mode 100644 patches/postal-mime+3.0.0.patch create mode 100644 patches/postal-mime+4.2.0.patch diff --git a/package-lock.json b/package-lock.json index 6f01d072..711f1db1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "expo-status-bar": "~3.0.9", "expo-web-browser": "~15.0.8", "jszip": "^3.10.1", - "postal-mime": "^3.0.0", + "postal-mime": "^4.2.0", "react": "19.1.0", "react-native": "0.81.5", "react-native-android-widget": "0.22.1", @@ -8940,10 +8940,13 @@ } }, "node_modules/postal-mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-3.0.0.tgz", - "integrity": "sha512-Z4a9ar2Bv3YpK3IXag+Yda30k7bMZfpRuUGyqtHnZ2pjHG8Bl62EhZIk4n1dzv00gfzP9g+94e9kd8+XmjVWLA==", - "license": "MIT-0" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-4.2.0.tgz", + "integrity": "sha512-QG5Rh8wY/zjwFF1lKWcBbwWcAWR5jWodmMPL4gMik43XDLebtWoAAPyG0lrE2kYYZ5IHTASrepGuGO0YIwNKAg==", + "license": "MIT-0", + "engines": { + "node": ">=20" + } }, "node_modules/postcss": { "version": "8.4.49", diff --git a/package.json b/package.json index 6dc0afd7..ffcbd94f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "expo-status-bar": "~3.0.9", "expo-web-browser": "~15.0.8", "jszip": "^3.10.1", - "postal-mime": "^3.0.0", + "postal-mime": "^4.2.0", "react": "19.1.0", "react-native": "0.81.5", "react-native-android-widget": "0.22.1", diff --git a/patches/postal-mime+3.0.0.patch b/patches/postal-mime+3.0.0.patch deleted file mode 100644 index a9a976d3..00000000 --- a/patches/postal-mime+3.0.0.patch +++ /dev/null @@ -1,82 +0,0 @@ -diff --git a/node_modules/postal-mime/src/base64-decoder.js b/node_modules/postal-mime/src/base64-decoder.js -index 93c2aea..ba6e3f1 100644 ---- a/node_modules/postal-mime/src/base64-decoder.js -+++ b/node_modules/postal-mime/src/base64-decoder.js -@@ -1,4 +1,4 @@ --import { decodeBase64, blobToArrayBuffer } from './decode-strings.js'; -+import { decodeBase64, concatChunks } from './decode-strings.js'; - - export default class Base64Decoder { - constructor(opts) { -@@ -49,6 +49,6 @@ export default class Base64Decoder { - finalize() { - this.flushRemainder(); - -- return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' })); -+ return concatChunks(this.chunks); - } - } -diff --git a/node_modules/postal-mime/src/decode-strings.js b/node_modules/postal-mime/src/decode-strings.js -index a5e058e..f255e8d 100644 ---- a/node_modules/postal-mime/src/decode-strings.js -+++ b/node_modules/postal-mime/src/decode-strings.js -@@ -145,6 +145,23 @@ export function getDecoder(charset) { - * @param {Blob} blob Blob to convert - * @returns {ArrayBuffer} Converted value - */ -+// React Native's Blob cannot be built from ArrayBuffers or typed arrays, so -+// the decoders join their chunks here instead of through a Blob. -+export function concatChunks(chunks) { -+ const parts = chunks.map(chunk => { -+ if (typeof chunk === 'string') return textEncoder.encode(chunk); -+ if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk); -+ return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); -+ }); -+ const out = new Uint8Array(parts.reduce((n, part) => n + part.byteLength, 0)); -+ let pos = 0; -+ for (const part of parts) { -+ out.set(part, pos); -+ pos += part.byteLength; -+ } -+ return out.buffer; -+} -+ - export async function blobToArrayBuffer(blob) { - if ('arrayBuffer' in blob) { - return await blob.arrayBuffer(); -diff --git a/node_modules/postal-mime/src/pass-through-decoder.js b/node_modules/postal-mime/src/pass-through-decoder.js -index ad1c222..1e1d012 100644 ---- a/node_modules/postal-mime/src/pass-through-decoder.js -+++ b/node_modules/postal-mime/src/pass-through-decoder.js -@@ -1,4 +1,4 @@ --import { blobToArrayBuffer } from './decode-strings.js'; -+import { concatChunks } from './decode-strings.js'; - - export default class PassThroughDecoder { - constructor() { -@@ -12,6 +12,6 @@ export default class PassThroughDecoder { - - finalize() { - // convert an array of arraybuffers into a blob and then back into a single arraybuffer -- return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' })); -+ return concatChunks(this.chunks); - } - } -diff --git a/node_modules/postal-mime/src/qp-decoder.js b/node_modules/postal-mime/src/qp-decoder.js -index adee72c..3973673 100644 ---- a/node_modules/postal-mime/src/qp-decoder.js -+++ b/node_modules/postal-mime/src/qp-decoder.js -@@ -1,4 +1,4 @@ --import { blobToArrayBuffer, hexNibble } from './decode-strings.js'; -+import { concatChunks, hexNibble } from './decode-strings.js'; - - const CHR_EQUALS = 0x3d; - const CHR_LF = 0x0a; -@@ -83,6 +83,6 @@ export default class QPDecoder { - this.flushBuffer(); - - // convert an array of arraybuffers into a blob and then back into a single arraybuffer -- return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' })); -+ return concatChunks(this.chunks); - } - } diff --git a/patches/postal-mime+4.2.0.patch b/patches/postal-mime+4.2.0.patch new file mode 100644 index 00000000..bc17b239 --- /dev/null +++ b/patches/postal-mime+4.2.0.patch @@ -0,0 +1,128 @@ +diff --git a/node_modules/postal-mime/dist/cjs/chunk-writer.js b/node_modules/postal-mime/dist/cjs/chunk-writer.js +index aac95eb..3425832 100644 +--- a/node_modules/postal-mime/dist/cjs/chunk-writer.js ++++ b/node_modules/postal-mime/dist/cjs/chunk-writer.js +@@ -57,7 +57,7 @@ class ChunkWriter { + finalize() { + this.flush(); + // convert an array of arraybuffers into a blob and then back into a single arraybuffer +- return (0, decode_strings_js_1.blobToArrayBuffer)(new Blob(this.chunks, { type: 'application/octet-stream' })); ++ return (0, decode_strings_js_1.concatChunks)(this.chunks); + } + } + exports.default = ChunkWriter; +diff --git a/node_modules/postal-mime/dist/cjs/decode-strings.js b/node_modules/postal-mime/dist/cjs/decode-strings.js +index 40c59f6..74b0c41 100644 +--- a/node_modules/postal-mime/dist/cjs/decode-strings.js ++++ b/node_modules/postal-mime/dist/cjs/decode-strings.js +@@ -8,6 +8,7 @@ exports.decodeUtf8OrWindows1252 = decodeUtf8OrWindows1252; + exports.decodeCharset = decodeCharset; + exports.decodeText = decodeText; + exports.blobToArrayBuffer = blobToArrayBuffer; ++exports.concatChunks = concatChunks; + exports.hexNibble = hexNibble; + exports.foldLineBreaks = foldLineBreaks; + exports.decodeWord = decodeWord; +@@ -461,6 +462,22 @@ function decodeText(bytes, charset, html) { + } + return (decoder.encoding === 'x-user-defined' ? getDecoder('windows-1252') : decoder).decode(view); + } ++// React Native's Blob cannot be built from ArrayBuffers or typed arrays, so ++// the decoders join their chunks here instead of through a Blob. ++function concatChunks(chunks) { ++ const parts = chunks.map(chunk => { ++ if (typeof chunk === 'string') return exports.textEncoder.encode(chunk); ++ if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk); ++ return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); ++ }); ++ const out = new Uint8Array(parts.reduce((n, part) => n + part.byteLength, 0)); ++ let pos = 0; ++ for (const part of parts) { ++ out.set(part, pos); ++ pos += part.byteLength; ++ } ++ return out.buffer; ++} + /** + * Converts a Blob into an ArrayBuffer + * @param blob Blob to convert +diff --git a/node_modules/postal-mime/dist/cjs/pass-through-decoder.js b/node_modules/postal-mime/dist/cjs/pass-through-decoder.js +index 755a1a7..d7f4ebc 100644 +--- a/node_modules/postal-mime/dist/cjs/pass-through-decoder.js ++++ b/node_modules/postal-mime/dist/cjs/pass-through-decoder.js +@@ -68,7 +68,7 @@ class PassThroughDecoder { + } + this.flushRun(); + // convert an array of arraybuffers into a blob and then back into a single arraybuffer +- return (0, decode_strings_js_1.blobToArrayBuffer)(new Blob(this.chunks, { type: 'application/octet-stream' })); ++ return (0, decode_strings_js_1.concatChunks)(this.chunks); + } + } + exports.default = PassThroughDecoder; +diff --git a/node_modules/postal-mime/dist/esm/chunk-writer.js b/node_modules/postal-mime/dist/esm/chunk-writer.js +index eb22f8a..ceebace 100644 +--- a/node_modules/postal-mime/dist/esm/chunk-writer.js ++++ b/node_modules/postal-mime/dist/esm/chunk-writer.js +@@ -1,4 +1,4 @@ +-import { blobToArrayBuffer } from './decode-strings.js'; ++import { concatChunks } from './decode-strings.js'; + /** + * Collects decoded content in fixed size chunks, so memory follows the number of bytes + * written rather than the number of writes. Decoders that write many short runs, such as +@@ -55,7 +55,7 @@ export default class ChunkWriter { + finalize() { + this.flush(); + // convert an array of arraybuffers into a blob and then back into a single arraybuffer +- return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' })); ++ return concatChunks(this.chunks); + } + } + //# sourceMappingURL=chunk-writer.js.map +\ No newline at end of file +diff --git a/node_modules/postal-mime/dist/esm/decode-strings.js b/node_modules/postal-mime/dist/esm/decode-strings.js +index 7108f8d..3dbd459 100644 +--- a/node_modules/postal-mime/dist/esm/decode-strings.js ++++ b/node_modules/postal-mime/dist/esm/decode-strings.js +@@ -443,6 +443,22 @@ export function decodeText(bytes, charset, html) { + } + return (decoder.encoding === 'x-user-defined' ? getDecoder('windows-1252') : decoder).decode(view); + } ++// React Native's Blob cannot be built from ArrayBuffers or typed arrays, so ++// the decoders join their chunks here instead of through a Blob. ++export function concatChunks(chunks) { ++ const parts = chunks.map(chunk => { ++ if (typeof chunk === 'string') return textEncoder.encode(chunk); ++ if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk); ++ return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); ++ }); ++ const out = new Uint8Array(parts.reduce((n, part) => n + part.byteLength, 0)); ++ let pos = 0; ++ for (const part of parts) { ++ out.set(part, pos); ++ pos += part.byteLength; ++ } ++ return out.buffer; ++} + /** + * Converts a Blob into an ArrayBuffer + * @param blob Blob to convert +diff --git a/node_modules/postal-mime/dist/esm/pass-through-decoder.js b/node_modules/postal-mime/dist/esm/pass-through-decoder.js +index 4d493af..39e6ad5 100644 +--- a/node_modules/postal-mime/dist/esm/pass-through-decoder.js ++++ b/node_modules/postal-mime/dist/esm/pass-through-decoder.js +@@ -1,4 +1,4 @@ +-import { blobToArrayBuffer } from './decode-strings.js'; ++import { concatChunks } from './decode-strings.js'; + // Hands the body through as it is, the line endings included. + // + // The line break is written when the next line arrives, not after each line. The CRLF in +@@ -66,7 +66,7 @@ export default class PassThroughDecoder { + } + this.flushRun(); + // convert an array of arraybuffers into a blob and then back into a single arraybuffer +- return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' })); ++ return concatChunks(this.chunks); + } + } + //# sourceMappingURL=pass-through-decoder.js.map +\ No newline at end of file diff --git a/src/lib/__tests__/postal-mime-rn.test.ts b/src/lib/__tests__/postal-mime-rn.test.ts index 03f60e9c..804c6003 100644 --- a/src/lib/__tests__/postal-mime-rn.test.ts +++ b/src/lib/__tests__/postal-mime-rn.test.ts @@ -4,7 +4,7 @@ import PostalMime from 'postal-mime'; // React Native's Blob only takes strings and other Blobs; building one from an // ArrayBuffer or typed array throws. postal-mime used to join every decoded // part through such a Blob, so an .eml with an attachment could not be -// previewed on the device (patches/postal-mime+3.0.0.patch). +// previewed on the device (patches/postal-mime+4.2.0.patch). class ReactNativeBlob { constructor(parts: unknown[] = []) { if (parts.some((p) => p instanceof ArrayBuffer || ArrayBuffer.isView(p))) { diff --git a/src/lib/licences.json b/src/lib/licences.json index 0108c0ed..c6d5702a 100644 --- a/src/lib/licences.json +++ b/src/lib/licences.json @@ -150,7 +150,7 @@ }, { "name": "postal-mime", - "version": "3.0.0", + "version": "4.2.0", "license": "MIT-0", "url": "https://postal-mime.postalsys.com" },