From 5f0c882aaaa98d05f85230cb2ee867f18e409392 Mon Sep 17 00:00:00 2001 From: Vitali Lovich Date: Mon, 31 Jan 2022 20:01:24 -0800 Subject: [PATCH 1/3] Fix incorrect handling of latin1 without iconv Wrong regexp was used so if iconv is not present then the tests fail. --- lib/rfc2047.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rfc2047.js b/lib/rfc2047.js index 7ca5cc9..cdb6f38 100644 --- a/lib/rfc2047.js +++ b/lib/rfc2047.js @@ -1,7 +1,7 @@ /* global unescape */ const isUtf8RegExp = /^utf-?8$/i; -const isLatin1RegExp = /^(?:iso-8859-1|latin1)$/i; +const isLatin1RegExp = /^(?:iso-8859-1|latin1|us-ascii)$/i; const iconvLite = require('iconv-lite'); const rfc2047 = (module.exports = {}); @@ -98,7 +98,7 @@ function decodeEncodedWord(encodedText, encoding, charset) { ) { return decoded; } - } else if (/^(?:us-)?ascii$/i.test(charset)) { + } else if (isLatin1RegExp.test(charset)) { return buffer.toString('ascii'); } else if (iconvLite.encodingExists(charset)) { decoded = iconvLite.decode(buffer, charset); From c372d0bdf527b73a7a090db10f8d129040e63f7a Mon Sep 17 00:00:00 2001 From: Vitali Lovich Date: Mon, 31 Jan 2022 17:40:10 -0800 Subject: [PATCH 2/3] Make iconv and iconv-lite support optional Don't try to require iconv on browser environments in the first place. Not sure how to make iconv-lite properly configurable at runtime though. --- lib/rfc2047.js | 21 ++++++++---- package.json | 5 ++- test/rfc2047.js | 91 +++++++++++++++++++++++++++++++++++-------------- 3 files changed, 84 insertions(+), 33 deletions(-) diff --git a/lib/rfc2047.js b/lib/rfc2047.js index cdb6f38..d6a153e 100644 --- a/lib/rfc2047.js +++ b/lib/rfc2047.js @@ -2,9 +2,21 @@ const isUtf8RegExp = /^utf-?8$/i; const isLatin1RegExp = /^(?:iso-8859-1|latin1|us-ascii)$/i; -const iconvLite = require('iconv-lite'); const rfc2047 = (module.exports = {}); +let iconv; +let iconvLite; + +try { + iconv = require('iconv'); +} catch (e) {} + +if (!iconv) { + try { + iconvLite = require('iconv-lite'); + } catch (e) {} +} + function stringify(obj) { if (typeof obj === 'string') { return obj; @@ -15,11 +27,6 @@ function stringify(obj) { } } -let iconv; -try { - iconv = require('' + 'iconv'); // Prevent browserify from detecting iconv and failing -} catch (e) {} - const replacementCharacterBuffer = Buffer.from('�'); function decodeBuffer(encodedText, encoding) { @@ -100,7 +107,7 @@ function decodeEncodedWord(encodedText, encoding, charset) { } } else if (isLatin1RegExp.test(charset)) { return buffer.toString('ascii'); - } else if (iconvLite.encodingExists(charset)) { + } else if (iconvLite && iconvLite.encodingExists(charset)) { decoded = iconvLite.decode(buffer, charset); if ( !/\ufffd/.test(decoded) || diff --git a/package.json b/package.json index 6bb8faf..530aaa7 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "iconv": "^3.0.0", "mocha": "2.1.0", "nyc": "^15.1.0", "prettier": "~2.4.1", @@ -50,8 +49,12 @@ "unexpected": "10.20.0" }, "dependencies": { + "iconv": "^3.0.0", "iconv-lite": "0.4.5" }, + "browser": { + "iconv": false + }, "nyc": { "include": [ "lib/**" diff --git a/test/rfc2047.js b/test/rfc2047.js index 59558a0..b3d5442 100644 --- a/test/rfc2047.js +++ b/test/rfc2047.js @@ -1,14 +1,47 @@ const unexpected = require('unexpected'); const proxyquire = require('proxyquire'); +const IconvType = { + Unavailable: 0, + LiteOnly: 1, + Available: 2, +}; + +function description(iconvType) { + switch (iconvType) { + case IconvType.Unavailable: + return 'iconv unavailable'; + case IconvType.LiteOnly: + return 'iconv-lite available'; + case IconvType.Available: + return 'iconv available'; + } +} + describe('rfc2047', () => { - for (const iconvAvailable of [false, true]) { - describe(`with iconv ${iconvAvailable ? '' : 'un'}available`, () => { - const rfc2047 = iconvAvailable - ? require('../lib/rfc2047') - : proxyquire('../lib/rfc2047', { - iconv: null, - }); + for (const iconvType of [ + IconvType.Unavailable, + IconvType.LiteOnly, + IconvType.Available, + ]) { + describe(`with ${description(iconvType)}`, () => { + const requireStubs = {}; + + switch (iconvType) { + case IconvType.Unavailable: + requireStubs['iconv'] = null; + requireStubs['iconv-lite'] = null; + break; + case IconvType.LiteOnly: + requireStubs['iconv'] = null; + break; + } + + const iconvAvailable = iconvType === IconvType.Available; + const iconvLiteAvailable = + iconvAvailable || iconvType === IconvType.LiteOnly; + + const rfc2047 = proxyquire('../lib/rfc2047', requireStubs); const expect = unexpected .clone() @@ -212,11 +245,13 @@ describe('rfc2047', () => { 'to decode to', 'Patrik Fältström ' ); - expect( - 'Nathaniel Borenstein (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)', - 'to decode to', - 'Nathaniel Borenstein (םולש ןב ילטפנ)' - ); + if (iconvLiteAvailable) { + expect( + 'Nathaniel Borenstein (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)', + 'to decode to', + 'Nathaniel Borenstein (םולש ןב ילטפנ)' + ); + } expect('(=?ISO-8859-1?Q?a?=)', 'to decode to', '(a)'); expect('(=?ISO-8859-1?Q?a?= b)', 'to decode to', '(a b)'); expect( @@ -230,11 +265,13 @@ describe('rfc2047', () => { '(ab)' ); expect('(=?ISO-8859-1?Q?a_b?=)', 'to decode to', '(a b)'); - expect( - '(=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)', - 'to decode to', - '(a b)' - ); + if (iconvLiteAvailable) { + expect( + '(=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)', + 'to decode to', + '(a b)' + ); + } }); it('should handle subject found in mail with X-Mailer: MailChimp Mailer', () => { @@ -327,17 +364,21 @@ describe('rfc2047', () => { expect( '=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?==?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?==?US-ASCII?Q?.._cool!?=', 'to decode to', - 'If you can read this you understand the example... cool!' + iconvLiteAvailable + ? 'If you can read this you understand the example... cool!' + : 'If you can read this yo=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=.. cool!' ); }); - it('should handle a file name found in a Korean mail', () => { - expect( - '=?ks_c_5601-1987?B?MTMwMTE3X8HWwvfA5V+1tcDlX7jetLq+8y5wZGY=?=', - 'to decode to', - '130117_주차장_도장_메뉴얼.pdf' - ); - }); + if (iconvLiteAvailable) { + it('should handle a file name found in a Korean mail', () => { + expect( + '=?ks_c_5601-1987?B?MTMwMTE3X8HWwvfA5V+1tcDlX7jetLq+8y5wZGY=?=', + 'to decode to', + '130117_주차장_도장_메뉴얼.pdf' + ); + }); + } it('should handle bogus encoded words (spotted in the wild)', () => { expect( From 941ae92911ee7649f0ec8d74ba25052261eb7cfb Mon Sep 17 00:00:00 2001 From: Vitali Lovich Date: Tue, 1 Feb 2022 13:14:53 -0800 Subject: [PATCH 3/3] Make the package compatible with browser targets natively Remove the need for a polyfill by providing an implementation that uses web APIs. --- lib/browser-buffer-ops.js | 42 +++ lib/node-buffer-ops.js | 21 ++ lib/rfc2047.js | 21 +- package.json | 3 +- test/rfc2047.js | 664 +++++++++++++++++++------------------- 5 files changed, 410 insertions(+), 341 deletions(-) create mode 100644 lib/browser-buffer-ops.js create mode 100644 lib/node-buffer-ops.js diff --git a/lib/browser-buffer-ops.js b/lib/browser-buffer-ops.js new file mode 100644 index 0000000..a1e09d9 --- /dev/null +++ b/lib/browser-buffer-ops.js @@ -0,0 +1,42 @@ +const replacementCharacterBuffer = new Uint8Array([0xef, 0xbf, 0xbd]); + +const base64js = require('base64-js'); + +module.exports = { + fromBase64: (b64str) => { + try { + return base64js.toByteArray(b64str); + } catch (e) { + return new Uint8Array(); + } + }, + + toUtf8: TextEncoder.prototype.encode.bind(new TextEncoder()), + fromUtf8: TextDecoder.prototype.decode.bind(new TextDecoder()), + fromAscii: TextDecoder.prototype.decode.bind(new TextDecoder('ascii')), + + allocByteBuffer: (length) => { + return new Uint8Array(length); + }, + + includesReplacementCharacter: (haystack) => { + const needle = replacementCharacterBuffer; + if (haystack.length < needle.length) { + return false; + } + let fromIndex = 0; + while (fromIndex !== haystack.length - 3) { + const foundFirst = haystack[fromIndex] === needle[0]; + const foundSecond = haystack[fromIndex + 1] === needle[1]; + const foundThird = haystack[fromIndex + 2] === needle[2]; + + if (foundFirst && foundSecond && foundThird) { + return true; + } else { + fromIndex += 1; + } + } + + return false; + }, +}; diff --git a/lib/node-buffer-ops.js b/lib/node-buffer-ops.js new file mode 100644 index 0000000..9953da2 --- /dev/null +++ b/lib/node-buffer-ops.js @@ -0,0 +1,21 @@ +const replacementCharacterBuffer = new Uint8Array([0xef, 0xbf, 0xbd]); + +module.exports = { + fromBase64: (b64str) => { + return Buffer.from(b64str, 'base64'); + }, + + toUtf8: (str) => { + return Buffer.from(str, 'utf-8'); + }, + fromUtf8: String, + fromAscii: (buffer) => { + return buffer.toString('ascii'); + }, + + allocByteBuffer: Buffer.alloc, + + includesReplacementCharacter: (haystack) => { + return haystack.includes(replacementCharacterBuffer); + }, +}; diff --git a/lib/rfc2047.js b/lib/rfc2047.js index d6a153e..d20e198 100644 --- a/lib/rfc2047.js +++ b/lib/rfc2047.js @@ -2,6 +2,7 @@ const isUtf8RegExp = /^utf-?8$/i; const isLatin1RegExp = /^(?:iso-8859-1|latin1|us-ascii)$/i; +const bufferOps = require('./node-buffer-ops'); const rfc2047 = (module.exports = {}); let iconv; @@ -22,13 +23,13 @@ function stringify(obj) { return obj; } else if (obj === null || typeof obj === 'undefined') { return ''; + } else if (obj instanceof Uint8Array) { + return bufferOps.fromUtf8(obj); } else { return String(obj); } } -const replacementCharacterBuffer = Buffer.from('�'); - function decodeBuffer(encodedText, encoding) { if (encoding === 'q') { encodedText = encodedText.replace(/_/g, ' '); @@ -42,7 +43,7 @@ function decodeBuffer(encodedText, encoding) { numValidlyEncodedBytes += 1; } } - const buffer = Buffer.alloc( + const buffer = bufferOps.allocByteBuffer( encodedText.length - numValidlyEncodedBytes * 2 ); let j = 0; @@ -62,7 +63,7 @@ function decodeBuffer(encodedText, encoding) { } return buffer; } else { - return Buffer.from(encodedText, 'base64'); + return bufferOps.fromBase64(encodedText); } } @@ -95,23 +96,23 @@ function decodeEncodedWord(encodedText, encoding, charset) { converter = new iconv.Iconv('iso-8859-1', 'utf-8//TRANSLIT'); } try { - return converter.convert(buffer).toString('utf-8'); + return bufferOps.fromUtf8(converter.convert(buffer)); } catch (e2) {} } else if (isUtf8RegExp.test(charset)) { - const decoded = buffer.toString('utf-8'); + const decoded = bufferOps.fromUtf8(buffer); if ( !/\ufffd/.test(decoded) || - buffer.includes(replacementCharacterBuffer) + bufferOps.includesReplacementCharacter(buffer) ) { return decoded; } } else if (isLatin1RegExp.test(charset)) { - return buffer.toString('ascii'); + return bufferOps.fromAscii(buffer); } else if (iconvLite && iconvLite.encodingExists(charset)) { decoded = iconvLite.decode(buffer, charset); if ( !/\ufffd/.test(decoded) || - buffer.includes(replacementCharacterBuffer) + bufferOps.includesReplacementCharacter(buffer) ) { return decoded; } @@ -264,7 +265,7 @@ rfc2047.encode = (text) => { const charset = 'utf-8'; // Around 25% faster than encodeURIComponent(token.replace(/ /g, "_")).replace(/%/g, "="): const encodedWordBody = bufferToQuotedPrintableString( - Buffer.from(token, 'utf-8') + bufferOps.toUtf8(token) ); if (previousTokenWasEncodedWord) { result += ' '; diff --git a/package.json b/package.json index 530aaa7..fddea37 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "iconv-lite": "0.4.5" }, "browser": { - "iconv": false + "iconv": false, + "lib/node-buffer-ops.js": "lib/browser-buffer-ops.js" }, "nyc": { "include": [ diff --git a/test/rfc2047.js b/test/rfc2047.js index b3d5442..54cecf2 100644 --- a/test/rfc2047.js +++ b/test/rfc2047.js @@ -1,5 +1,5 @@ const unexpected = require('unexpected'); -const proxyquire = require('proxyquire'); +const proxyquire = require('proxyquire').noPreserveCache(); const IconvType = { Unavailable: 0, @@ -18,386 +18,390 @@ function description(iconvType) { } } -describe('rfc2047', () => { - for (const iconvType of [ - IconvType.Unavailable, - IconvType.LiteOnly, - IconvType.Available, - ]) { - describe(`with ${description(iconvType)}`, () => { - const requireStubs = {}; - - switch (iconvType) { - case IconvType.Unavailable: - requireStubs['iconv'] = null; - requireStubs['iconv-lite'] = null; - break; - case IconvType.LiteOnly: - requireStubs['iconv'] = null; - break; - } - - const iconvAvailable = iconvType === IconvType.Available; - const iconvLiteAvailable = - iconvAvailable || iconvType === IconvType.LiteOnly; - - const rfc2047 = proxyquire('../lib/rfc2047', requireStubs); - - const expect = unexpected - .clone() - .addAssertion('to encode to', (expect, subject, value) => { - expect(rfc2047.encode(subject), 'to equal', value); - }) - .addAssertion('to decode to', (expect, subject, value) => { - expect(rfc2047.decode(subject), 'to equal', value); - }) - .addAssertion( - 'to encode back and forth to', - (expect, subject, value) => { - expect(subject, 'to encode to', value); - expect(value, 'to decode to', subject); - } - ); +for (const environment of ['node', 'browser']) { + describe(`${environment} rfc2047`, () => { + const iconvTypes = [IconvType.Unavailable, IconvType.LiteOnly]; + if (environment === 'node') { + iconvTypes.push(IconvType.Available); + } + for (const iconvType of iconvTypes) { + describe(`with ${description(iconvType)}`, () => { + const requireStubs = { + '../lib/node-buffer-ops': require(`../lib/${environment}-buffer-ops`), + }; + + switch (iconvType) { + case IconvType.Unavailable: + requireStubs.iconv = null; + requireStubs['iconv-lite'] = null; + break; + case IconvType.LiteOnly: + requireStubs.iconv = null; + break; + } - describe('#encode() and #decode()', () => { - it('should handle the empty string', () => { - expect('', 'to encode back and forth to', ''); - }); + const iconvAvailable = iconvType === IconvType.Available; + const iconvLiteAvailable = + iconvAvailable || iconvType === IconvType.LiteOnly; + + const rfc2047 = proxyquire('../lib/rfc2047', requireStubs); + + const expect = unexpected + .clone() + .addAssertion('to encode to', (expect, subject, value) => { + expect(rfc2047.encode(subject), 'to equal', value); + }) + .addAssertion('to decode to', (expect, subject, value) => { + expect(rfc2047.decode(subject), 'to equal', value); + }) + .addAssertion( + 'to encode back and forth to', + (expect, subject, value) => { + expect(subject, 'to encode to', value); + expect(value, 'to decode to', subject); + } + ); - it('should handle a string only containing a space', () => { - expect(' ', 'to encode back and forth to', ' '); - }); + describe('#encode() and #decode()', () => { + it('should handle the empty string', () => { + expect('', 'to encode back and forth to', ''); + }); - it('should not encode an equals sign', () => { - expect('=', 'to encode back and forth to', '='); - }); + it('should handle a string only containing a space', () => { + expect(' ', 'to encode back and forth to', ' '); + }); - it('should handle a string that does not need to be encoded', () => { - expect( - 'Andreas Lind ', - 'to encode back and forth to', - 'Andreas Lind ' - ); - }); + it('should not encode an equals sign', () => { + expect('=', 'to encode back and forth to', '='); + }); - it('should handle a multi-word string where the middle word has to be encoded', () => { - expect( - 'Andreas Lindø ', - 'to encode back and forth to', - 'Andreas =?utf-8?Q?Lind=C3=B8?= ' - ); - }); + it('should handle a string that does not need to be encoded', () => { + expect( + 'Andreas Lind ', + 'to encode back and forth to', + 'Andreas Lind ' + ); + }); - it('should use an UTF-8 encoded word when a character is not in iso-8859-1', () => { - expect( - 'Mr. Smiley face aka ☺ ', - 'to encode back and forth to', - 'Mr. Smiley face aka =?utf-8?Q?=E2=98=BA?= ' - ); - }); + it('should handle a multi-word string where the middle word has to be encoded', () => { + expect( + 'Andreas Lindø ', + 'to encode back and forth to', + 'Andreas =?utf-8?Q?Lind=C3=B8?= ' + ); + }); - it('should handle two neighbouring words that have to be encoded', () => { - expect( - '¡Hola, señor!', - 'to encode back and forth to', - '=?utf-8?Q?=C2=A1Hola=2C?= =?utf-8?Q?_se=C3=B1or!?=' - ); - expect( - 'På lördag', - 'to encode back and forth to', - '=?utf-8?Q?P=C3=A5?= =?utf-8?Q?_l=C3=B6rdag?=' - ); - }); + it('should use an UTF-8 encoded word when a character is not in iso-8859-1', () => { + expect( + 'Mr. Smiley face aka ☺ ', + 'to encode back and forth to', + 'Mr. Smiley face aka =?utf-8?Q?=E2=98=BA?= ' + ); + }); - it('should not rely on the space between neighbouring encoded words to be preserved', () => { - expect( - '☺ ☺', - 'to encode back and forth to', - '=?utf-8?Q?=E2=98=BA?= =?utf-8?Q?_=E2=98=BA?=' - ); - }); + it('should handle two neighbouring words that have to be encoded', () => { + expect( + '¡Hola, señor!', + 'to encode back and forth to', + '=?utf-8?Q?=C2=A1Hola=2C?= =?utf-8?Q?_se=C3=B1or!?=' + ); + expect( + 'På lördag', + 'to encode back and forth to', + '=?utf-8?Q?P=C3=A5?= =?utf-8?Q?_l=C3=B6rdag?=' + ); + }); - it('should handle some dreamed up edge cases', () => { - expect( - 'lördag', - 'to encode back and forth to', - '=?utf-8?Q?l=C3=B6rdag?=' - ); - }); + it('should not rely on the space between neighbouring encoded words to be preserved', () => { + expect( + '☺ ☺', + 'to encode back and forth to', + '=?utf-8?Q?=E2=98=BA?= =?utf-8?Q?_=E2=98=BA?=' + ); + }); - it('should handle a multi-word string where the middle word has to be left unencoded', () => { - expect( - 'Så er fødselen i gang', - 'to encode back and forth to', - '=?utf-8?Q?S=C3=A5?= er =?utf-8?Q?f=C3=B8dselen?= i gang' - ); - }); + it('should handle some dreamed up edge cases', () => { + expect( + 'lördag', + 'to encode back and forth to', + '=?utf-8?Q?l=C3=B6rdag?=' + ); + }); - it('should place leading quotes correctly', () => { - expect( - '"ÅÄÖ" ', - 'to encode back and forth to', - '"=?utf-8?Q?=C3=85=C3=84=C3=96?=" ' - ); - }); + it('should handle a multi-word string where the middle word has to be left unencoded', () => { + expect( + 'Så er fødselen i gang', + 'to encode back and forth to', + '=?utf-8?Q?S=C3=A5?= er =?utf-8?Q?f=C3=B8dselen?= i gang' + ); + }); - it('should place trailing quotes correctly', () => { - expect( - '"TEST ÅÄÖ" ', - 'to encode back and forth to', - '"TEST =?utf-8?Q?=C3=85=C3=84=C3=96?=" ' - ); - }); + it('should place leading quotes correctly', () => { + expect( + '"ÅÄÖ" ', + 'to encode back and forth to', + '"=?utf-8?Q?=C3=85=C3=84=C3=96?=" ' + ); + }); - // Regression test for #2: - it('should handle an emoji test case', () => { - expect( - '{"tags":"","fullName":"😬"}', - 'to encode back and forth to', - '=?utf-8?Q?{=22tags=22=3A?=""=?utf-8?Q?=2C=22fullNa?= =?utf-8?Q?me=22=3A=22=F0=9F=98=AC=22?=}' - ); - }); + it('should place trailing quotes correctly', () => { + expect( + '"TEST ÅÄÖ" ', + 'to encode back and forth to', + '"TEST =?utf-8?Q?=C3=85=C3=84=C3=96?=" ' + ); + }); - it('should handle the replacement character', () => { - expect( - 'test_�.docx', - 'to encode back and forth to', - '=?utf-8?Q?test=5F=EF=BF=BD=2Ed?=ocx' - ); - }); - }); + // Regression test for #2: + it('should handle an emoji test case', () => { + expect( + '{"tags":"","fullName":"😬"}', + 'to encode back and forth to', + '=?utf-8?Q?{=22tags=22=3A?=""=?utf-8?Q?=2C=22fullNa?= =?utf-8?Q?me=22=3A=22=F0=9F=98=AC=22?=}' + ); + }); - describe('#encode()', () => { - it('should handle non-string values correctly', () => { - expect(-1, 'to encode to', '-1'); - expect(Infinity, 'to encode to', 'Infinity'); - expect(false, 'to encode to', 'false'); - expect(true, 'to encode to', 'true'); - expect(/bla/, 'to encode to', '/bla/'); - expect(undefined, 'to encode to', ''); - expect(null, 'to encode to', ''); + it('should handle the replacement character', () => { + expect( + 'test_�.docx', + 'to encode back and forth to', + '=?utf-8?Q?test=5F=EF=BF=BD=2Ed?=ocx' + ); + }); }); - it('should handle a tab character at the beginning of a word', () => { - expect('\tfoo', 'to encode to', ' foo'); - }); + describe('#encode()', () => { + it('should handle non-string values correctly', () => { + expect(-1, 'to encode to', '-1'); + expect(Infinity, 'to encode to', 'Infinity'); + expect(false, 'to encode to', 'false'); + expect(true, 'to encode to', 'true'); + expect(/bla/, 'to encode to', '/bla/'); + expect(undefined, 'to encode to', ''); + expect(null, 'to encode to', ''); + }); - it('should handle control chars', () => { - expect( - '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f', - 'to encode to', - '=?utf-8?Q?=00=01=02=03=04=05=06=07?= =?utf-8?Q?=08?= =?utf-8?Q?_=0E=0F=10=11=12=13=14=15?= =?utf-8?Q?=16=17=18=19=1A=1B=1C=1D?= =?utf-8?Q?=1E=1F?=' - ); - }); + it('should handle a tab character at the beginning of a word', () => { + expect('\tfoo', 'to encode to', ' foo'); + }); - it('should handle a tab character at the end of a word', () => { - expect('foo\t', 'to encode to', 'foo '); - }); + it('should handle control chars', () => { + expect( + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f', + 'to encode to', + '=?utf-8?Q?=00=01=02=03=04=05=06=07?= =?utf-8?Q?=08?= =?utf-8?Q?_=0E=0F=10=11=12=13=14=15?= =?utf-8?Q?=16=17=18=19=1A=1B=1C=1D?= =?utf-8?Q?=1E=1F?=' + ); + }); - it('should handle a tab character with spaces around it', () => { - expect('bar \t foo', 'to encode to', 'bar foo'); - }); + it('should handle a tab character at the end of a word', () => { + expect('foo\t', 'to encode to', 'foo '); + }); - it('should not split a backslash from the doublequote it is escaping', () => { - expect('"Öland\\""', 'to encode to', '"=?utf-8?Q?=C3=96land?=\\""'); - }); - }); + it('should handle a tab character with spaces around it', () => { + expect('bar \t foo', 'to encode to', 'bar foo'); + }); - describe('#decode()', () => { - it('should handle non-string values correctly', () => { - expect(-1, 'to decode to', '-1'); - expect(Infinity, 'to decode to', 'Infinity'); - expect(false, 'to decode to', 'false'); - expect(true, 'to decode to', 'true'); - expect(/bla/, 'to decode to', '/bla/'); - expect(undefined, 'to decode to', ''); - expect(null, 'to decode to', ''); + it('should not split a backslash from the doublequote it is escaping', () => { + expect('"Öland\\""', 'to encode to', '"=?utf-8?Q?=C3=96land?=\\""'); + }); }); - it('should decode encoded word with invalid quoted-printable, decodeURIComponent case', () => { - expect('=?UTF-8?Q?=xxfoo?=', 'to decode to', '=xxfoo'); - }); + describe('#decode()', () => { + it('should handle non-string values correctly', () => { + expect(-1, 'to decode to', '-1'); + expect(Infinity, 'to decode to', 'Infinity'); + expect(false, 'to decode to', 'false'); + expect(true, 'to decode to', 'true'); + expect(/bla/, 'to decode to', '/bla/'); + expect(undefined, 'to decode to', ''); + expect(null, 'to decode to', ''); + }); - it('should decode encoded word with invalid quoted-printable, unescape case', () => { - expect('=?iso-8859-1?Q?=xxfoo?=', 'to decode to', '=xxfoo'); - }); + it('should decode encoded word with invalid quoted-printable, decodeURIComponent case', () => { + expect('=?UTF-8?Q?=xxfoo?=', 'to decode to', '=xxfoo'); + }); - it('should decode encoded word with invalid base64', () => { - expect('=?iso-8859-1?B?\u0000``?=', 'to decode to', ''); - }); + it('should decode encoded word with invalid quoted-printable, unescape case', () => { + expect('=?iso-8859-1?Q?=xxfoo?=', 'to decode to', '=xxfoo'); + }); - it('should decode separated encoded words', () => { - expect( - '=?utf-8?Q?One.com=E2=80?= =?utf-8?Q?=99s_=E2=80=9CDon=E2=80=99t_screw_it_up=E2=80=9D_?= =?utf-8?Q?code?=', - 'to decode to', - 'One.com’s “Don’t screw it up” code' - ); - }); + it('should decode encoded word with invalid base64', () => { + expect('=?iso-8859-1?B?\u0000``?=', 'to decode to', ''); + }); - it('should handle the test cases listed in RFC 2047', () => { - expect( - '=?ISO-8859-1?Q?Olle_J=E4rnefors?= ', - 'to decode to', - 'Olle Järnefors ' - ); - expect( - '=?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= ', - 'to decode to', - 'Patrik Fältström ' - ); - if (iconvLiteAvailable) { + it('should decode separated encoded words', () => { expect( - 'Nathaniel Borenstein (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)', + '=?utf-8?Q?One.com=E2=80?= =?utf-8?Q?=99s_=E2=80=9CDon=E2=80=99t_screw_it_up=E2=80=9D_?= =?utf-8?Q?code?=', 'to decode to', - 'Nathaniel Borenstein (םולש ןב ילטפנ)' + 'One.com’s “Don’t screw it up” code' ); - } - expect('(=?ISO-8859-1?Q?a?=)', 'to decode to', '(a)'); - expect('(=?ISO-8859-1?Q?a?= b)', 'to decode to', '(a b)'); - expect( - '(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)', - 'to decode to', - '(ab)' - ); - expect( - '(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)', - 'to decode to', - '(ab)' - ); - expect('(=?ISO-8859-1?Q?a_b?=)', 'to decode to', '(a b)'); - if (iconvLiteAvailable) { + }); + + it('should handle the test cases listed in RFC 2047', () => { expect( - '(=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)', + '=?ISO-8859-1?Q?Olle_J=E4rnefors?= ', 'to decode to', - '(a b)' + 'Olle Järnefors ' ); - } - }); - - it('should handle subject found in mail with X-Mailer: MailChimp Mailer', () => { - expect( - '=?utf-8?Q?Spar=2020=20%=20p=C3=A5=20de=20bedste=20businessb=C3=B8ger=20fra=20Gyldendal=21?=', - 'to decode to', - 'Spar 20 % på de bedste businessbøger fra Gyldendal!' - ); - expect( - '=?iso-8859-1?Q?Spar 20 %...?=', - 'to decode to', - 'Spar 20 %...' - ); - }); + expect( + '=?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= ', + 'to decode to', + 'Patrik Fältström ' + ); + if (iconvLiteAvailable) { + expect( + 'Nathaniel Borenstein (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)', + 'to decode to', + 'Nathaniel Borenstein (םולש ןב ילטפנ)' + ); + } + expect('(=?ISO-8859-1?Q?a?=)', 'to decode to', '(a)'); + expect('(=?ISO-8859-1?Q?a?= b)', 'to decode to', '(a b)'); + expect( + '(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)', + 'to decode to', + '(ab)' + ); + expect( + '(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)', + 'to decode to', + '(ab)' + ); + expect('(=?ISO-8859-1?Q?a_b?=)', 'to decode to', '(a b)'); + if (iconvLiteAvailable) { + expect( + '(=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)', + 'to decode to', + '(a b)' + ); + } + }); - it('should handle multiple base64 encoded words issued by Thunderbird', () => { - expect( - '=?UTF-8?B?Rm9vw6YsIEZvbyDDpiwgw6bDuMOmw7jDpsO4w6bDuMOmw7jDpsO4LCA=?==?UTF-8?B?4pi6IE1y4pi6IOKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYug==?= =?UTF-8?B?4pi64pi64pi64pi64pi64pi64pi6?=', - 'to decode to', - 'Fooæ, Foo æ, æøæøæøæøæøæø, ☺ Mr☺ ☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺' - ); - }); + it('should handle subject found in mail with X-Mailer: MailChimp Mailer', () => { + expect( + '=?utf-8?Q?Spar=2020=20%=20p=C3=A5=20de=20bedste=20businessb=C3=B8ger=20fra=20Gyldendal=21?=', + 'to decode to', + 'Spar 20 % på de bedste businessbøger fra Gyldendal!' + ); + expect( + '=?iso-8859-1?Q?Spar 20 %...?=', + 'to decode to', + 'Spar 20 %...' + ); + }); - it('should handle two back-to-back UTF-8 encoded words from the subject in a raygun mail', () => { - expect( - '=?utf-8?B?d2VibWFpbCBwcm9kdWN0aW9uIC0gbmV3IGVycm9yIC0gR2XD?==?utf-8?B?p2Vyc2l6IGRlxJ9pxZ9rZW4u?=', - 'to decode to', - 'webmail production - new error - Geçersiz değişken.' - ); - }); + it('should handle multiple base64 encoded words issued by Thunderbird', () => { + expect( + '=?UTF-8?B?Rm9vw6YsIEZvbyDDpiwgw6bDuMOmw7jDpsO4w6bDuMOmw7jDpsO4LCA=?==?UTF-8?B?4pi6IE1y4pi6IOKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYuuKYug==?= =?UTF-8?B?4pi64pi64pi64pi64pi64pi64pi6?=', + 'to decode to', + 'Fooæ, Foo æ, æøæøæøæøæøæø, ☺ Mr☺ ☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺' + ); + }); - it('should keep encoded words with partial sequences separate if there is text between them', () => { - expect( - '=?utf-8?B?d2VibWFpbCBwcm9kdWN0aW9uIC0gbmV3IGVycm9yIC0gR2XD?=foo=?utf-8?B?p2Vyc2l6IGRlxJ9pxZ9rZW4u?=', - 'to decode to', - '=?utf-8?B?d2VibWFpbCBwcm9kdWN0aW9uIC0gbmV3IGVycm9yIC0gR2XD?=foo=?utf-8?B?p2Vyc2l6IGRlxJ9pxZ9rZW4u?=' - ); - }); + it('should handle two back-to-back UTF-8 encoded words from the subject in a raygun mail', () => { + expect( + '=?utf-8?B?d2VibWFpbCBwcm9kdWN0aW9uIC0gbmV3IGVycm9yIC0gR2XD?==?utf-8?B?p2Vyc2l6IGRlxJ9pxZ9rZW4u?=', + 'to decode to', + 'webmail production - new error - Geçersiz değişken.' + ); + }); - it('should decode a UTF-8 smiley (illegally) split up into 2 encoded words', () => { - expect('=?utf-8?Q?=E2=98?= =?utf-8?Q?=BA?=', 'to decode to', '☺'); - }); + it('should keep encoded words with partial sequences separate if there is text between them', () => { + expect( + '=?utf-8?B?d2VibWFpbCBwcm9kdWN0aW9uIC0gbmV3IGVycm9yIC0gR2XD?=foo=?utf-8?B?p2Vyc2l6IGRlxJ9pxZ9rZW4u?=', + 'to decode to', + '=?utf-8?B?d2VibWFpbCBwcm9kdWN0aW9uIC0gbmV3IGVycm9yIC0gR2XD?=foo=?utf-8?B?p2Vyc2l6IGRlxJ9pxZ9rZW4u?=' + ); + }); - it('should decode a UTF-8 smiley (illegally) split up into 3 encoded words', () => { - expect( - '=?utf-8?Q?=E2?= =?utf-8?Q?=98?= =?utf-8?Q?=BA?=', - 'to decode to', - '☺' - ); - }); + it('should decode a UTF-8 smiley (illegally) split up into 2 encoded words', () => { + expect('=?utf-8?Q?=E2=98?= =?utf-8?Q?=BA?=', 'to decode to', '☺'); + }); - it('should give up decoding a UTF-8 smiley (illegally) split up into 3 encoded words if there is regular text between the encoded words', () => { - expect( - '=?utf-8?Q?=E2?= =?utf-8?Q?=98?=a=?utf-8?Q?=BA?==?utf-8?Q?=BA?=a', - 'to decode to', - '=?utf-8?Q?=E2?==?utf-8?Q?=98?=a=?utf-8?Q?=BA?==?utf-8?Q?=BA?=a' - ); - }); + it('should decode a UTF-8 smiley (illegally) split up into 3 encoded words', () => { + expect( + '=?utf-8?Q?=E2?= =?utf-8?Q?=98?= =?utf-8?Q?=BA?=', + 'to decode to', + '☺' + ); + }); - it('should decode an encoded word following a undecodable sequence of encoded words', () => { - expect( - '=?utf-8?Q?=E2?= =?utf-8?Q?=98?= =?iso-8859-1?Q?=A1?=Hola, se=?iso-8859-1?Q?=F1?=or!', - 'to decode to', - '=?utf-8?Q?=E2?==?utf-8?Q?=98?=¡Hola, señor!' - ); - }); + it('should give up decoding a UTF-8 smiley (illegally) split up into 3 encoded words if there is regular text between the encoded words', () => { + expect( + '=?utf-8?Q?=E2?= =?utf-8?Q?=98?=a=?utf-8?Q?=BA?==?utf-8?Q?=BA?=a', + 'to decode to', + '=?utf-8?Q?=E2?==?utf-8?Q?=98?=a=?utf-8?Q?=BA?==?utf-8?Q?=BA?=a' + ); + }); - it('should handle test cases from the MIME tools package', () => { - // From http://search.cpan.org/~dskoll/MIME-tools-5.502/lib/MIME/Words.pm: - expect( - '=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= ', - 'to decode to', - 'Keld Jørn Simonsen ' - ); - expect( - '=?US-ASCII?Q?Keith_Moore?= ', - 'to decode to', - 'Keith Moore ' - ); - expect( - '=?ISO-8859-1?Q?Andr=E9_?= Pirard ', - 'to decode to', - 'André Pirard ' - ); - expect( - '=?iso-8859-1?Q?J=F8rgen_Nellemose?=', - 'to decode to', - 'Jørgen Nellemose' - ); - expect( - '=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?==?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?==?US-ASCII?Q?.._cool!?=', - 'to decode to', - iconvLiteAvailable - ? 'If you can read this you understand the example... cool!' - : 'If you can read this yo=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=.. cool!' - ); - }); + it('should decode an encoded word following a undecodable sequence of encoded words', () => { + expect( + '=?utf-8?Q?=E2?= =?utf-8?Q?=98?= =?iso-8859-1?Q?=A1?=Hola, se=?iso-8859-1?Q?=F1?=or!', + 'to decode to', + '=?utf-8?Q?=E2?==?utf-8?Q?=98?=¡Hola, señor!' + ); + }); - if (iconvLiteAvailable) { - it('should handle a file name found in a Korean mail', () => { + it('should handle test cases from the MIME tools package', () => { + // From http://search.cpan.org/~dskoll/MIME-tools-5.502/lib/MIME/Words.pm: expect( - '=?ks_c_5601-1987?B?MTMwMTE3X8HWwvfA5V+1tcDlX7jetLq+8y5wZGY=?=', + '=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= ', 'to decode to', - '130117_주차장_도장_메뉴얼.pdf' + 'Keld Jørn Simonsen ' + ); + expect( + '=?US-ASCII?Q?Keith_Moore?= ', + 'to decode to', + 'Keith Moore ' + ); + expect( + '=?ISO-8859-1?Q?Andr=E9_?= Pirard ', + 'to decode to', + 'André Pirard ' + ); + expect( + '=?iso-8859-1?Q?J=F8rgen_Nellemose?=', + 'to decode to', + 'Jørgen Nellemose' + ); + expect( + '=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?==?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?==?US-ASCII?Q?.._cool!?=', + 'to decode to', + iconvLiteAvailable + ? 'If you can read this you understand the example... cool!' + : 'If you can read this yo=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=.. cool!' ); }); - } - it('should handle bogus encoded words (spotted in the wild)', () => { - expect( - '=?utf-8?Q??= ', - 'to decode to', - ' ' - ); - }); + if (iconvLiteAvailable) { + it('should handle a file name found in a Korean mail', () => { + expect( + '=?ks_c_5601-1987?B?MTMwMTE3X8HWwvfA5V+1tcDlX7jetLq+8y5wZGY=?=', + 'to decode to', + '130117_주차장_도장_메뉴얼.pdf' + ); + }); + } - if (iconvAvailable) { - it('should decode a character set not in iconv-lite', () => { + it('should handle bogus encoded words (spotted in the wild)', () => { expect( - '=?iso-2022-jp?B?GyRCRnxLXDhsJE4lNSVWJTglJyUvJUghXRsoQnRlc3Q=?=', + '=?utf-8?Q??= ', 'to decode to', - '日本語のサブジェクト−test' + ' ' ); }); - } + + if (iconvAvailable) { + it('should decode a character set not in iconv-lite', () => { + expect( + '=?iso-2022-jp?B?GyRCRnxLXDhsJE4lNSVWJTglJyUvJUghXRsoQnRlc3Q=?=', + 'to decode to', + '日本語のサブジェクト−test' + ); + }); + } + }); }); - }); - } -}); + } + }); +}