Skip to content

Instantly share code, notes, and snippets.

@vvelizariy
Created November 2, 2017 11:52
Show Gist options
  • Save vvelizariy/9c8c1c59afc03d23f433a23de2631a99 to your computer and use it in GitHub Desktop.
Save vvelizariy/9c8c1c59afc03d23f433a23de2631a99 to your computer and use it in GitHub Desktop.
var mimecodec = (function (TextEncoder, TextDecoder, btoa) {
'use strict';
btoa = btoa || base64Encode;
var mimecodec = {
/**
* Encodes all non printable and non ascii bytes to =XX form, where XX is the
* byte value in hex. This function does not convert linebreaks etc. it
* only escapes character sequences
*
* @param {String|Uint8Array} data Either a string or an Uint8Array
* @param {String} [fromCharset='UTF-8'] Source encoding
* @return {String} Mime encoded string
*/
mimeEncode: function(data, fromCharset) {
fromCharset = fromCharset || 'UTF-8';
var buffer = mimecodec.charset.convert(data || '', fromCharset),
ranges = [
// https://tools.ietf.org/html/rfc2045#section-6.7
[0x09], // <TAB>
[0x0A], // <LF>
[0x0D], // <CR>
[0x20, 0x3C], // <SP>!"#$%&'()*+,-./0123456789:;
[0x3E, 0x7E] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
],
result = '',
ord;
for (var i = 0, len = buffer.length; i < len; i++) {
ord = buffer[i];
// if the char is in allowed range, then keep as is, unless it is a ws in the end of a line
if (mimecodec._checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) {
result += String.fromCharCode(ord);
continue;
}
result += '=' + (ord < 0x10 ? '0' : '') + ord.toString(16).toUpperCase();
}
return result;
},
/**
* Decodes mime encoded string to an unicode string
*
* @param {String} str Mime encoded string
* @param {String} [fromCharset='UTF-8'] Source encoding
* @return {String} Decoded unicode string
*/
mimeDecode: function(str, fromCharset) {
str = (str || '').toString();
fromCharset = fromCharset || 'UTF-8';
var encodedBytesCount = (str.match(/\=[\da-fA-F]{2}/g) || []).length,
bufferLength = str.length - encodedBytesCount * 2,
chr, hex,
buffer = new Uint8Array(bufferLength),
bufferPos = 0;
for (var i = 0, len = str.length; i < len; i++) {
chr = str.charAt(i);
if (chr === '=' && (hex = str.substr(i + 1, 2)) && /[\da-fA-F]{2}/.test(hex)) {
buffer[bufferPos++] = parseInt(hex, 16);
i += 2;
continue;
}
buffer[bufferPos++] = chr.charCodeAt(0);
}
return mimecodec.charset.decode(buffer, fromCharset);
},
/**
* Encodes a string or an typed array of given charset into unicode
* base64 string. Also adds line breaks
*
* @param {String|Uint8Array} data String to be base64 encoded
* @param {String} [fromCharset='UTF-8']
* @return {String} Base64 encoded string
*/
base64Encode: function(data, fromCharset) {
var buf, b64;
if (fromCharset !== 'binary' && typeof data !== 'string') {
buf = mimecodec.charset.convert(data || '', fromCharset);
} else {
buf = data;
}
b64 = mimecodec.base64.encode(buf);
return mimecodec._addSoftLinebreaks(b64, 'base64');
},
/**
* Decodes a base64 string of any charset into an unicode string
*
* @param {String} str Base64 encoded string
* @param {String} [fromCharset='UTF-8'] Original charset of the base64 encoded string
* @return {String} Decoded unicode string
*/
base64Decode: function(str, fromCharset) {
var buf = mimecodec.base64.decode(str || '', 'buffer');
return mimecodec.charset.decode(buf, fromCharset);
},
/**
* Encodes a string or an Uint8Array into a quoted printable encoding
* This is almost the same as mimeEncode, except line breaks will be changed
* as well to ensure that the lines are never longer than allowed length
*
* @param {String|Uint8Array} data String or an Uint8Array to mime encode
* @param {String} [fromCharset='UTF-8'] Original charset of the string
* @return {String} Mime encoded string
*/
quotedPrintableEncode: function(data, fromCharset) {
var mimeEncodedStr = mimecodec.mimeEncode(data, fromCharset);
mimeEncodedStr = mimeEncodedStr.
// fix line breaks, ensure <CR><LF>
replace(/\r?\n|\r/g, '\r\n').
// replace spaces in the end of lines
replace(/[\t ]+$/gm, function(spaces) {
return spaces.replace(/ /g, '=20').replace(/\t/g, '=09');
});
// add soft line breaks to ensure line lengths sjorter than 76 bytes
return mimecodec._addSoftLinebreaks(mimeEncodedStr, 'qp');
},
/**
* Decodes a string from a quoted printable encoding. This is almost the
* same as mimeDecode, except line breaks will be changed as well
*
* @param {String} str Mime encoded string to decode
* @param {String} [fromCharset='UTF-8'] Original charset of the string
* @return {String} Mime decoded string
*/
quotedPrintableDecode: function(str, fromCharset) {
str = (str || '').toString();
str = str.
// remove invalid whitespace from the end of lines
replace(/[\t ]+$/gm, '').
// remove soft line breaks
replace(/\=(?:\r?\n|$)/g, '');
return mimecodec.mimeDecode(str, fromCharset);
},
/**
* Encodes a string or an Uint8Array to an UTF-8 MIME Word (rfc2047)
*
* @param {String|Uint8Array} data String to be encoded
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
* @param {String} [fromCharset='UTF-8'] Source sharacter set
* @return {String} Single or several mime words joined together
*/
mimeWordEncode: function(data, mimeWordEncoding, maxLength, fromCharset) {
mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0);
if (!fromCharset && typeof maxLength === 'string' && !maxLength.match(/^[0-9]+$/)) {
fromCharset = maxLength;
maxLength = undefined;
}
maxLength = maxLength || 0;
var encodedStr,
toCharset = 'UTF-8',
i, len, parts;
if (maxLength && maxLength > 7 + toCharset.length) {
maxLength -= (7 + toCharset.length);
}
if (mimeWordEncoding === 'Q') {
encodedStr = mimecodec.mimeEncode(data, fromCharset);
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
encodedStr = encodedStr.replace(/[^a-z0-9!*+\-\/=]/ig, function(chr) {
var code = chr.charCodeAt(0);
if(chr === ' '){
return '_';
}else{
return '=' + (code < 0x10 ? '0' : '') + code.toString(16).toUpperCase();
}
});
} else if (mimeWordEncoding === 'B') {
encodedStr = typeof data === 'string' ? data : mimecodec.decode(data, fromCharset);
maxLength = Math.max(3, (maxLength - maxLength % 4) / 4 * 3);
}
if (maxLength && encodedStr.length > maxLength) {
if (mimeWordEncoding === 'Q') {
encodedStr = mimecodec._splitMimeEncodedString(encodedStr, maxLength).join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
} else {
// RFC2047 6.3 (2) states that encoded-word must include an integral number of characters, so no chopping unicode sequences
parts = [];
for (i = 0, len = encodedStr.length; i < len; i += maxLength) {
parts.push(mimecodec.base64.encode(encodedStr.substr(i, maxLength)));
}
if (parts.length > 1) {
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?') + '?=';
} else {
encodedStr = parts.join('');
}
}
} else if (mimeWordEncoding === 'B') {
encodedStr = mimecodec.base64.encode(encodedStr);
}
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?=');
},
/**
* Finds word sequences with non ascii text and converts these to mime words
*
* @param {String|Uint8Array} data String to be encoded
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
* @param {String} [fromCharset='UTF-8'] Source sharacter set
* @return {String} String with possible mime words
*/
mimeWordsEncode: function(data, mimeWordEncoding, maxLength, fromCharset) {
if (!fromCharset && typeof maxLength === 'string' && !maxLength.match(/^[0-9]+$/)) {
fromCharset = maxLength;
maxLength = undefined;
}
maxLength = maxLength || 0;
var decodedValue = mimecodec.charset.decode(mimecodec.charset.convert((data || ''), fromCharset)),
encodedValue;
encodedValue = decodedValue.replace(/([^\s\u0080-\uFFFF]*[\u0080-\uFFFF]+[^\s\u0080-\uFFFF]*(?:\s+[^\s\u0080-\uFFFF]*[\u0080-\uFFFF]+[^\s\u0080-\uFFFF]*\s*)?)+/g, function(match) {
return match.length ? mimecodec.mimeWordEncode(match, mimeWordEncoding || 'Q', maxLength) : '';
});
return encodedValue;
},
/**
* Decode a complete mime word encoded string
*
* @param {String} str Mime word encoded string
* @return {String} Decoded unicode string
*/
mimeWordDecode: function(str) {
str = (str || '').toString().trim();
var fromCharset, encoding, match;
match = str.match(/^\=\?([\w_\-\*]+)\?([QqBb])\?([^\?]+)\?\=$/i);
if (!match) {
return str;
}
// RFC2231 added language tag to the encoding
// see: https://tools.ietf.org/html/rfc2231#section-5
// this implementation silently ignores this tag
fromCharset = match[1].split('*').shift();
encoding = (match[2] || 'Q').toString().toUpperCase();
str = (match[3] || '').replace(/_/g, ' ');
if (encoding === 'B') {
return mimecodec.base64Decode(str, fromCharset);
} else if (encoding === 'Q') {
return mimecodec.mimeDecode(str, fromCharset);
} else {
return str;
}
},
/**
* Decode a string that might include one or several mime words
*
* @param {String} str String including some mime words that will be encoded
* @return {String} Decoded unicode string
*/
mimeWordsDecode: function(str) {
str = (str || "").toString().replace(/(=\?[^?]+\?[QqBb]\?[^?]+\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, "$1");
str = str.replace(/\?==\?[uU][tT][fF]-8\?[QqBb]\?/g, ""); // join bytes of multi-byte UTF-8
str = str.replace(/\=\?[\w_\-\*]+\?[QqBb]\?[^\?]+\?\=/g, function(mimeWord) {
return mimecodec.mimeWordDecode(mimeWord.replace(/\s+/g, ''));
});
return str;
},
/**
* Folds long lines, useful for folding header lines (afterSpace=false) and
* flowed text (afterSpace=true)
*
* @param {String} str String to be folded
* @param {Number} [lineLengthMax=76] Maximum length of a line
* @param {Boolean} afterSpace If true, leave a space in th end of a line
* @return {String} String with folded lines
*/
foldLines: function(str, lineLengthMax, afterSpace) {
str = (str || '').toString();
lineLengthMax = lineLengthMax || 76;
var pos = 0,
len = str.length,
result = '',
line, match;
while (pos < len) {
line = str.substr(pos, lineLengthMax);
if (line.length < lineLengthMax) {
result += line;
break;
}
if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
line = match[0];
result += line;
pos += line.length;
continue;
} else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
}
result += line;
pos += line.length;
if (pos < len) {
result += '\r\n';
}
}
return result;
},
/**
* Encodes and folds a header line for a MIME message header.
* Shorthand for mimeWordsEncode + foldLines
*
* @param {String} key Key name, will not be encoded
* @param {String|Uint8Array} value Value to be encoded
* @param {String} [fromCharset='UTF-8'] Character set of the value
* @return {String} encoded and folded header line
*/
headerLineEncode: function(key, value, fromCharset) {
var encodedValue = mimecodec.mimeWordsEncode(value, 'Q', 52, fromCharset);
return mimecodec.foldLines(key + ': ' + encodedValue, 76);
},
/**
* Splits a string by :
* The result is not mime word decoded, you need to do your own decoding based
* on the rules for the specific header key
*
* @param {String} headerLine Single header line, might include linebreaks as well if folded
* @return {Object} And object of {key, value}
*/
headerLineDecode: function(headerLine) {
var line = (headerLine || '').toString().replace(/(?:\r?\n|\r)[ \t]*/g, ' ').trim(),
match = line.match(/^\s*([^:]+):(.*)$/),
key = (match && match[1] || '').trim(),
value = (match && match[2] || '').trim();
return {
key: key,
value: value
};
},
/**
* Parses a block of header lines. Does not decode mime words as every
* header might have its own rules (eg. formatted email addresses and such)
*
* @param {String} headers Headers string
* @return {Object} An object of headers, where header keys are object keys. NB! Several values with the same key make up an Array
*/
headerLinesDecode: function(headers) {
var lines = headers.split(/\r?\n|\r/),
headersObj = {},
key, value,
header,
i, len;
for (i = lines.length - 1; i >= 0; i--) {
if (i && lines[i].match(/^\s/)) {
lines[i - 1] += '\r\n' + lines[i];
lines.splice(i, 1);
}
}
for (i = 0, len = lines.length; i < len; i++) {
header = mimecodec.headerLineDecode(lines[i]);
key = (header.key || '').toString().toLowerCase().trim();
value = header.value || '';
if (!headersObj[key]) {
headersObj[key] = value;
} else {
headersObj[key] = [].concat(headersObj[key], value);
}
}
return headersObj;
},
/**
* Converts 'binary' string to an Uint8Array
*
* @param {String} 'binary' string
* @return {Uint8Array} Octet stream buffer
*/
toTypedArray: function(binaryString) {
var buf = new Uint8Array(binaryString.length);
for (var i = 0, len = binaryString.length; i < len; i++) {
buf[i] = binaryString.charCodeAt(i);
}
return buf;
},
/**
* Converts an Uint8Array to 'binary' string
*
* @param {Uint8Array} buf Octet stream buffer
* @return {String} 'binary' string
*/
fromTypedArray: function(buf) {
var i, l;
// ensure the value is a Uint8Array, not ArrayBuffer if used
if (!buf.buffer) {
buf = new Uint8Array(buf);
}
var sbits = new Array(buf.length);
for (i = 0, l = buf.length; i < l; i++) {
sbits[i] = String.fromCharCode(buf[i]);
}
return sbits.join('');
},
/**
* Parses a header value with key=value arguments into a structured
* object.
*
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
* {
* 'value': 'text/plain',
* 'params': {
* 'charset': 'UTF-8'
* }
* }
*
* @param {String} str Header value
* @return {Object} Header value as a parsed structure
*/
parseHeaderValue: function(str) {
var response = {
value: false,
params: {}
},
key = false,
value = '',
type = 'value',
quote = false,
escaped = false,
chr;
for (var i = 0, len = str.length; i < len; i++) {
chr = str.charAt(i);
if (type === 'key') {
if (chr === '=') {
key = value.trim().toLowerCase();
type = 'value';
value = '';
continue;
}
value += chr;
} else {
if (escaped) {
value += chr;
} else if (chr === '\\') {
escaped = true;
continue;
} else if (quote && chr === quote) {
quote = false;
} else if (!quote && chr === '"') {
quote = chr;
} else if (!quote && chr === ';') {
if (key === false) {
response.value = value.trim();
} else {
response.params[key] = value.trim();
}
type = 'key';
value = '';
} else {
value += chr;
}
escaped = false;
}
}
if (type === 'value') {
if (key === false) {
response.value = value.trim();
} else {
response.params[key] = value.trim();
}
} else if (value.trim()) {
response.params[value.trim().toLowerCase()] = '';
}
// handle parameter value continuations
// https://tools.ietf.org/html/rfc2231#section-3
// preprocess values
Object.keys(response.params).forEach(function(key) {
var actualKey, nr, match, value;
if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) {
actualKey = key.substr(0, match.index);
nr = Number(match[2] || match[3]) || 0;
if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
response.params[actualKey] = {
charset: false,
values: []
};
}
value = response.params[key];
if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
response.params[actualKey].charset = match[1] || 'iso-8859-1';
value = match[2];
}
response.params[actualKey].values[nr] = value;
// remove the old reference
delete response.params[key];
}
});
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words
Object.keys(response.params).forEach(function(key) {
var value;
if (response.params[key] && Array.isArray(response.params[key].values)) {
value = response.params[key].values.map(function(val) {
return val || '';
}).join('');
if (response.params[key].charset) {
// convert "%AB" to "=?charset?Q?=AB?="
response.params[key] = '=?' +
response.params[key].charset +
'?Q?' +
value.
// fix invalidly encoded chars
replace(/[=\?_\s]/g, function(s) {
var c = s.charCodeAt(0).toString(16);
if (s === ' ') {
return '_';
} else {
return '%' + (c.length < 2 ? '0' : '') + c;
}
}).
// change from urlencoding to percent encoding
replace(/%/g, '=') +
'?=';
} else {
response.params[key] = value;
}
}
}.bind(this));
return response;
},
/**
* Encodes a string or an Uint8Array to an UTF-8 Parameter Value Continuation encoding (rfc2231)
* Useful for splitting long parameter values.
*
* For example
* title="unicode string"
* becomes
* title*0*="utf-8''unicode"
* title*1*="%20string"
*
* @param {String|Uint8Array} data String to be encoded
* @param {Number} [maxLength=50] Max length for generated chunks
* @param {String} [fromCharset='UTF-8'] Source sharacter set
* @return {Array} A list of encoded keys and headers
*/
continuationEncode: function(key, data, maxLength, fromCharset) {
var list = [];
var encodedStr = typeof data === 'string' ? data : mimecodec.decode(data, fromCharset);
var chr;
var line;
var startPos = 0;
var isEncoded = false;
maxLength = maxLength || 50;
// process ascii only text
if (/^[\w.\- ]*$/.test(data)) {
// check if conversion is even needed
if (encodedStr.length <= maxLength) {
return [{
key: key,
value: /[\s";=]/.test(encodedStr) ? '"' + encodedStr + '"' : encodedStr
}];
}
encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), function(str) {
list.push({
line: str
});
return '';
});
if (encodedStr) {
list.push({
line: encodedStr
});
}
} else {
// first line includes the charset and language info and needs to be encoded
// even if it does not contain any unicode characters
line = 'utf-8\'\'';
isEncoded = true;
startPos = 0;
// process text with unicode or special chars
for (var i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr[i];
if (isEncoded) {
chr = encodeURIComponent(chr);
} else {
// try to urlencode current char
chr = chr === ' ' ? chr : encodeURIComponent(chr);
// By default it is not required to encode a line, the need
// only appears when the string contains unicode or special chars
// in this case we start processing the line over and encode all chars
if (chr !== encodedStr[i]) {
// Check if it is even possible to add the encoded char to the line
// If not, there is no reason to use this line, just push it to the list
// and start a new line with the char that needs encoding
if ((encodeURIComponent(line) + chr).length >= maxLength) {
list.push({
line: line,
encoded: isEncoded
});
line = '';
startPos = i - 1;
} else {
isEncoded = true;
i = startPos;
line = '';
continue;
}
}
}
// if the line is already too long, push it to the list and start a new one
if ((line + chr).length >= maxLength) {
list.push({
line: line,
encoded: isEncoded
});
line = chr = encodedStr[i] === ' ' ? ' ' : encodeURIComponent(encodedStr[i]);
if (chr === encodedStr[i]) {
isEncoded = false;
startPos = i - 1;
} else {
isEncoded = true;
}
} else {
line += chr;
}
}
if (line) {
list.push({
line: line,
encoded: isEncoded
});
}
}
return list.map(function(item, i) {
return {
// encoded lines: {name}*{part}*
// unencoded lines: {name}*{part}
// if any line needs to be encoded then the first line (part==0) is always encoded
key: key + '*' + i + (item.encoded ? '*' : ''),
value: /[\s";=]/.test(item.line) ? '"' + item.line + '"' : item.line
};
});
},
/**
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
*
* @param {String} str Mime encoded string to be split up
* @param {Number} maxlen Maximum length of characters for one part (minimum 12)
* @return {Array} Split string
*/
_splitMimeEncodedString: function(str, maxlen) {
var curLine, match, chr, done,
lines = [];
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
maxlen = Math.max(maxlen || 0, 12);
while (str.length) {
curLine = str.substr(0, maxlen);
// move incomplete escaped char back to main
if ((match = curLine.match(/\=[0-9A-F]?$/i))) {
curLine = curLine.substr(0, match.index);
}
done = false;
while (!done) {
done = true;
// check if not middle of a unicode char sequence
if ((match = str.substr(curLine.length).match(/^\=([0-9A-F]{2})/i))) {
chr = parseInt(match[1], 16);
// invalid sequence, move one char back anc recheck
if (chr < 0xC2 && chr > 0x7F) {
curLine = curLine.substr(0, curLine.length - 3);
done = false;
}
}
}
if (curLine.length) {
lines.push(curLine);
}
str = str.substr(curLine.length);
}
return lines;
},
/**
* Adds soft line breaks (the ones that will be stripped out when decoding) to
* ensure that no line in the message is never longer than 76 symbols
*
* Lines can't be longer than 76 + <CR><LF> = 78 bytes
* http://tools.ietf.org/html/rfc2045#section-6.7
*
* @param {String} str Encoded string
* @param {String} encoding Either "qp" or "base64" (the default)
* @return {String} String with forced line breaks
*/
_addSoftLinebreaks: function(str, encoding) {
var lineLengthMax = 76;
encoding = (encoding || 'base64').toString().toLowerCase().trim();
if (encoding === 'qp') {
return mimecodec._addQPSoftLinebreaks(str, lineLengthMax);
} else {
return mimecodec._addBase64SoftLinebreaks(str, lineLengthMax);
}
},
/**
* Adds soft line breaks (the ones that will be stripped out when decoding base64) to
* ensure that no line in the message is never longer than lineLengthMax
*
* @param {String} base64EncodedStr String in BASE64 encoding
* @param {Number} lineLengthMax Maximum length of a line
* @return {String} String with forced line breaks
*/
_addBase64SoftLinebreaks: function(base64EncodedStr, lineLengthMax) {
base64EncodedStr = (base64EncodedStr || '').toString().trim();
return base64EncodedStr.replace(new RegExp('.{' + lineLengthMax + '}', 'g'), '$&\r\n').trim();
},
/**
* Adds soft line breaks(the ones that will be stripped out when decoding QP) to * ensure that no line in the message is never longer than lineLengthMax * * Not sure of how and why this works, but at least it seems to be working: /
*
* @param {String} qpEncodedStr String in Quoted-Printable encoding
* @param {Number} lineLengthMax Maximum length of a line
* @return {String} String with forced line breaks
*/
_addQPSoftLinebreaks: function(qpEncodedStr, lineLengthMax) {
qpEncodedStr = (qpEncodedStr || '').toString();
lineLengthMax = lineLengthMax || 76;
var pos = 0,
len = qpEncodedStr.length,
match, code, line,
lineMargin = Math.floor(lineLengthMax / 3),
result = '';
// insert soft linebreaks where needed
while (pos < len) {
line = qpEncodedStr.substr(pos, lineLengthMax);
if ((match = line.match(/\r\n/))) {
line = line.substr(0, match.index + match[0].length);
result += line;
pos += line.length;
continue;
}
if (line.substr(-1) === '\n') {
// nothing to change here
result += line;
pos += line.length;
continue;
} else if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
// truncate to nearest line break
line = line.substr(0, line.length - (match[0].length - 1));
result += line;
pos += line.length;
continue;
} else if (line.length > lineLengthMax - lineMargin && (match = line.substr(-lineMargin).match(/[ \t\.,!\?][^ \t\.,!\?]*$/))) {
// truncate to nearest space
line = line.substr(0, line.length - (match[0].length - 1));
} else if (line.substr(-1) === '\r') {
line = line.substr(0, line.length - 1);
} else {
if (line.match(/\=[\da-f]{0,2}$/i)) {
// push incomplete encoding sequences to the next line
if ((match = line.match(/\=[\da-f]{0,1}$/i))) {
line = line.substr(0, line.length - match[0].length);
}
// ensure that utf-8 sequences are not split
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/\=[\da-f]{2}$/ig))) {
code = parseInt(match[0].substr(1, 2), 16);
if (code < 128) {
break;
}
line = line.substr(0, line.length - 3);
if (code >= 0xC0) {
break;
}
}
}
}
if (pos + line.length < len && line.substr(-1) !== '\n') {
if (line.length === lineLengthMax && line.match(/\=[\da-f]{2}$/i)) {
line = line.substr(0, line.length - 3);
} else if (line.length === lineLengthMax) {
line = line.substr(0, line.length - 1);
}
pos += line.length;
line += '=\r\n';
} else {
pos += line.length;
}
result += line;
}
return result;
},
/**
* Checks if a number is in specified ranges or not
*
* @param {Number} nr Number to check for
* @ranges {Array} ranges Array of range duples
* @return {Boolean} Returns true, if nr was found to be at least one of the specified ranges
*/
_checkRanges: function(nr, ranges) {
for (var i = ranges.length - 1; i >= 0; i--) {
if (!ranges[i].length) {
continue;
}
if (ranges[i].length === 1 && nr === ranges[i][0]) {
return true;
}
if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) {
return true;
}
}
return false;
}
};
/**
* Character set encoding and decoding functions
*/
mimecodec.charset = {
/**
* Encodes an unicode string into an Uint8Array object as UTF-8
*
* TextEncoder only supports unicode encodings (utf-8, utf16le/be) but no other,
* so we force UTF-8 here.
*
* @param {String} str String to be encoded
* @return {Uint8Array} UTF-8 encoded typed array
*/
encode: function(str) {
return new TextEncoder('UTF-8').encode(str);
},
/**
* Decodes a string from Uint8Array to an unicode string using specified encoding
*
* @param {Uint8Array} buf Binary data to be decoded
* @param {String} [fromCharset='UTF-8'] Binary data is decoded into string using this charset
* @return {String} Decded string
*/
decode: function(buf, fromCharset) {
fromCharset = mimecodec.charset.normalizeCharset(fromCharset || 'UTF-8');
// ensure the value is a Uint8Array, not ArrayBuffer if used
if (!buf.buffer) {
buf = new Uint8Array(buf);
}
try {
return new TextDecoder(fromCharset).decode(buf);
} catch (E) {
try {
return new TextDecoder('utf-8', {
fatal: true // if the input is not a valid utf-8 the decoder will throw
}).decode(buf);
} catch (E) {
try {
return new TextDecoder('iso-8859-15').decode(buf);
} catch (E) {
// should not happen as there is something matching for every byte (non character bytes are allowed)
return mimecodec.fromTypedArray(buf);
}
}
}
},
/**
* Convert a string from specific encoding to UTF-8 Uint8Array
*
* @param {String|Uint8Array} str String to be encoded
* @param {String} [fromCharset='UTF-8'] Source encoding for the string
* @return {Uint8Array} UTF-8 encoded typed array
*/
convert: function(data, fromCharset) {
fromCharset = mimecodec.charset.normalizeCharset(fromCharset || 'UTF-8');
var bufString;
if (typeof data !== 'string') {
if (fromCharset.match(/^utf[\-_]?8$/)) {
return data;
}
bufString = mimecodec.charset.decode(data, fromCharset);
return mimecodec.charset.encode(bufString);
}
return mimecodec.charset.encode(data);
},
/**
* Converts well known invalid character set names to proper names.
* eg. win-1257 will be converted to WINDOWS-1257
*
* @param {String} charset Charset name to convert
* @return {String} Canoninicalized charset name
*/
normalizeCharset: function(charset) {
var match;
if ((match = charset.match(/^utf[\-_]?(\d+)$/i))) {
return 'UTF-' + match[1];
}
if ((match = charset.match(/^win[\-_]?(\d+)$/i))) {
return 'WINDOWS-' + match[1];
}
if ((match = charset.match(/^latin[\-_]?(\d+)$/i))) {
return 'ISO-8859-' + match[1];
}
return charset;
}
};
/**
* Base64 encoding and decoding functions
*/
mimecodec.base64 = {
/**
* Encodes input into base64
*
* @param {String|Uint8Array} data Data to be encoded into base64
* @return {String} Base64 encoded string
*/
encode: function(data) {
if (!data) {
return '';
}
if (typeof data === 'string') {
// window.btoa uses pseudo binary encoding, so unicode strings
// need to be converted before encoding
return btoa(unescape(encodeURIComponent(data)));
}
var len = data.byteLength,
binStr = '';
if (!data.buffer) {
data.buffer = new Uint8Array(data);
}
for (var i = 0; i < len; i++) {
binStr += String.fromCharCode(data[i]);
}
return btoa(binStr);
},
/**
* Decodes base64 encoded string into an unicode string or Uint8Array
*
* @param {String} data Base64 encoded data
* @param {String} [outputEncoding='buffer'] Output encoding, either 'string' or 'buffer' (Uint8Array)
* @return {String|Uint8Array} Decoded string
*/
decode: function(data, outputEncoding) {
outputEncoding = (outputEncoding || 'buffer').toLowerCase().trim();
var buf = mimecodec.base64.toTypedArray(data);
if (outputEncoding === 'string') {
return mimecodec.charset.decode(buf);
} else {
return buf;
}
},
/**
* Safe base64 decoding. Does not throw on unexpected input.
*
* Implementation from the MDN docs:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
* (MDN code samples are MIT licensed)
*
* @param {String} base64Str Base64 encoded string
* @returns {Uint8Array} Decoded binary blob
*/
toTypedArray: function(base64Str) {
var bitsSoFar = 0;
var validBits = 0;
var iOut = 0;
var arr = new Uint8Array(Math.ceil(base64Str.length * 3 / 4));
var c;
var bits;
for (var i = 0, len = base64Str.length; i < len; i++) {
c = base64Str.charCodeAt(i);
if (c >= 0x41 && c <= 0x5a) { // [A-Z]
bits = c - 0x41;
} else if (c >= 0x61 && c <= 0x7a) { // [a-z]
bits = c - 0x61 + 0x1a;
} else if (c >= 0x30 && c <= 0x39) { // [0-9]
bits = c - 0x30 + 0x34;
} else if (c === 0x2b) { // +
bits = 0x3e;
} else if (c === 0x2f) { // /
bits = 0x3f;
} else if (c === 0x3d) { // =
validBits = 0;
continue;
} else {
// ignore all other characters!
continue;
}
bitsSoFar = (bitsSoFar << 6) | bits;
validBits += 6;
if (validBits >= 8) {
validBits -= 8;
arr[iOut++] = bitsSoFar >> validBits;
if (validBits === 2) {
bitsSoFar &= 0x03;
} else if (validBits === 4) {
bitsSoFar &= 0x0f;
}
}
}
if (iOut < arr.length) {
return arr.subarray(0, iOut);
}
return arr;
}
};
/*
* Encodes a string in base 64. DedicatedWorkerGlobalScope for Safari does not provide btoa.
* https://github.com/davidchambers/Base64.js
*/
function base64Encode(input) {
var str = String(input);
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
for (var block, charCode, idx = 0, map = chars, output = ''; str.charAt(idx | 0) || (map = '=', idx % 1); output += map.charAt(63 & block >> 8 - idx % 1 * 8)) {
charCode = str.charCodeAt(idx += 3 / 4);
if (charCode > 0xFF) {
throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
}
block = block << 8 | charCode;
}
return output;
}
return mimecodec;
}());
@vvelizariy
Copy link
Author

Link on https://github.com/emailjs/emailjs-mime-codec for "mimecodec.js " file is broken. So I've uploaded here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment