New:Socket for Asana Is Now Available.Learn more
Get Started

libmime

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

libmime - npm Package Compare versions

Comparing version
5.4.1
to
5.4.2
+7
-0
CHANGELOG.md
# Changelog
## [5.4.2](https://github.com/nodemailer/libmime/compare/v5.4.1...v5.4.2) (2026-08-07)
### Bug Fixes
* harden header parsing against prototype pollution and parser differentials ([c470201](https://github.com/nodemailer/libmime/commit/c47020107045864c14d0c526acff1c9d2e12ed88))
## [5.4.1](https://github.com/nodemailer/libmime/compare/v5.4.0...v5.4.1) (2026-07-05)

@@ -4,0 +11,0 @@

+156
-98

@@ -13,2 +13,27 @@ /* eslint no-control-regex: 0, no-div-regex: 0, quotes: 0 */

// Sets an own property on a plain object. A regular assignment with a
// '__proto__' key would silently modify the object's prototype instead of
// creating an own property, so it needs special handling.
const setOwnProperty = (obj, key, value) => {
if (key === '__proto__') {
Object.defineProperty(obj, key, {
value,
writable: true,
enumerable: true,
configurable: true
});
} else {
obj[key] = value;
}
};
// Checks own properties only, as attacker controlled keys like '__proto__'
// or 'toString' would otherwise resolve to inherited Object.prototype members.
// NB! Not named hasOwnProperty, that would shadow the global builtin
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
// Whitespace that separates the tokens of a header value. Kept as a char comparison,
// a regex test would be run for every single char of every parsed header
const isWSP = chr => chr === ' ' || chr === '\t' || chr === '\r' || chr === '\n' || chr === '\f' || chr === '\v';
class Libmime {

@@ -390,3 +415,3 @@ constructor(config) {

* @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
* @return {Object} An object of headers, where header keys are object keys and every value is an Array of the values for that key
*/

@@ -400,4 +425,17 @@ decodeHeaders(headers) {

for (i = lines.length - 1; i >= 0; i--) {
if (i && lines[i].match(/^\s/)) {
// Empty lines in front of the block are not part of any header, a caller might
// include the line break that separates a mime part boundary from its headers
let headersPos = 0;
while (headersPos < lines.length && lines[headersPos] === '') {
headersPos++;
}
// An empty line terminates the header block, anything after it is the message body.
// Without this a body line that starts with whitespace would be folded into the last header
let bodyPos = lines.indexOf('', headersPos);
lines = lines.slice(headersPos, bodyPos >= 0 ? bodyPos : lines.length);
// unfold folded lines, a continuation line always starts with a space or a tab
for (i = lines.length - 1; i > 0; i--) {
if (/^[ \t]/.test(lines[i])) {
lines[i - 1] += '\r\n' + lines[i];

@@ -410,4 +448,4 @@ lines.splice(i, 1);

header = this.decodeHeader(lines[i]);
if (!headersObj[header.key]) {
headersObj[header.key] = [header.value];
if (!hasOwn(headersObj, header.key)) {
setOwnProperty(headersObj, header.key, [header.value]);
} else {

@@ -456,3 +494,3 @@ headersObj[header.key].push(header.value);

*
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
* parseHeaderValue('text/plain; CHARSET=UTF-8') ->
* {

@@ -475,2 +513,5 @@ * 'value': 'text/plain',

let value = '';
// Length of the significant part of `value`. Whitespace outside of quotes is
// not part of the value, while whitespace inside quotes is (rfc2045 section 5.1)
let valueEnd = 0;
let stage = STAGE_VALUE;

@@ -482,2 +523,24 @@

// Stores the collected value, either as the default value, as the value for the
// current key, or as a key that has no value at all. Runs once per parameter
let commit = () => {
let collected = value.substring(0, valueEnd);
value = '';
valueEnd = 0;
if (stage === STAGE_KEY) {
// key without a value, see emptykey:
// Header-Key: somevalue; key=value; emptykey
if (collected) {
setOwnProperty(response.params, collected.toLowerCase(), '');
}
} else if (key === false) {
// default value
response.value = collected;
} else {
// subkey value
setOwnProperty(response.params, key, collected);
}
};
for (let i = 0, len = str.length; i < len; i++) {

@@ -488,8 +551,17 @@ chr = str.charAt(i);

if (chr === '=') {
key = value.trim().toLowerCase();
key = value.substring(0, valueEnd).toLowerCase();
value = '';
valueEnd = 0;
stage = STAGE_VALUE;
value = '';
break;
} else if (chr === ';') {
// key without a value, eg. the "inline" in "attachment; inline; filename=a.txt"
commit();
} else if (isWSP(chr)) {
if (value.length) {
value += chr;
}
} else {
value += chr;
valueEnd = value.length;
}
value += chr;
break;

@@ -499,19 +571,19 @@ case STAGE_VALUE:

value += chr;
valueEnd = value.length;
} else if (chr === '\\') {
escaped = true;
continue;
} else if (quote && chr === quote) {
quote = false;
} else if (!quote && chr === '"') {
quote = chr;
} else if (chr === '"') {
// every char between the quotes is significant, including whitespace
quote = !quote;
} else if (!quote && chr === ';') {
if (key === false) {
response.value = value.trim();
} else {
response.params[key] = value.trim();
commit();
stage = STAGE_KEY;
} else if (!quote && isWSP(chr)) {
if (value.length) {
value += chr;
}
stage = STAGE_KEY;
value = '';
} else {
value += chr;
valueEnd = value.length;
}

@@ -524,16 +596,3 @@ escaped = false;

// finalize remainder
value = value.trim();
if (stage === STAGE_VALUE) {
if (key === false) {
// default value
response.value = value;
} else {
// subkey value
response.params[key] = value;
}
} else if (value) {
// treat as key without value, see emptykey:
// Header-Key: somevalue; key=value; emptykey
response.params[value.toLowerCase()] = '';
}
commit();

@@ -543,8 +602,7 @@ // handle parameter value continuations

// preprocess values
Object.keys(response.params).forEach(key => {
let actualKey;
let nr;
let value;
// Collect the segments of every continuation parameter. These are kept out of
// response.params so that a half assembled value can not leak into the result
let continuations = new Map();
for (let key of Object.keys(response.params)) {
let match = key.match(/\*((\d+)\*?)?$/);

@@ -554,63 +612,63 @@

// nothing to do here, does not seem like a continuation param
return;
continue;
}
actualKey = key.substr(0, match.index).toLowerCase();
nr = Number(match[2]) || 0;
let actualKey = key.substr(0, match.index).toLowerCase();
let nr = Number(match[2]) || 0;
let value = response.params[key];
if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
response.params[actualKey] = {
// remove the old reference
delete response.params[key];
let continuation = continuations.get(actualKey);
if (!continuation) {
continuation = {
charset: false,
values: []
};
continuations.set(actualKey, continuation);
}
value = response.params[key];
if (nr === 0 && match[0].charAt(match[0].length - 1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
response.params[actualKey].charset = match[1] || 'utf-8';
continuation.charset = match[1] || 'utf-8';
value = match[2];
}
response.params[actualKey].values.push({ nr, value });
continuation.values.push({ nr, value });
}
// remove the old reference
delete response.params[key];
});
// concatenate the split rfc2231 strings and decode the encoded ones
for (let [key, continuation] of continuations) {
// NB! An assembled continuation always overrides a plain parameter with the same
// name, in either source order. The plain form is the legacy ascii fallback of the
// extended one, so rfc6266 section 4.3 requires picking the extended value. Letting
// the fallback win would make libmime disagree with the mail clients that follow it
let value = continuation.values
.sort((a, b) => a.nr - b.nr)
.map(val => val.value)
.join('');
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words
Object.keys(response.params).forEach(key => {
let value;
if (response.params[key] && Array.isArray(response.params[key].values)) {
value = response.params[key].values
.sort((a, b) => a.nr - b.nr)
.map(val => (val && val.value) || '')
.join('');
if (response.params[key].charset) {
// convert "%AB" to "=?charset?Q?=AB?=" and then to unicode
response.params[key] = this.decodeWords(
'=?' +
response.params[key].charset +
'?Q?' +
value
// fix invalidly encoded chars
.replace(/[=?_\s]/g, s => {
let 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] = this.decodeWords(value);
}
if (!continuation.charset) {
setOwnProperty(response.params, key, this.decodeWords(value));
continue;
}
});
// convert "%AB" to the quoted printable "=AB" and decode it as the charset of the parameter.
// NB! The charset is passed in as an argument instead of building a "=?charset?Q?=AB?=" string,
// a charset that contains "?" would otherwise inject an encoded word of its own
let qpValue = value
// fix invalidly encoded chars
.replace(/[=_\s]/g, s => {
if (s === ' ') {
return '_';
}
let c = s.charCodeAt(0).toString(16);
return '%' + (c.length < 2 ? '0' : '') + c;
})
// change from urlencoding to percent encoding
.replace(/%/g, '=');
setOwnProperty(response.params, key, this.decodeWord(continuation.charset, 'Q', qpValue));
}
return response;

@@ -637,3 +695,9 @@ }

let list = [];
let encodedStr = typeof data === 'string' ? data : this.decode(data, fromCharset);
if (typeof data !== 'string' && !Buffer.isBuffer(data)) {
// documented input is a string or a Buffer, normalize anything else
data = data === null || data === undefined ? '' : data.toString();
}
let encodedStr = typeof data === 'string' ? data : libcharset.decode(data, fromCharset);
let encodedStrArr;

@@ -772,3 +836,3 @@ let chr, ord;

mimeType = (mimeType || '').toString().toLowerCase().replace(/\s/g, '');
if (!(mimeType in mimetypes.list)) {
if (!hasOwn(mimetypes.list, mimeType)) {
return 'bin';

@@ -804,3 +868,3 @@ }

if (!(extension in mimetypes.extensions)) {
if (!hasOwn(mimetypes.extensions, extension)) {
return 'application/octet-stream';

@@ -923,16 +987,10 @@ }

let res = '';
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
// percent encoding is defined over bytes, so encode the char as UTF-8 first
let buf = Buffer.from(chr, 'utf-8');
if (ord.length % 2) {
ord = '0' + ord;
for (let i = 0, len = buf.length; i < len; i++) {
let ord = buf[i].toString(16).toUpperCase();
res += '%' + (ord.length < 2 ? '0' : '') + ord;
}
if (ord.length > 2) {
for (let i = 0, len = ord.length / 2; i < len; i++) {
res += '%' + ord.substr(i, 2);
}
} else {
res += '%' + ord;
}
return res;

@@ -939,0 +997,0 @@ }

{
"name": "libmime",
"description": "Encode and decode quoted printable and base64 strings",
"version": "5.4.1",
"version": "5.4.2",
"main": "lib/libmime.js",

@@ -36,8 +36,8 @@ "files": [

"eslint-config-prettier": "10.1.8",
"grunt": "1.6.2",
"grunt": "1.6.3",
"grunt-cli": "1.5.0",
"grunt-eslint": "24.3.0",
"grunt-mocha-test": "0.13.3",
"mocha": "11.7.6"
"mocha": "11.8.0"
}
}