From 5f0c882aaaa98d05f85230cb2ee867f18e409392 Mon Sep 17 00:00:00 2001
From: Vitali Lovich <vlovich@cloudflare.com>
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 <vlovich@cloudflare.com>
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 <paf@nada.kth.se>'
           );
-          expect(
-            'Nathaniel Borenstein <nsb@thumper.bellcore.com> (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)',
-            'to decode to',
-            'Nathaniel Borenstein <nsb@thumper.bellcore.com> (םולש ןב ילטפנ)'
-          );
+          if (iconvLiteAvailable) {
+            expect(
+              'Nathaniel Borenstein <nsb@thumper.bellcore.com> (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)',
+              'to decode to',
+              'Nathaniel Borenstein <nsb@thumper.bellcore.com> (םולש ןב ילטפנ)'
+            );
+          }
           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 <vlovich@cloudflare.com>
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 <andreas@one.com>',
-            'to encode back and forth to',
-            'Andreas Lind <andreas@one.com>'
-          );
-        });
+          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ø <andreas@one.com>',
-            'to encode back and forth to',
-            'Andreas =?utf-8?Q?Lind=C3=B8?= <andreas@one.com>'
-          );
-        });
+          it('should handle a string that does not need to be encoded', () => {
+            expect(
+              'Andreas Lind <andreas@one.com>',
+              'to encode back and forth to',
+              'Andreas Lind <andreas@one.com>'
+            );
+          });
 
-        it('should use an UTF-8 encoded word when a character is not in iso-8859-1', () => {
-          expect(
-            'Mr. Smiley face aka ☺ <smiley@face.dk>',
-            'to encode back and forth to',
-            'Mr. Smiley face aka =?utf-8?Q?=E2=98=BA?= <smiley@face.dk>'
-          );
-        });
+          it('should handle a multi-word string where the middle word has to be encoded', () => {
+            expect(
+              'Andreas Lindø <andreas@one.com>',
+              'to encode back and forth to',
+              'Andreas =?utf-8?Q?Lind=C3=B8?= <andreas@one.com>'
+            );
+          });
 
-        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 ☺ <smiley@face.dk>',
+              'to encode back and forth to',
+              'Mr. Smiley face aka =?utf-8?Q?=E2=98=BA?= <smiley@face.dk>'
+            );
+          });
 
-        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(
-            '"ÅÄÖ" <sss@example.com>',
-            'to encode back and forth to',
-            '"=?utf-8?Q?=C3=85=C3=84=C3=96?=" <sss@example.com>'
-          );
-        });
+          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 ÅÄÖ" <sss@example.com>',
-            'to encode back and forth to',
-            '"TEST =?utf-8?Q?=C3=85=C3=84=C3=96?=" <sss@example.com>'
-          );
-        });
+          it('should place leading quotes correctly', () => {
+            expect(
+              '"ÅÄÖ" <sss@example.com>',
+              'to encode back and forth to',
+              '"=?utf-8?Q?=C3=85=C3=84=C3=96?=" <sss@example.com>'
+            );
+          });
 
-        // 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 ÅÄÖ" <sss@example.com>',
+              'to encode back and forth to',
+              '"TEST =?utf-8?Q?=C3=85=C3=84=C3=96?=" <sss@example.com>'
+            );
+          });
 
-        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?= <ojarnef@admin.kth.se>',
-            'to decode to',
-            'Olle Järnefors <ojarnef@admin.kth.se>'
-          );
-          expect(
-            '=?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <paf@nada.kth.se>',
-            'to decode to',
-            'Patrik Fältström <paf@nada.kth.se>'
-          );
-          if (iconvLiteAvailable) {
+          it('should decode separated encoded words', () => {
             expect(
-              'Nathaniel Borenstein <nsb@thumper.bellcore.com> (=?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 <nsb@thumper.bellcore.com> (םולש ןב ילטפנ)'
+              '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?= <ojarnef@admin.kth.se>',
               'to decode to',
-              '(a b)'
+              'Olle Järnefors <ojarnef@admin.kth.se>'
             );
-          }
-        });
-
-        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?= <paf@nada.kth.se>',
+              'to decode to',
+              'Patrik Fältström <paf@nada.kth.se>'
+            );
+            if (iconvLiteAvailable) {
+              expect(
+                'Nathaniel Borenstein <nsb@thumper.bellcore.com> (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)',
+                'to decode to',
+                'Nathaniel Borenstein <nsb@thumper.bellcore.com> (םולש ןב ילטפנ)'
+              );
+            }
+            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?= <keld@dkuug.dk>',
-            'to decode to',
-            'Keld Jørn Simonsen <keld@dkuug.dk>'
-          );
-          expect(
-            '=?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>',
-            'to decode to',
-            'Keith Moore <moore@cs.utk.edu>'
-          );
-          expect(
-            '=?ISO-8859-1?Q?Andr=E9_?= Pirard <PIRARD@vm1.ulg.ac.be>',
-            'to decode to',
-            'André  Pirard <PIRARD@vm1.ulg.ac.be>'
-          );
-          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?= <keld@dkuug.dk>',
               'to decode to',
-              '130117_주차장_도장_메뉴얼.pdf'
+              'Keld Jørn Simonsen <keld@dkuug.dk>'
+            );
+            expect(
+              '=?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>',
+              'to decode to',
+              'Keith Moore <moore@cs.utk.edu>'
+            );
+            expect(
+              '=?ISO-8859-1?Q?Andr=E9_?= Pirard <PIRARD@vm1.ulg.ac.be>',
+              'to decode to',
+              'André  Pirard <PIRARD@vm1.ulg.ac.be>'
+            );
+            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??= <andreas@one.com>',
-            'to decode to',
-            ' <andreas@one.com>'
-          );
-        });
+          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??= <andreas@one.com>',
               'to decode to',
-              '日本語のサブジェクト−test'
+              ' <andreas@one.com>'
             );
           });
-        }
+
+          if (iconvAvailable) {
+            it('should decode a character set not in iconv-lite', () => {
+              expect(
+                '=?iso-2022-jp?B?GyRCRnxLXDhsJE4lNSVWJTglJyUvJUghXRsoQnRlc3Q=?=',
+                'to decode to',
+                '日本語のサブジェクト−test'
+              );
+            });
+          }
+        });
       });
-    });
-  }
-});
+    }
+  });
+}