Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
andris9 committed Jan 6, 2021
1 parent 4830d0a commit 57c80e9
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 129 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
},
globals: {
TextDecoder: 'readable',
TextEncoder: 'readable',
Blob: 'readable',
FileReader: 'readable',
console: 'readable'
Expand Down
24 changes: 17 additions & 7 deletions dist/postal-mime.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/address-parser.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use strict';
import { decodeWords } from './decode-strings';

/**
* Converts tokens for a single address into an address object
Expand Down Expand Up @@ -60,7 +60,7 @@ function _handleAddress(tokens) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' ');
addresses.push({
name: data.text || (address && address.name),
name: decodeWords(data.text || (address && address.name)),
group: data.group.length ? addressParser(data.group.join(',')) : []
});
} else {
Expand Down Expand Up @@ -114,7 +114,7 @@ function _handleAddress(tokens) {
} else {
address = {
address: data.address || data.text || '',
name: data.text || data.address || ''
name: decodeWords(data.text || data.address || '')
};

if (address.address === address.name) {
Expand Down
60 changes: 5 additions & 55 deletions src/base64-decoder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { decodeBase64, blobToArrayBuffer } from './decode-strings';

export default class Base64Decoder {
constructor(opts) {
opts = opts || {};
Expand All @@ -9,44 +11,6 @@ export default class Base64Decoder {
this.chunks = [];

this.remainder = '';

const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

// Use a lookup table to find the index.
this.lookup = new Uint8Array(256);
for (var i = 0; i < chars.length; i++) {
this.lookup[chars.charCodeAt(i)] = i;
}
}

decodeBase64(base64) {
const bufferLength = base64.length * 0.75;
const len = base64.length;

let p = 0;

if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}

const arrayBuffer = new ArrayBuffer(bufferLength);
const bytes = new Uint8Array(arrayBuffer);

for (let i = 0; i < len; i += 4) {
let encoded1 = this.lookup[base64.charCodeAt(i)];
let encoded2 = this.lookup[base64.charCodeAt(i + 1)];
let encoded3 = this.lookup[base64.charCodeAt(i + 2)];
let encoded4 = this.lookup[base64.charCodeAt(i + 3)];

bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}

return arrayBuffer;
}

update(buffer) {
Expand All @@ -71,30 +35,16 @@ export default class Base64Decoder {
}

if (base64Str.length) {
this.chunks.push(this.decodeBase64(base64Str));
this.chunks.push(decodeBase64(base64Str));
}
}
}

finalize() {
if (this.remainder && !/^=+$/.test(this.remainder)) {
this.chunks.push(this.decodeBase64(this.remainder));
this.chunks.push(decodeBase64(this.remainder));
}

// convert an array of arraybuffers into a blob and then back into a single arraybuffer
let blob = new Blob(this.chunks, { type: 'application/octet-stream' });
const fr = new FileReader();

return new Promise((resolve, reject) => {
fr.onload = function (e) {
resolve(e.target.result);
};

fr.onerror = function (e) {
reject(fr.error);
};

fr.readAsArrayBuffer(blob);
});
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
}
}
181 changes: 181 additions & 0 deletions src/decode-strings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
const textEncoder = new TextEncoder();
const decoders = new Map();

const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

// Use a lookup table to find the index.
const base64Lookup = new Uint8Array(256);
for (var i = 0; i < base64Chars.length; i++) {
base64Lookup[base64Chars.charCodeAt(i)] = i;
}

export function decodeBase64(base64) {
let bufferLength = Math.ceil(base64.length / 4) * 3;
const len = base64.length;

let p = 0;

if (base64.length % 4 === 3) {
bufferLength--;
} else if (base64.length % 4 === 2) {
bufferLength -= 2;
} else if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}

const arrayBuffer = new ArrayBuffer(bufferLength);
const bytes = new Uint8Array(arrayBuffer);

for (let i = 0; i < len; i += 4) {
let encoded1 = base64Lookup[base64.charCodeAt(i)];
let encoded2 = base64Lookup[base64.charCodeAt(i + 1)];
let encoded3 = base64Lookup[base64.charCodeAt(i + 2)];
let encoded4 = base64Lookup[base64.charCodeAt(i + 3)];

bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}

return arrayBuffer;
}

export function getDecoder(charset) {
charset = charset || 'utf8';
if (decoders.has(charset)) {
return decoders.get(charset);
}
let decoder;
try {
decoder = new TextDecoder(charset);
} catch (err) {
if (charset === 'utf8') {
// is this even possible?
throw err;
}
// use default
return getDecoder();
}

decoders.set(charset, decoder);
return decoder;
}

/**
* Converts a Blob into an ArrayBuffer
* @param {Blob} blob Blob to convert
* @returns {ArrayBuffer} Converted value
*/
export async function blobToArrayBuffer(blob) {
const fr = new FileReader();

return new Promise((resolve, reject) => {
fr.onload = function (e) {
resolve(e.target.result);
};

fr.onerror = function (e) {
reject(fr.error);
};

fr.readAsArrayBuffer(blob);
});
}

export function getHex(c) {
if ((c >= 0x30 /* 0 */ && c <= 0x39) /* 9 */ || (c >= 0x61 /* a */ && c <= 0x66) /* f */ || (c >= 0x41 /* A */ && c <= 0x46) /* F */) {
return String.fromCharCode(c);
}
return false;
}

/**
* Decode a complete mime word encoded string
*
* @param {String} str Mime word encoded string
* @return {String} Decoded unicode string
*/
export function decodeWord(charset, encoding, str) {
// RFC2231 added language tag to the encoding
// see: https://tools.ietf.org/html/rfc2231#section-5
// this implementation silently ignores this tag
let splitPos = charset.indexOf('*');
if (splitPos >= 0) {
charset = charset.substr(0, splitPos);
}

encoding = encoding.toUpperCase();

let byteStr;

if (encoding === 'Q') {
str = str
// remove spaces between = and hex char, this might indicate invalidly applied line splitting
.replace(/=\s+([0-9a-fA-F])/g, '=$1')
// convert all underscores to spaces
.replace(/[_\s]/g, ' ');

let buf = textEncoder.encode(str);
let encodedBytes = [];
for (let i = 0, len = buf.length; i < len; i++) {
let c = buf[i];
if (i <= len - 2 && c === 0x3d /* = */) {
let c1 = getHex(buf[i + 1]);
let c2 = getHex(buf[i + 2]);
if (c1 && c2) {
let c = parseInt(c1 + c2, 16);
encodedBytes.push(c);
i += 2;
continue;
}
}
encodedBytes.push(c);
}
byteStr = new ArrayBuffer(encodedBytes.length);
let dataView = new DataView(byteStr);
for (let i = 0, len = encodedBytes.length; i < len; i++) {
dataView.setUint8(i, encodedBytes[i]);
}
} else if (encoding === 'B') {
byteStr = decodeBase64(str.replace(/[^a-zA-Z0-9\+\/=]+/g, ''));
} else {
// keep as is, convert ArrayBuffer to unicode string, assume utf8
byteStr = textEncoder.encode(str);
}

return getDecoder(charset).decode(byteStr);
}

export function decodeWords(str) {
return (
(str || '')
.toString()
// find base64 words that can be joined
.replace(/(=\?([^?]+)\?[Bb]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Bb]\?[^?]*\?=)/g, (match, left, chLeft, chRight) => {
// only mark b64 chunks to be joined if charsets match
if (chLeft === chRight) {
// set a joiner marker
return left + '__\x00JOIN\x00__';
}
return match;
})
// find QP words that can be joined
.replace(/(=\?([^?]+)\?[Qq]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Qq]\?[^?]*\?=)/g, (match, left, chLeft, chRight) => {
// only mark QP chunks to be joined if charsets match
if (chLeft === chRight) {
// set a joiner marker
return left + '__\x00JOIN\x00__';
}
return match;
})
// join base64 encoded words
.replace(/(\?=)?__\x00JOIN\x00__(=\?([^?]+)\?[QqBb]\?)?/g, '')
// remove spaces between mime encoded words
.replace(/(=\?[^?]+\?[QqBb]\?[^?]*\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, '$1')
// decode words
.replace(/=\?([\w_\-*]+)\?([QqBb])\?([^?]*)\?=/g, (m, charset, encoding, text) => decodeWord(charset, encoding, text))
);
}
28 changes: 4 additions & 24 deletions src/mime-node.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getDecoder } from './decode-strings';
import PassThroughDecoder from './pass-through-decoder';
import Base64Decoder from './base64-decoder';
import QPDecoder from './qp-decoder';
Expand Down Expand Up @@ -36,32 +37,11 @@ export default class MimeNode {
this.contentDecoder = false;
}

getDecoder(charset) {
charset = charset || 'utf8';
if (this.decoders.has(charset)) {
return this.decoders.get(charset);
}
let decoder;
try {
decoder = new TextDecoder(charset);
} catch (err) {
if (charset === 'utf8') {
// is this even possible?
throw err;
}
// use default
return this.getDecoder();
}

this.decoders.set(charset, decoder);
return decoder;
}

setupContentDecoder(transferEncoding) {
if (/base64/i.test(transferEncoding)) {
this.contentDecoder = new Base64Decoder();
} else if (/quoted-printable/i.test(transferEncoding)) {
this.contentDecoder = new QPDecoder({ decoder: this.getDecoder(this.contentType.parsed.params.charset) });
this.contentDecoder = new QPDecoder({ decoder: getDecoder(this.contentType.parsed.params.charset) });
} else {
this.contentDecoder = new PassThroughDecoder();
}
Expand Down Expand Up @@ -200,7 +180,7 @@ export default class MimeNode {
return '';
}

let str = this.getDecoder(this.contentType.parsed.params.charset).decode(this.content);
let str = getDecoder(this.contentType.parsed.params.charset).decode(this.content);

if (/^flowed$/i.test(this.contentType.parsed.params.format)) {
str = this.decodeFlowedText(str, /^yes$/i.test(this.contentType.parsed.params.delsp));
Expand Down Expand Up @@ -267,7 +247,7 @@ export default class MimeNode {
this.state = 'body';
return this.processHeaders();
}
this.headerLines.push(this.getDecoder().decode(line));
this.headerLines.push(getDecoder().decode(line));
break;
case 'body': {
// add line to body
Expand Down
17 changes: 3 additions & 14 deletions src/pass-through-decoder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { blobToArrayBuffer } from './decode-strings';

export default class PassThroughDecoder {
constructor() {
this.chunks = [];
Expand All @@ -10,19 +12,6 @@ export default class PassThroughDecoder {

finalize() {
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
let blob = new Blob(this.chunks, { type: 'application/octet-stream' });
const fr = new FileReader();

return new Promise((resolve, reject) => {
fr.onload = function (e) {
resolve(e.target.result);
};

fr.onerror = function (e) {
reject(fr.error);
};

fr.readAsArrayBuffer(blob);
});
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
}
}
Loading

0 comments on commit 57c80e9

Please sign in to comment.