Files
send-mail/node_modules/nodemailer/dist/cjs/mime-funcs/index.js
T

674 lines
26 KiB
JavaScript
Raw Normal View History

2026-09-15 13:00:32 +02:00
"use strict";
/* eslint no-control-regex:0 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.isPlainText = isPlainText;
exports.quoteString = quoteString;
exports.hasLongerLines = hasLongerLines;
exports.encodeWord = encodeWord;
exports.encodeWords = encodeWords;
exports.buildHeaderValue = buildHeaderValue;
exports.buildHeaderParam = buildHeaderParam;
exports.parseHeaderValue = parseHeaderValue;
exports.detectExtension = detectExtension;
exports.detectMimeType = detectMimeType;
exports.foldLines = foldLines;
exports.splitMimeEncodedString = splitMimeEncodedString;
exports.encodeURICharComponent = encodeURICharComponent;
exports.safeEncodeURIComponent = safeEncodeURIComponent;
const base64 = __importStar(require("../base64/index.js"));
const qp = __importStar(require("../qp/index.js"));
const mimeTypes = __importStar(require("./mime-types.js"));
const objects_js_1 = require("../shared/objects.js");
/**
* Checks if a value is plaintext string (uses only printable 7bit chars)
*
* When isParam is set the value is destined for a header parameter, so HT, CR and LF
* are not plaintext either: a header parameter has no way to carry them. HT is a valid
* fold point, so folding and unfolding a header would rewrite it as a space, and CR/LF
* cannot appear in a header value at all. DEL is neither a token character nor qtext,
* so it can not be carried bare or quoted. Such values have to go through the rfc2231
* parameter continuation encoding instead, the same way a quote already does.
*
* @param value String to be tested
* @param [isParam] Set to true if the value is a header parameter value
* @returns true if it is a plaintext string
*/
function isPlainText(value, isParam) {
const re = isParam ? /[\x00-\x1f\x7f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
return typeof value === 'string' && !re.test(value);
}
/**
* Wraps a value into a quoted-string. Inside one a quote would end the string early
* and a backslash would escape whatever follows it, so both go out as quoted-pairs.
*
* @param value String to be quoted
* @returns The value as a quoted-string, quotes included
*/
function quoteString(value) {
return '"' + (value || '').toString().replace(/["\\]/g, '\\$&') + '"';
}
/**
* Checks if a multi line string containes lines longer than the selected value.
*
* Useful when detecting if a mail message needs any processing at all:
* if only plaintext characters are used and lines are short, then there is
* no need to encode the values in any way. If the value is plaintext but has
* longer lines then allowed, then use format=flowed
*
* @param lineLength Max line length to check for
* @returns Returns true if there is at least one line longer than lineLength chars
*/
function hasLongerLines(str, lineLength) {
if (str.length > 128 * 1024) {
// do not test strings longer than 128kB
return true;
}
return new RegExp('^.{' + (lineLength + 1) + ',}', 'm').test(str);
}
/**
* Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047)
*
* @param data String to be encoded
* @param mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param [maxLength=0] If set, split mime words into several chunks if needed
* @return Single or several mime words joined together
*/
function encodeWord(data, mimeWordEncoding, maxLength) {
mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0);
maxLength = maxLength || 0;
let encodedStr;
const toCharset = 'UTF-8';
if (maxLength && maxLength > 7 + toCharset.length) {
maxLength -= 7 + toCharset.length;
}
if (mimeWordEncoding === 'Q') {
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => {
const ord = chr.charCodeAt(0).toString(16).toUpperCase();
if (chr === ' ') {
return '_';
}
return '=' + (ord.length === 1 ? '0' + ord : ord);
});
}
else if (mimeWordEncoding === 'B') {
// the chunking loop below splits raw text and base64 encodes each part, so a
// Buffer goes in as its UTF-8 string: handing it the base64 of the whole input
// would encode the encoding itself and decode back to base64 text
encodedStr = typeof data === 'string' ? data : data.toString('utf-8');
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
}
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) {
if (mimeWordEncoding === 'Q') {
encodedStr = 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
const parts = [];
let lpart = '';
for (let i = 0, len = encodedStr.length; i < len; i++) {
let chr = encodedStr.charAt(i);
if (/[\ud800-\udbff]/.test(chr) && /[\udc00-\udfff]/.test(encodedStr.charAt(i + 1))) {
// leading surrogate, so add the trailing surrogate as well
// an unpaired one must not swallow the next unit, that would destroy
// a valid pair following it
chr += encodedStr.charAt(++i);
}
// check if we can add this character to the existing string
// without breaking byte length limit
if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) {
lpart += chr;
}
else {
// we hit the length limit, so push the existing string and start over
parts.push(base64.encode(lpart));
lpart = chr;
}
}
if (lpart) {
parts.push(base64.encode(lpart));
}
if (parts.length > 1) {
encodedStr = parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
}
else {
encodedStr = parts.join('');
}
}
}
else if (mimeWordEncoding === 'B') {
encodedStr = base64.encode(data);
}
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?=');
}
/**
* Finds word sequences with non ascii text and converts these to mime words
*
* @param value String to be encoded
* @param mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param [maxLength=0] If set, split mime words into several chunks if needed
* @param [encodeAll=false] If true and the value needs encoding then encodes entire string, not just the smallest match
* @return String with possible mime words
*/
function encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
maxLength = maxLength || 0;
// find first word with a non-printable ascii or special symbol in it
const firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
if (!firstMatch) {
return value;
}
if (encodeAll) {
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
return encodeWord(value, mimeWordEncoding, maxLength);
}
// find the last word with a non-printable ascii in it
const lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
if (!lastMatch) {
// should not happen
return value;
}
const startIndex = firstMatch.index +
(firstMatch[0].match(/[^\s]/) || {
index: 0
}).index;
const endIndex = lastMatch.index + (lastMatch[1] || '').length;
return ((startIndex ? value.substr(0, startIndex) : '') +
encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
(endIndex < value.length ? value.substr(endIndex) : ''));
}
/**
* Joins parsed header value together as 'value; param1=value1; param2=value2'
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
* @param structured Parsed header value
* @return joined header value
*/
function buildHeaderValue(structured) {
const paramsArray = [];
Object.keys(structured.params || {}).forEach(key => {
// filename might include unicode characters so it is a special case
// other values probably do not
const value = structured.params[key];
// a parameter name is a token too and it is emitted without any quoting around it
const param = key.replace(/[\x00-\x1f\x7f]/g, '');
if (!isPlainText(value, true) || value.length >= 75) {
buildHeaderParam(param, value, 50).forEach(encodedParam => {
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
}
else {
paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value));
}
});
}
else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
paramsArray.push(param + '=' + JSON.stringify(value));
}
else {
paramsArray.push(param + '=' + value);
}
});
// the value ahead of the parameters is a token, it has no way to carry a control
// char or DEL and there is no quoting construct around it to escape one into
const value = typeof structured.value === 'string' ? structured.value.replace(/[\x00-\x1f\x7f]/g, '') : structured.value;
return value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
}
/**
* Encodes a string or an Buffer 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 data String to be encoded
* @param [maxLength=50] Max length for generated chunks
* @param [fromCharset='UTF-8'] Source sharacter set
* @return A list of encoded keys and headers
*/
function buildHeaderParam(key, data, maxLength) {
const list = [];
let encodedStr = typeof data === 'string' ? data : (data || '').toString();
let chr;
let line;
let startPos = 0;
let i, len;
maxLength = maxLength || 50;
// process ascii only text
if (isPlainText(data, true)) {
// check if conversion is even needed
if (encodedStr.length <= maxLength) {
return [
{
key,
value: encodedStr
}
];
}
encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => {
list.push({
line: str
});
return '';
});
if (encodedStr) {
list.push({
line: encodedStr
});
}
}
else {
if (/[\uD800-\uDBFF]/.test(encodedStr)) {
// string containts surrogate pairs, so normalize it to an array of bytes
const encodedStrArr = [];
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr.charAt(i);
if (/[\ud800-\udbff]/.test(chr) && /[\udc00-\udfff]/.test(encodedStr.charAt(i + 1))) {
// an unpaired leading surrogate must not consume the next unit, that
// would tear apart a valid pair following it
chr += encodedStr.charAt(i + 1);
encodedStrArr.push(chr);
i++;
}
else {
encodedStrArr.push(chr);
}
}
encodedStr = encodedStrArr;
}
// 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''";
let encoded = true;
startPos = 0;
// process text with unicode or special chars
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr[i];
if (encoded) {
chr = safeEncodeURIComponent(chr);
}
else {
// try to urlencode current char
chr = chr === ' ' ? chr : safeEncodeURIComponent(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 ((safeEncodeURIComponent(line) + chr).length >= maxLength) {
list.push({
line,
encoded
});
// the line we start here holds an encoded char, so it has to be
// flagged as one. otherwise it gets an unstarred continuation key
// and a receiver reads the percent escapes as literal text
line = '';
encoded = true;
}
else {
encoded = 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,
encoded
});
line = chr = encodedStr[i] === ' ' ? ' ' : safeEncodeURIComponent(encodedStr[i]);
if (chr === encodedStr[i]) {
encoded = false;
startPos = i - 1;
}
else {
encoded = true;
}
}
else {
line += chr;
}
}
if (line) {
list.push({
line,
encoded
});
}
}
return list.map((item, i) => ({
// 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: item.line
}));
}
/**
* 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 str Header value
* @return Header value as a parsed structure
*/
function parseHeaderValue(str) {
const response = {
value: false,
params: {}
};
// Parameter names come from a caller supplied contentType/contentDisposition. A
// "__proto__" name would target the prototype chain of the params object instead of
// an own property of it, and read back as Object.prototype, so it is dropped.
const setParam = (name, value) => {
if (!(0, objects_js_1.isProtoKey)(name)) {
response.params[name] = value;
}
};
let key = false;
let value = '';
let type = 'value';
let quote = false;
let escaped = false;
let chr;
for (let 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 {
setParam(key, value.trim());
}
type = 'key';
value = '';
}
else {
value += chr;
}
escaped = false;
}
}
if (type === 'value') {
if (key === false) {
response.value = value.trim();
}
else {
setParam(key, value.trim());
}
}
else if (value.trim()) {
setParam(value.trim().toLowerCase(), '');
}
// handle parameter value continuations
// https://tools.ietf.org/html/rfc2231#section-3
// preprocess values
Object.keys(response.params).forEach(key => {
let actualKey, nr, match, value;
if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) {
actualKey = key.substr(0, match.index);
nr = Number(match[2] || match[3]) || 0;
if ((0, objects_js_1.isProtoKey)(actualKey)) {
// see setParam. Reading it back would yield Object.prototype, which is
// an object, so the initializer below would be skipped and the write
// that follows would throw out of a header build the caller can not catch
delete response.params[key];
return;
}
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(key => {
let value;
if (response.params[key] && Array.isArray(response.params[key].values)) {
value = response.params[key].values.map((val) => 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, s => {
const c = s.charCodeAt(0).toString(16);
if (s === ' ') {
return '_';
}
return '%' + (c.length < 2 ? '0' : '') + c;
})
// change from urlencoding to percent encoding
.replace(/%/g, '=') +
'?=';
}
else {
response.params[key] = value;
}
}
});
return response;
}
/**
* Returns file extension for a content type string. If no suitable extensions
* are found, 'bin' is used as the default extension
*
* @param mimeType Content type to be checked for
* @return File extension
*/
function detectExtension(mimeType) {
return mimeTypes.detectExtension(mimeType);
}
/**
* Returns content type for a file extension. If no suitable content types
* are found, 'application/octet-stream' is used as the default content type
*
* @param extension Extension to be checked for
* @return File extension
*/
function detectMimeType(extension) {
return mimeTypes.detectMimeType(extension);
}
/**
* Folds long lines, useful for folding header lines (afterSpace=false) and
* flowed text (afterSpace=true)
*
* @param str String to be folded
* @param [lineLength=76] Maximum length of a line
* @param afterSpace If true, leave a space in th end of a line
* @return String with folded lines
*/
function foldLines(str, lineLength, afterSpace) {
str = (str || '').toString();
lineLength = lineLength || 76;
let pos = 0;
const len = str.length;
let result = '';
let line, match;
while (pos < len) {
line = str.substr(pos, lineLength);
if (line.length < lineLength) {
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;
}
/**
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
*
* @param str Mime encoded string to be split up
* @param maxlen Maximum length of characters for one part (minimum 12)
* @return Split string
*/
function splitMimeEncodedString(str, maxlen) {
const lines = [];
let curLine, fallbackLine, match, chr, done;
// 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);
}
// Malformed input (a run of stray UTF-8 continuation bytes) has no split point
// that keeps a character sequence whole, so the loop below walks back to an
// empty line looking for one. Keep the widest chunk that at least does not cut
// a "=XX" escape in half, so the part stays a decodable encoded word.
fallbackLine = curLine.length ? curLine : str.substr(0, maxlen);
done = false;
while (!done && curLine.length) {
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) {
curLine = fallbackLine;
}
lines.push(curLine);
str = str.substr(curLine.length);
}
return lines;
}
function encodeURICharComponent(chr) {
let res = '';
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
if (ord.length % 2) {
ord = '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;
}
function safeEncodeURIComponent(str) {
str = (str || '').toString();
try {
// might throw if we try to encode invalid sequences, eg. partial emoji
str = encodeURIComponent(str);
}
catch (_E) {
// an unpaired surrogate has no utf-8 representation, so run the value through a
// utf-8 roundtrip to get the same U+FFFD every other encoder here produces and
// retry. the value must never come back unencoded, it goes into a header parameter
// where a bare quote or semicolon would break it out into a parameter of its own
str = encodeURIComponent(Buffer.from(str, 'utf-8').toString('utf-8'));
}
// ensure chars that are not handled by encodeURICompent are converted as well
return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => encodeURICharComponent(chr));
}