node-imap
Version:
A fork of the famous and outdated IMAP module for node.js that makes communicating with IMAP servers easy
1,036 lines (975 loc) • 30.8 kB
JavaScript
'use strict'
const EventEmitter = require('events').EventEmitter,
ReadableStream = require('stream').Readable
|| require('readable-stream').Readable,
// inherits = require('util').inherits,
inspect = require('util').inspect
const utf7 = require('utf7').imap
let jsencoding // lazy-loaded
const CH_LF = 10,
LITPLACEHOLDER = String.fromCharCode(0),
EMPTY_READCB = function (n) { },
RE_INTEGER = /^\d+$/,
RE_PRECEDING = /^(?:\* |A\d+ |^\+ ?)/, // Before: /^(?:\* |A\d+ |\+ ?)/,
RE_BODYLITERAL = /BODY\[(.*)\] \{(\d+)\}$/i,
RE_BODYINLINEKEY = /^BODY\[(.*)\]$/i,
RE_SEQNO = /^\* (\d+)/,
RE_LISTCONTENT = /^\((.*)\)$/,
RE_LITERAL = /\{(\d+)\}$/,
RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|ID|LIST|XLIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT|THREAD|ESEARCH|QUOTA|QUOTAROOT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?:(?: \[([^\]]+)\])?(?: (.+))?)?$/i,
RE_TAGGED = /^A(\d+) (OK|NO|BAD) ?(?:\[([^\]]+)\] )?(.*)$/i,
RE_CONTINUE = /^\+(?: (?:\[([^\]]+)\] )?(.+))?$/i,
RE_CRLF = /\r\n/g,
RE_HDR = /^([^:]+):[ \t]?(.+)?$/,
RE_ENCWORD = /=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=/gi,
RE_ENCWORD_END = /=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=$/i,
RE_ENCWORD_BEGIN = /^[ \t]=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=/i,
RE_QENC = /(?:=([a-fA-F0-9]{2}))|_/g,
RE_SEARCH_MODSEQ = /^(.+) \(MODSEQ (.+?)\)$/i,
RE_LWS_ONLY = /^[ \t]*$/
class Parser extends EventEmitter {
constructor(stream, debug) {
super()
if (!(this instanceof Parser))
return new Parser(stream, debug)
// EventEmitter.call(this);
this._stream = undefined
this._body = undefined
this._literallen = 0
this._literals = []
this._buffer = ''
this._ignoreReadable = false
this.debug = debug
const self = this
this._cbReadable = function () {
if (self._ignoreReadable)
return
if (self._literallen > 0 && !self._body)
self._tryread(self._literallen)
else
self._tryread()
}
this.setStream(stream)
process.nextTick(this._cbReadable)
}
setStream(stream) {
if (this._stream)
this._stream.removeListener('readable', this._cbReadable)
if (/^v0\.8\./.test(process.version)) {
this._stream = (new ReadableStream()).wrap(stream)
// since Readable.wrap() proxies events, we need to remove at least the
// proxied 'error' event since this can cause problems and Parser doesn't
// care about such events
stream._events.error.pop()
}
else
this._stream = stream
this._stream.on('readable', this._cbReadable)
}
_tryread(n) {
if (this._stream.readable) {
const r = this._stream.read(n)
r && this._parse(r)
}
}
_parse(data) {
let i = 0
let idxlf
const datalen = data.length
if (this._literallen > 0) {
if (this._body) {
const body = this._body
if (datalen >= this._literallen) {
const litlen = this._literallen
i = litlen
this._literallen = 0
this._body = undefined
body._read = EMPTY_READCB
if (datalen > litlen)
body.push(data.slice(0, litlen))
else
body.push(data)
body.push(null)
}
else {
this._literallen -= datalen
const r = body.push(data)
if (!r) {
body._read = this._cbReadable
return
}
i = datalen
}
}
else {
if (datalen > this._literallen)
this._literals.push(data.slice(0, this._literallen))
else
this._literals.push(data)
i = this._literallen
this._literallen = 0
}
}
while (i < datalen) {
idxlf = indexOfCh(data, datalen, i, CH_LF)
if (idxlf === -1) {
this._buffer += data.toString('utf8', i)
break
}
else {
this._buffer += data.toString('utf8', i, idxlf)
this._buffer = this._buffer.trim()
i = idxlf + 1
this.debug && this.debug('<= ' + inspect(this._buffer))
if (RE_PRECEDING.test(this._buffer)) {
const firstChar = this._buffer[0]
if (firstChar === '*')
this._resUntagged()
else if (firstChar === 'A')
this._resTagged()
else if (firstChar === '+')
this._resContinue()
if (this._literallen > 0 && i < datalen) {
this._ignoreReadable = true
// literal data included in this chunk -- put it back onto stream
this._stream.unshift(data.slice(i))
this._ignoreReadable = false
i = datalen
if (!this._body) {
// check if unshifted contents satisfies non-body literal length
this._tryread(this._literallen)
}
}
}
else {
this.emit('other', this._buffer)
this._buffer = ''
}
}
}
if (this._literallen === 0 || this._body)
this._tryread()
}
_resTagged() {
let m
if (m = RE_LITERAL.exec(this._buffer)) {
// non-BODY literal -- buffer it
this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER)
this._literallen = parseInt(m[1], 10)
}
else if (m = RE_TAGGED.exec(this._buffer)) {
this._buffer = ''
this._literals = []
this.emit('tagged', {
type: m[2].toLowerCase(),
tagnum: parseInt(m[1], 10),
textCode: (m[3] ? parseTextCode(m[3], this._literals) : m[3]),
text: m[4]
})
}
else
this._buffer = ''
}
_resUntagged() {
// console.log('_resUntagged')
let m
if (m = RE_BODYLITERAL.exec(this._buffer)) {
// console.log('RE_BODYLITERAL', this._buffer);
// BODY literal -- stream it
const which = m[1], size = parseInt(m[2], 10)
this._literallen = size
this._body = new ReadableStream()
this._body._readableState.sync = false
this._body._read = EMPTY_READCB
m = RE_SEQNO.exec(this._buffer)
this._buffer = this._buffer.replace(RE_BODYLITERAL, '')
this.emit('body', this._body, {
seqno: parseInt(m[1], 10),
which: which,
size: size
})
}
else if (m = RE_LITERAL.exec(this._buffer)) {
// console.log('RE_LITERAL', this._buffer);
// non-BODY literal -- buffer it
this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER)
this._literallen = parseInt(m[1], 10)
}
else if (m = RE_UNTAGGED.exec(this._buffer)) {
// console.log('RE_UNTAGGED', this._buffer);
this._buffer = ''
// normal single line response
// m[1] or m[3] = response type
// if m[3] is set, m[2] = sequence number (for FETCH) or count
// m[4] = response text code (optional)
// m[5] = response text (optional)
let type, num, textCode, val
if (m[2] !== undefined)
num = parseInt(m[2], 10)
if (m[4] !== undefined)
textCode = parseTextCode(m[4], this._literals)
type = (m[1] || m[3]).toLowerCase()
// console.log('Untagged Type: ', type);
if (type === 'flags'
|| type === 'search'
|| type === 'capability'
|| type === 'sort') {
if (m[5]) {
if (type === 'search' && RE_SEARCH_MODSEQ.test(m[5])) {
// CONDSTORE search response
const p = RE_SEARCH_MODSEQ.exec(m[5])
val = {
results: p[1].split(' '),
modseq: p[2]
}
}
else {
if (m[5][0] === '(')
val = RE_LISTCONTENT.exec(m[5])[1].split(' ')
else
val = m[5].split(' ')
if (type === 'search' || type === 'sort')
val = val.map(function (v) { return parseInt(v, 10) })
}
}
else
val = []
}
else if (type === 'thread') {
if (m[5])
val = parseExpr(m[5], this._literals)
else
val = []
}
else if (type === 'list' || type === 'lsub' || type === 'xlist')
val = parseBoxList(m[5], this._literals)
else if (type === 'id')
val = parseId(m[5], this._literals)
else if (type === 'status')
val = parseStatus(m[5], this._literals)
else if (type === 'fetch')
val = parseFetch.call(this, m[5], this._literals, num)
else if (type === 'namespace')
val = parseNamespaces(m[5], this._literals)
else if (type === 'esearch')
val = parseESearch(m[5], this._literals)
else if (type === 'quota')
val = parseQuota(m[5], this._literals)
else if (type === 'quotaroot')
val = parseQuotaRoot(m[5], this._literals)
else if (type === 'no') {
val = m[5]
} else
val = m[5]
this._literals = []
this.emit('untagged', {
type: type,
num: num,
textCode: textCode,
text: val
})
}
else
this._buffer = ''
}
_resContinue() {
const m = RE_CONTINUE.exec(this._buffer)
let textCode, text
this._buffer = ''
if (!m)
return
text = m[2]
if (m[1] !== undefined)
textCode = parseTextCode(m[1], this._literals)
this.emit('continue', {
textCode: textCode,
text: text
})
}
}
// inherits(Parser, EventEmitter);
function indexOfCh(buffer, len, i, ch) {
let r = -1
for (; i < len; ++i) {
if (buffer[i] === ch) {
r = i
break
}
}
return r
}
function parseTextCode(text, literals) {
const r = parseExpr(text, literals)
if (r.length === 1)
return r[0]
else
return { key: r[0], val: r.length === 2 ? r[1] : r.slice(1) }
}
function parseESearch(text, literals) {
const r = parseExpr(text.toUpperCase().replace('UID', ''), literals),
attrs = {}
// RFC4731 unfortunately is lacking on documentation, so we're going to
// assume that the response text always begins with (TAG "A123") and skip that
// part ...
for (let i = 1, len = r.length, key, val; i < len; i += 2) {
key = r[i].toLowerCase()
val = r[i + 1]
if (key === 'all')
val = val.toString().split(',')
attrs[key] = val
}
return attrs
}
function parseId(text, literals) {
const r = parseExpr(text, literals),
id = {}
if (r[0] === null)
return null
for (let i = 0, len = r[0].length; i < len; i += 2)
id[r[0][i].toLowerCase()] = r[0][i + 1]
return id
}
function parseQuota(text, literals) {
const r = parseExpr(text, literals),
resources = {}
for (let i = 0, len = r[1].length; i < len; i += 3) {
resources[r[1][i].toLowerCase()] = {
usage: r[1][i + 1],
limit: r[1][i + 2]
}
}
return {
root: r[0],
resources: resources
}
}
function parseQuotaRoot(text, literals) {
const r = parseExpr(text, literals)
return {
roots: r.slice(1),
mailbox: r[0]
}
}
function parseBoxList(text, literals) {
const r = parseExpr(text, literals)
return {
flags: r[0],
delimiter: r[1],
name: utf7.decode('' + r[2])
}
}
function parseNamespaces(text, literals) {
const r = parseExpr(text, literals)
let i, len, j, len2, ns, nsobj, namespaces, n
for (n = 0; n < 3; ++n) {
if (r[n]) {
namespaces = []
for (i = 0, len = r[n].length; i < len; ++i) {
ns = r[n][i]
nsobj = {
prefix: ns[0],
delimiter: ns[1],
extensions: undefined
}
if (ns.length > 2)
nsobj.extensions = {}
for (j = 2, len2 = ns.length; j < len2; j += 2)
nsobj.extensions[ns[j]] = ns[j + 1]
namespaces.push(nsobj)
}
r[n] = namespaces
}
}
return {
personal: r[0],
other: r[1],
shared: r[2]
}
}
function parseStatus(text, literals) {
const r = parseExpr(text, literals), attrs = {}
// r[1] is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn]
for (let i = 0, len = r[1].length; i < len; i += 2)
attrs[r[1][i].toLowerCase()] = r[1][i + 1]
return {
name: utf7.decode('' + r[0]),
attrs: attrs
}
}
function parseFetch(text, literals, seqno) {
const list = parseExpr(text, literals)[0], attrs = {}
let m, body
// list is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn]
for (let i = 0, len = list.length, key, val; i < len; i += 2) {
key = list[i].toLowerCase()
val = list[i + 1]
if (key === 'envelope')
val = parseFetchEnvelope(val)
else if (key === 'internaldate')
val = new Date(val)
else if (key === 'modseq') // always a list of one value
val = '' + val[0]
else if (key === 'body' || key === 'bodystructure')
val = parseBodyStructure(val)
else if (m = RE_BODYINLINEKEY.exec(list[i])) {
// a body was sent as a non-literal
val = Buffer.from(val) //new Buffer(''+val);
body = new ReadableStream()
body._readableState.sync = false
body._read = EMPTY_READCB
this.emit('body', body, {
seqno: seqno,
which: m[1],
size: val.length
})
body.push(val)
body.push(null)
continue
}
attrs[key] = val
}
return attrs
}
function parseBodyStructure(cur, literals, prefix, partID) {
let ret = []
let i, len
if (prefix === undefined) {
const result = (Array.isArray(cur) ? cur : parseExpr(cur, literals))
if (result.length)
ret = parseBodyStructure(result, literals, '', 1)
} else {
let part, partLen = cur.length, next
if (Array.isArray(cur[0])) { // multipart
next = -1
while (Array.isArray(cur[++next])) {
ret.push(parseBodyStructure(cur[next],
literals,
prefix + (prefix !== '' ? '.' : '')
+ (partID++).toString(), 1))
}
part = { type: cur[next++].toLowerCase() }
if (partLen > next) {
if (Array.isArray(cur[next])) {
part.params = {}
for (i = 0, len = cur[next].length; i < len; i += 2)
part.params[cur[next][i].toLowerCase()] = decodeWords(cur[next][i + 1])
} else
part.params = cur[next]
++next
}
} else { // single part
next = 7
if (typeof cur[1] === 'string') {
part = {
// the path identifier for this part, useful for fetching specific
// parts of a message
partID: (prefix !== '' ? prefix : '1'),
// required fields as per RFC 3501 -- null or otherwise
type: cur[0].toLowerCase(), subtype: cur[1].toLowerCase(),
params: null, id: cur[3], description: cur[4], encoding: cur[5],
size: cur[6]
}
} else {
// type information for malformed multipart body
part = { type: cur[0] ? cur[0].toLowerCase() : null, params: null }
cur.splice(1, 0, null)
++partLen
next = 2
}
if (Array.isArray(cur[2])) {
part.params = {}
for (i = 0, len = cur[2].length; i < len; i += 2)
part.params[cur[2][i].toLowerCase()] = decodeWords(cur[2][i + 1])
if (cur[1] === null)
++next
}
if (part.type === 'message' && part.subtype === 'rfc822') {
// envelope
if (partLen > next && Array.isArray(cur[next]))
part.envelope = parseFetchEnvelope(cur[next])
else
part.envelope = null
++next
// body
if (partLen > next && Array.isArray(cur[next]))
part.body = parseBodyStructure(cur[next], literals, prefix, 1)
else
part.body = null
++next
}
if ((part.type === 'text'
|| (part.type === 'message' && part.subtype === 'rfc822'))
&& partLen > next)
part.lines = cur[next++]
if (typeof cur[1] === 'string' && partLen > next)
part.md5 = cur[next++]
}
// add any extra fields that may or may not be omitted entirely
parseStructExtra(part, partLen, cur, next)
ret.unshift(part)
}
return ret
}
function parseStructExtra(part, partLen, cur, next) {
if (partLen > next) {
// disposition
// null or a special k/v list with these kinds of values:
// e.g.: ['Foo', null]
// ['Foo', ['Bar', 'Baz']]
// ['Foo', ['Bar', 'Baz', 'Bam', 'Pow']]
const disposition = { type: null, params: null }
if (Array.isArray(cur[next])) {
disposition.type = cur[next][0]
if (Array.isArray(cur[next][1])) {
disposition.params = {}
for (let i = 0, len = cur[next][1].length, key; i < len; i += 2) {
key = cur[next][1][i].toLowerCase()
disposition.params[key] = decodeWords(cur[next][1][i + 1])
}
}
} else if (cur[next] !== null)
disposition.type = cur[next]
if (disposition.type === null)
part.disposition = null
else
part.disposition = disposition
++next
}
if (partLen > next) {
// language can be a string or a list of one or more strings, so let's
// make this more consistent ...
if (cur[next] !== null)
part.language = (Array.isArray(cur[next]) ? cur[next] : [cur[next]])
else
part.language = null
++next
}
if (partLen > next)
part.location = cur[next++]
if (partLen > next) {
// extension stuff introduced by later RFCs
// this can really be any value: a string, number, or (un)nested list
// let's not parse it for now ...
part.extensions = cur[next]
}
}
function parseFetchEnvelope(list) {
return {
date: new Date(list[0]),
subject: decodeWords(list[1]),
from: parseEnvelopeAddresses(list[2]),
sender: parseEnvelopeAddresses(list[3]),
replyTo: parseEnvelopeAddresses(list[4]),
to: parseEnvelopeAddresses(list[5]),
cc: parseEnvelopeAddresses(list[6]),
bcc: parseEnvelopeAddresses(list[7]),
inReplyTo: list[8],
messageId: list[9]
}
}
function parseEnvelopeAddresses(list) {
let addresses = null
if (Array.isArray(list)) {
addresses = []
let inGroup = false
let curGroup
for (let i = 0, len = list.length, addr; i < len; ++i) {
addr = list[i]
if (addr[2] === null) { // end of group addresses
inGroup = false
if (curGroup) {
addresses.push(curGroup)
curGroup = undefined
}
} else if (addr[3] === null) { // start of group addresses
inGroup = true
curGroup = {
group: addr[2],
addresses: []
}
} else { // regular user address
const info = {
name: decodeWords(addr[0]),
mailbox: addr[2],
host: addr[3]
}
if (inGroup)
curGroup.addresses.push(info)
else if (!inGroup)
addresses.push(info)
}
list[i] = addr
}
if (inGroup) {
// no end of group found, assume implicit end
addresses.push(curGroup)
}
}
return addresses
}
function parseExpr(o, literals, result, start, useBrackets) {
start = start || 0
let inQuote = false,
lastPos = start - 1,
isTop = false,
isBody = false,
escaping = false,
val
if (useBrackets === undefined)
useBrackets = true
if (!result)
result = []
if (typeof o === 'string') {
o = { str: o }
isTop = true
}
for (let i = start, len = o.str.length; i < len; ++i) {
if (!inQuote) {
if (isBody) {
if (o.str[i] === ']') {
val = convStr(o.str.substring(lastPos + 1, i + 1), literals)
result.push(val)
lastPos = i
isBody = false
}
} else if (o.str[i] === '"')
inQuote = true
else if (o.str[i] === ' '
|| o.str[i] === ')'
|| (useBrackets && o.str[i] === ']')) {
if (i - (lastPos + 1) > 0) {
val = convStr(o.str.substring(lastPos + 1, i), literals)
result.push(val)
}
if ((o.str[i] === ')' || (useBrackets && o.str[i] === ']')) && !isTop)
return i
lastPos = i
} else if ((o.str[i] === '(' || (useBrackets && o.str[i] === '['))) {
if (o.str[i] === '['
&& i - 4 >= start
&& o.str.substring(i - 4, i).toUpperCase() === 'BODY') {
isBody = true
lastPos = i - 5
} else {
const innerResult = []
i = parseExpr(o, literals, innerResult, i + 1, useBrackets)
lastPos = i
result.push(innerResult)
}
}
} else if (o.str[i] === '\\')
escaping = !escaping
else if (o.str[i] === '"') {
if (!escaping)
inQuote = false
escaping = false
}
if (i + 1 === len && len - (lastPos + 1) > 0)
result.push(convStr(o.str.substring(lastPos + 1), literals))
}
return (isTop ? result : start)
}
function convStr(str, literals) {
if (str[0] === '"') {
str = str.substring(1, str.length - 1)
let newstr = '', isEscaping = false, p = 0
for (let i = 0, len = str.length; i < len; ++i) {
if (str[i] === '\\') {
if (!isEscaping)
isEscaping = true
else {
isEscaping = false
newstr += str.substring(p, i - 1)
p = i
}
} else if (str[i] === '"') {
if (isEscaping) {
isEscaping = false
newstr += str.substring(p, i - 1)
p = i
}
}
}
if (p === 0)
return str
else {
newstr += str.substring(p)
return newstr
}
} else if (str === 'NIL')
return null
else if (RE_INTEGER.test(str)) {
// some IMAP extensions utilize large (64-bit) integers, which JavaScript
// can't handle natively, so we'll just keep it as a string if it's too big
const val = parseInt(str, 10)
return (val.toString() === str ? val : str)
} else if (literals && literals.length && str === LITPLACEHOLDER) {
let l = literals.shift()
if (Buffer.isBuffer(l))
l = l.toString('utf8')
return l
}
return str
}
function repeat(chr, len) {
let s = ''
for (let i = 0; i < len; ++i)
s += chr
return s
}
function decodeBytes(buf, encoding, offset, mlen, pendoffset, state, nextBuf) {
if (!jsencoding)
jsencoding = require('../deps/encoding/encoding')
if (jsencoding.encodingExists(encoding)) {
if (state.buffer !== undefined) {
if (state.encoding === encoding && state.consecutive) {
// concatenate buffer + current bytes in hopes of finally having
// something that's decodable
const newbuf = Buffer.alloc(state.buffer.length + buf.length) //new Buffer(state.buffer.length + buf.length);
state.buffer.copy(newbuf, 0)
buf.copy(newbuf, state.buffer.length)
buf = newbuf
} else {
// either:
// - the current encoded word is not separated by the previous partial
// encoded word by linear whitespace, OR
// - the current encoded word and the previous partial encoded word
// use different encodings
state.buffer = state.encoding = undefined
state.curReplace = undefined
}
}
let ret, isPartial = false
if (state.remainder !== undefined) {
// use cached remainder from the previous lookahead
ret = state.remainder
state.remainder = undefined
} else {
try {
ret = jsencoding.TextDecoder(encoding).decode(buf)
} catch (e) {
if (e.message.indexOf('Seeking') === 0)
isPartial = true
}
}
if (!isPartial && nextBuf) {
// try to decode a lookahead buffer (current buffer + next buffer)
// and see if it starts with the decoded value of the current buffer.
// if not, the current buffer is partial
let lookahead, lookaheadBuf = Buffer.alloc(buf.length + nextBuf.length) //new Buffer(buf.length + nextBuf.length);
buf.copy(lookaheadBuf)
nextBuf.copy(lookaheadBuf, buf.length)
try {
lookahead = jsencoding.TextDecoder(encoding).decode(lookaheadBuf)
} catch (e) {
// cannot decode the lookahead, do nothing
}
if (lookahead !== undefined) {
if (lookahead.indexOf(ret) === 0) {
// the current buffer is whole, cache the lookahead's remainder
state.remainder = lookahead.substring(ret.length)
} else {
isPartial = true
ret = undefined
}
}
}
if (ret !== undefined) {
if (state.curReplace) {
// we have some previous partials which were finally "satisfied" by the
// current encoded word, so replace from the beginning of the first
// partial to the end of the current encoded word
state.replaces.push({
fromOffset: state.curReplace[0].fromOffset,
toOffset: offset + mlen,
val: ret
})
state.replaces.splice(state.replaces.indexOf(state.curReplace), 1)
state.curReplace = undefined
} else {
// normal case where there are no previous partials and we successfully
// decoded a single encoded word
state.replaces.push({
// we ignore linear whitespace between consecutive encoded words
fromOffset: state.consecutive ? pendoffset : offset,
toOffset: offset + mlen,
val: ret
})
}
state.buffer = state.encoding = undefined
return
} else if (isPartial) {
// RFC2047 says that each decoded encoded word "MUST represent an integral
// number of characters. A multi-octet character may not be split across
// adjacent encoded-words." However, some MUAs appear to go against this,
// so we join broken encoded words separated by linear white space until
// we can successfully decode or we see a change in encoding
state.encoding = encoding
state.buffer = buf
if (!state.curReplace)
state.replaces.push(state.curReplace = [])
state.curReplace.push({
fromOffset: offset,
toOffset: offset + mlen,
// the value we replace this encoded word with if it doesn't end up
// becoming part of a successful decode
val: repeat('\uFFFD', buf.length)
})
return
}
}
// in case of unexpected error or unsupported encoding, just substitute the
// raw bytes
state.replaces.push({
fromOffset: offset,
toOffset: offset + mlen,
val: buf.toString('binary')
})
}
function qEncReplacer(match, byte) {
if (match === '_')
return ' '
else
return String.fromCharCode(parseInt(byte, 16))
}
function decodeWords(str, state) {
let pendoffset = -1
if (!state) {
state = {
buffer: undefined,
encoding: undefined,
consecutive: false,
replaces: undefined,
curReplace: undefined,
remainder: undefined
}
}
state.replaces = []
let bytes, m, next, i, j, leni, lenj, seq
let replaces = [], lastReplace = {}
// join consecutive q-encoded words that have the same charset first
while (m = RE_ENCWORD.exec(str)) {
seq = {
consecutive: (pendoffset > -1
? RE_LWS_ONLY.test(str.substring(pendoffset, m.index))
: false),
charset: m[1].toLowerCase(),
encoding: m[2].toLowerCase(),
chunk: m[3],
index: m.index,
length: m[0].length,
pendoffset: pendoffset,
buf: undefined
}
lastReplace = replaces.length && replaces[replaces.length - 1]
if (seq.consecutive
&& seq.charset === lastReplace.charset
&& seq.encoding === lastReplace.encoding
&& seq.encoding === 'q') {
lastReplace.length += seq.length + seq.index - pendoffset
lastReplace.chunk += seq.chunk
} else {
replaces.push(seq)
lastReplace = seq
}
pendoffset = m.index + m[0].length
}
// generate replacement substrings and their positions
for (i = 0, leni = replaces.length; i < leni; ++i) {
m = replaces[i]
state.consecutive = m.consecutive
if (m.encoding === 'q') {
// q-encoding, similar to quoted-printable
bytes = Buffer.from(m.chunk.replace(RE_QENC, qEncReplacer), 'binary') //new Buffer(m.chunk.replace(RE_QENC, qEncReplacer), 'binary');
next = undefined
} else {
// base64
bytes = m.buf || Buffer.from(m.chunk, 'base64') //new Buffer(m.chunk, 'base64');
next = replaces[i + 1]
if (next && next.consecutive && next.encoding === m.encoding
&& next.charset === m.charset) {
// we use the next base64 chunk, if any, to determine the integrity
// of the current chunk
next.buf = Buffer.from(next.chunk, 'base64') //new Buffer(next.chunk, 'base64');
}
}
decodeBytes(bytes, m.charset, m.index, m.length, m.pendoffset, state,
next && next.buf)
}
// perform the actual replacements
for (i = state.replaces.length - 1; i >= 0; --i) {
seq = state.replaces[i]
if (Array.isArray(seq)) {
for (j = 0, lenj = seq.length; j < lenj; ++j) {
str = str.substring(0, seq[j].fromOffset)
+ seq[j].val
+ str.substring(seq[j].toOffset)
}
} else {
str = str.substring(0, seq.fromOffset)
+ seq.val
+ str.substring(seq.toOffset)
}
}
return str
}
function parseHeader(str, noDecode) {
const lines = str.split(RE_CRLF),
header = {},
state = {
buffer: undefined,
encoding: undefined,
consecutive: false,
replaces: undefined,
curReplace: undefined,
remainder: undefined
}
let len = lines.length
let m, h, i, val
for (i = 0; i < len; ++i) {
if (lines[i].length === 0)
break // empty line separates message's header and body
if (lines[i][0] === '\t' || lines[i][0] === ' ') {
if (!Array.isArray(header[h]))
continue // ignore invalid first line
// folded header content
val = lines[i]
if (!noDecode) {
if (RE_ENCWORD_END.test(lines[i - 1])
&& RE_ENCWORD_BEGIN.test(val)) {
// RFC2047 says to *ignore* leading whitespace in folded header values
// for adjacent encoded-words ...
val = val.substring(1)
}
}
header[h][header[h].length - 1] += val
} else {
m = RE_HDR.exec(lines[i])
if (m) {
h = m[1].toLowerCase().trim()
if (m[2]) {
if (header[h] === undefined)
header[h] = [m[2]]
else
header[h].push(m[2])
} else
header[h] = ['']
} else
break
}
}
if (!noDecode) {
let hvs
for (h in header) {
hvs = header[h]
for (i = 0, len = header[h].length; i < len; ++i)
hvs[i] = decodeWords(hvs[i], state)
}
}
return header
}
exports.Parser = Parser
exports.parseExpr = parseExpr
exports.parseEnvelopeAddresses = parseEnvelopeAddresses
exports.parseBodyStructure = parseBodyStructure
exports.parseHeader = parseHeader