UNPKG

haraka-email-message

Version:
254 lines (213 loc) 6.34 kB
'use strict' const libmime = require('libmime') const libqp = require('libqp') const logger = require('./logger') const { try_convert } = require('./encoding') /* eslint no-control-regex: 0 */ function _decode_header(matched, encoding, lang, cte, data) { cte = cte.toUpperCase() switch (cte) { case 'Q': data = libqp.decode(data.replace(/_/g, ' ')) break case 'B': data = Buffer.from(data, 'base64') break default: logger.logerror(`Invalid header encoding type: ${cte}`) } if (!/UTF-?8/i.test(encoding)) return try_convert(data, encoding) return data.toString() } function _parse_rfc2231(params, str) { while (true) { const sub_matches = /(([!#$%&'*+.0-9A-Zdiff^_`a-z{|}~-]*)\*)(\d*)=(\s*".*?[^\\]";?|\S*)/.exec( str, ) if (!sub_matches) return const key = sub_matches[1] let key_actual = sub_matches[2] let key_id = sub_matches[3] || '0' let value = sub_matches[4].replace(/;$/, '') str = str.replace(sub_matches[0], '') const key_extract = /^(.*?)(\*(\d+)\*)$/.exec(key) if (key_extract) { key_actual = key_extract[1] key_id = key_extract[3] } const quote = /^\s*"(.*)"$/.exec(value) if (quote) value = quote[1] const lang_match = /^(.*?)'(.*?)'(.*)/.exec(value) if (lang_match) { if (key_actual === params.cur_key && lang_match[2] !== params.cur_lang) { continue } params.cur_enc = lang_match[1] params.cur_lang = lang_match[2] value = lang_match[3] } else if (key_actual !== params.cur_key) { params.cur_lang = '' params.cur_enc = '' } params.cur_key = key_actual params.keys[key_actual] = '' params.kv[`${key_actual}*${key_id}`] = value } } function _decode_rfc2231(params, str) { _parse_rfc2231(params, str) for (const key in params.keys) { str += ` ${key}="` /* eslint no-constant-condition: 0 */ let merged = '' for (let i = 0; true; i++) { const _val = params.kv[`${key}*${i}`] if (_val === undefined) break merged += _val } try { merged = decodeURIComponent(merged) } catch { logger.logerror(`Decode header failed: ${key}: ${merged}`) } if (params.cur_enc) { merged = try_convert(Buffer.from(merged, 'utf8'), params.cur_enc) } str += `${merged}";` } return str } // An RFC 2822 email header parser class Header { #options constructor(options) { this.headers = Object.create(null) this.headers_decoded = Object.create(null) this.header_list = [] this.#options = options } parse(lines) { for (const line of lines) { if (/^[ \t]/.test(line)) { this.header_list[this.header_list.length - 1] += line } else { this.header_list.push(line) } } for (const header of this.header_list) { const match = header.match(/^([^\s:]*):\s*([\s\S]*)$/) if (match) { this._add_header(match[1].toLowerCase(), match[2], 'push') } else { logger.lognotice(`Header did not look right: ${header}`) } } for (const key of Object.keys(this.headers)) { for (const val of this.headers[key]) { this._add_header_decode(key, val, 'push') } } } decode_header(val) { val = val.replace(/\r?\n/g, '') const rfc2231_params = { kv: {}, keys: {}, cur_key: '', cur_enc: '', cur_lang: '', } val = _decode_rfc2231(rfc2231_params, val) if (!/^[\x00-\x7f]*$/.test(val)) { const matches = /\bcharset\s*=\s*["']?([\w_-]*)/.exec( this.get('content-type'), ) if (matches && !/UTF-?8/i.test(matches[1])) { val = try_convert(Buffer.from(val, 'binary'), matches[1]) } } if (!/=\?/.test(val)) return val return val .replace(/(=\?.+?\?=)\s+(?==\?.+?\?=)/g, '$1') .replace( /=\?([\w_-]+)(\*[\w_-]+)?\?([bqBQ])\?([\s\S]*?)\?=/g, _decode_header, ) } get(key) { return (this.headers[key.toLowerCase()] ?? []).join('\n') } get_all(key) { return Object.freeze([...(this.headers[key.toLowerCase()] ?? [])]) } get_decoded(key) { return (this.headers_decoded[key.toLowerCase()] ?? []).join('\n') } remove(key) { key = key.toLowerCase() delete this.headers[key] delete this.headers_decoded[key] this._remove_more(key) } _remove_more(key) { const key_len = key.length for (let i = 0, l = this.header_list.length; i < l; i++) { if ( this.header_list[i].substring(0, key_len + 1).toLowerCase() === `${key}:` ) { this.header_list.splice(i, 1) return this._remove_more(key) } } } add(key, value) { if (!key) key = 'X-Haraka-Blank' value = trimNewlines(value) if (/[^\x00-\x7f]/.test(value)) value = libmime.encodeWords(value, 'Q') this._add_header(key.toLowerCase(), value, 'unshift') this._add_header_decode(key.toLowerCase(), value, 'unshift') this.header_list.unshift(`${key}: ${value}\n`) } add_end(key, value) { if (!key) key = 'X-Haraka-Blank' value = trimNewlines(value) if (/[^\x00-\x7f]/.test(value)) value = libmime.encodeWords(value, 'Q') this._add_header(key.toLowerCase(), value, 'push') this._add_header_decode(key.toLowerCase(), value, 'push') this.header_list.push(`${key}: ${value}\n`) } _add_header(key, value, method) { if (['__proto__', 'constructor', 'prototype'].includes(key)) return this.headers[key] ??= [] this.headers[key][method](value) } _add_header_decode(key, value, method) { if (['__proto__', 'constructor', 'prototype'].includes(key)) return this.headers_decoded[key] ??= [] this.headers_decoded[key][method](this.decode_header(value)) } lines() { return Object.freeze([...this.header_list]) } toString() { return this.header_list.join('\n') } } function trimNewlines(value) { // 4B ops/sec vs regex at 1.4M ops/sec while (value.length > 0) { if (value.endsWith('\n')) { if (value.endsWith('\r\n')) { value = value.slice(0, -2) } else { value = value.slice(0, -1) } } else { break // Stop at a stray \r or any other character } } return value } module.exports = Header