From ab6403735cc9ffd4d1fadfe41af32301af26ad07 Mon Sep 17 00:00:00 2001 From: Brian White Date: Wed, 13 Apr 2011 00:28:24 -0400 Subject: [PATCH] Simplify and fix parsing of message structures and mailbox namespaces --- README.md | 34 ++-- imap.js | 529 ++++++++++++++++++++++-------------------------------- 2 files changed, 225 insertions(+), 338 deletions(-) diff --git a/README.md b/README.md index 1919ba5..cef876a 100644 --- a/README.md +++ b/README.md @@ -104,13 +104,11 @@ A message structure with multiple parts might look something like the following: , params: { boundary: '000e0cd294e80dc83c0475bf339b' } , disposition: null , language: null - , location: null } , [ { partID: '1.1' - , type: - { name: 'text/plain' - , params: { charset: 'ISO-8859-1' } - } + , type: 'text' + , subtype: 'plain' + , params: { charset: 'ISO-8859-1' } , id: null , description: null , encoding: '7BIT' @@ -119,14 +117,12 @@ A message structure with multiple parts might look something like the following: , md5: null , disposition: null , language: null - , location: null } ] , [ { partID: '1.2' - , type: - { name: 'text/html' - , params: { charset: 'ISO-8859-1' } - } + , type: 'text' + , subtype: 'html' + , params: { charset: 'ISO-8859-1' } , id: null , description: null , encoding: 'QUOTED-PRINTABLE' @@ -135,15 +131,13 @@ A message structure with multiple parts might look something like the following: , md5: null , disposition: null , language: null - , location: null } ] ] , [ { partID: '2' - , type: - { name: 'application/octet-stream' - , params: { name: 'somefile' } - } + , type: 'application' + , subtype: 'octet-stream' + , params: { name: 'somefile' } , id: null , description: null , encoding: 'BASE64' @@ -165,10 +159,9 @@ Each message part is identified by a partID which is used when you want to fetch The structure of a message with only one part will simply look something like this: [ { partID: '1' - , type: - { name: 'text/plain' - , params: { charset: 'ISO-8859-1' } - } + , type: 'text' + , subtype: 'plain' + , params: { charset: 'ISO-8859-1' } , id: null , description: null , encoding: '7BIT' @@ -177,7 +170,6 @@ The structure of a message with only one part will simply look something like th , md5: null , disposition: null , language: null - , location: null } ] Therefore, an easy way to check for a multipart message is to check if the structure length is >1. @@ -216,7 +208,7 @@ ImapConnection Properties * **namespaces** - An Object containing 3 properties, one for each namespace type: personal (mailboxes that belong to the logged in user), other (mailboxes that belong to other users that the logged in user has access to), and shared (mailboxes that are accessible by any logged in user). The value of each of these properties is an Array of namespace Objects containing necessary information about each available namespace. There should always be one entry (although the IMAP spec allows for more, it doesn't seem to be very common) in the personal namespace list (if the server supports namespaces) with a blank namespace prefix. Each namespace Object has the following format (with example values): { prefix: '' // A String containing the prefix to use to access mailboxes in this namespace - , delimiter: '/' // A String containing the hierarchy delimiter for this namespace, or Boolean false for a flat namespace with no hierarchy + , delim: '/' // A String containing the hierarchy delimiter for this namespace, or Boolean false for a flat namespace with no hierarchy , extensions: [ // An Array of namespace extensions supported by this namespace, or null if none are specified { name: 'X-FOO-BAR' // A String indicating the extension name , params: [ 'BAZ' ] // An Array of Strings containing the parameters for this extension, or null if none are specified diff --git a/imap.js b/imap.js index c99a335..d1309e4 100644 --- a/imap.js +++ b/imap.js @@ -906,79 +906,31 @@ function validateUIDList(uids) { } function parseNamespaces(str, namespaces) { - // str contains 3 parenthesized lists (or NIL) describing the personal, other users', and shared namespaces available - var idxNext, idxNextName, idxNextVal, strNamespace, strList, details, types = Object.keys(namespaces), curType = 0; - while (str.length > 0) { - if (str.substr(0, 3) === 'NIL') - idxNext = 3; - else { - idxNext = getNextIdxParen(str)+1; - - // examples: (...) - // (...)(...) - strList = str.substring(1, idxNext-1); - - // parse each namespace for the current type - while (strList.length > 0) { - details = {}; - idxNextName = getNextIdxParen(strList)+1; - - // examples: "prefix" "delimiter" - // "prefix" NIL - // "prefix" NIL "X-SOME-EXT" ("FOO" "BAR" "BAZ") - strNamespace = strList.substring(1, idxNextName-1); - - // prefix - idxNextVal = getNextIdxQuoted(strNamespace)+1; - details.prefix = strNamespace.substring(1, idxNextVal-1); - strNamespace = strNamespace.substr(idxNextVal).trim(); - - // delimiter - if (strNamespace.substr(0, 3) === 'NIL') { - details.delim = false; - strNamespace = strNamespace.substr(3).trim(); - } else { - idxNextVal = getNextIdxQuoted(strNamespace)+1; - details.delim = strNamespace.substring(1, idxNextVal-1); - strNamespace = strNamespace.substr(idxNextVal).trim(); - } - - // [extensions] - if (strNamespace.length > 0) { - details.extensions = []; - var extension; - while (strNamespace.length > 0) { - extension = { name: '', params: null }; - - // name - idxNextVal = getNextIdxQuoted(strNamespace)+1; - extension.name = strNamespace.substring(1, idxNextVal-1); - strNamespace = strNamespace.substr(idxNextVal).trim(); - - // params - idxNextVal = getNextIdxParen(strNamespace)+1; - var strParams = strNamespace.substring(1, idxNextVal-1), idxNextParam; - if (strParams.length > 0) { - extension.params = []; - while (strParams.length > 0) { - idxNextParam = getNextIdxQuoted(strParams)+1; - extension.params.push(strParams.substring(1, idxNextParam-1)); - strParams = strParams.substr(idxNextParam).trim(); - } - } - strNamespace = strNamespace.substr(idxNextVal).trim(); - - details.extensions.push(extension); + var result = parseExpr(str); + for (var grp=0; grp<3; ++grp) { + if (Array.isArray(result[grp])) { + var vals = []; + for (var i=0,len=result[grp].length; i 2) { + // extension data + val.extensions = []; + for (var j=2,len2=result[grp][i].length; j 0) { - if (str[0] === '(') { - var isKey = true, key; - str = str.substr(1); - extensionData.params = {}; - while (str[0] !== ')') { - lastIndex = getNextIdxQuoted(str); - if (isKey) - key = str.substring(1, lastIndex).toLowerCase(); - else - extensionData.params[key] = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - isKey = !isKey; - } - str = str.substr(1).trim(); - } else - str = str.substr(4); - - // [disposition] - if (str.length > 0) { - if (str.substr(0, 3) !== 'NIL') { - extensionData.disposition = { type: null, params: null }; - str = str.substr(1); - lastIndex = getNextIdxQuoted(str); - extensionData.disposition.type = str.substring(1, lastIndex).toLowerCase(); - str = str.substr(lastIndex+1).trim(); - if (str[0] === '(') { - var isKey = true, key; - str = str.substr(1); - extensionData.disposition.params = {}; - while (str[0] !== ')') { - lastIndex = getNextIdxQuoted(str); - if (isKey) - key = str.substring(1, lastIndex).toLowerCase(); - else - extensionData.disposition.params[key] = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - isKey = !isKey; - } - str = str.substr(2).trim(); - } else - str = str.substr(4).trim(); +function parseBodyStructure(cur, prefix, partID) { + var ret = []; + if (typeof cur === 'string') { + var result = parseExpr(cur); + if (result.length) + ret = parseBodyStructure(result, '', 1); + } else { + var part, partLen = cur.length, next; + if (Array.isArray(cur[0])) { // multipart + next = -1; + while (Array.isArray(cur[++next])) + ret.push(parseBodyStructure(cur[next], prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1)); + part = { type: cur[next++].toLowerCase() }; + if (partLen > next) { + if (Array.isArray(cur[next])) { + part.params = {}; + for (var i=0,len=cur[next].length; i 0) { - if (str.substr(0, 3) !== 'NIL') { - lastIndex = getNextIdxQuoted(str); - extensionData.language = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - } else - str = str.substr(4); - - // [location] - if (str.length > 0) { - if (str.substr(0, 3) !== 'NIL') { - lastIndex = getNextIdxQuoted(str); - extensionData.location = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - } else - str = str.substr(4); - } - } + part.params = cur[next]; } - } - - retVal.unshift(extensionData); - } else { // single part - var part = { - partID: (prefix !== '' ? prefix : '1'), // the path identifier for this part, useful for fetching specific parts of a message - type: { name: null, params: null }, // content type and parameters (NIL or otherwise) - id: null, description: null, encoding: null, size: null, lines: null, // required -- NIL or otherwise - md5: null, disposition: null, language: null, location: null // optional extension data that may be omitted entirely - }, - lastIndex = getNextIdxQuoted(str), - contentTypeMain = str.substring(1, lastIndex), - contentTypeSub; - str = str.substr(lastIndex+1).trim(); - lastIndex = getNextIdxQuoted(str); - contentTypeSub = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - - // content type - part.type.name = contentTypeMain.toLowerCase() + '/' + contentTypeSub.toLowerCase(); - - // content type parameters - if (str[0] === '(') { - var isKey = true, key; - str = str.substr(1); - part.type.params = {}; - while (str[0] !== ')') { - lastIndex = getNextIdxQuoted(str); - if (isKey) - key = str.substring(1, lastIndex).toLowerCase(); - else - part.type.params[key] = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - isKey = !isKey; + } else { // single part + next = 7; + 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] } - str = str.substr(2); - } else - str = str.substr(4); - - // content id - if (str.substr(0, 3) !== 'NIL') { - lastIndex = getNextIdxQuoted(str); - part.id = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - } else - str = str.substr(4); - - // content description - if (str.substr(0, 3) !== 'NIL') { - lastIndex = getNextIdxQuoted(str); - part.description = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - } else - str = str.substr(4); - - // content encoding - if (str.substr(0, 3) !== 'NIL') { - lastIndex = getNextIdxQuoted(str); - part.encoding = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - } else - str = str.substr(4); - - // size of content encoded in bytes - if (str.substr(0, 3) !== 'NIL') { - lastIndex = 0; - while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57) - lastIndex++; - part.size = parseInt(str.substring(0, lastIndex)); - str = str.substr(lastIndex).trim(); - } else - str = str.substr(4); - - // [# of lines] - if (part.type.name.indexOf('text/') === 0) { - if (str.substr(0, 3) !== 'NIL') { - lastIndex = 0; - while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57) - lastIndex++; - part.lines = parseInt(str.substring(0, lastIndex)); - str = str.substr(lastIndex).trim(); - } else - str = str.substr(4); - } - - // [md5 hash of content] - if (str.length > 0) { - if (str.substr(0, 3) !== 'NIL') { - lastIndex = getNextIdxQuoted(str); - part.md5 = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - } else - str = str.substr(4); - - // [disposition] - if (str.length > 0) { - if (str.substr(0, 3) !== 'NIL') { - part.disposition = { type: null, params: null }; - str = str.substr(1); - lastIndex = getNextIdxQuoted(str); - part.disposition.type = str.substring(1, lastIndex).toLowerCase(); - str = str.substr(lastIndex+1).trim(); - if (str[0] === '(') { - var isKey = true, key; - str = str.substr(1); - part.disposition.params = {}; - while (str[0] !== ')') { - lastIndex = getNextIdxQuoted(str); - if (isKey) - key = str.substring(1, lastIndex).toLowerCase(); - else - part.disposition.params[key] = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - isKey = !isKey; - } - str = str.substr(2).trim(); - } else - str = str.substr(4).trim(); - } else - str = str.substr(4); - - // [language] - if (str.length > 0) { - if (str.substr(0, 3) !== 'NIL') { - if (str[0] === '(') { - part.language = []; - str = str.substr(1); - while (str[0] !== ')') { - lastIndex = getNextIdxQuoted(str); - part.language.push(str.substring(1, lastIndex)); - str = str.substr(lastIndex+1).trim(); + if (Array.isArray(cur[2])) { + part.params = {}; + for (var i=0,len=cur[2].length; i next && Array.isArray(cur[next])) { + part.envelope = {}; + for (var i=0,field,len=cur[next].length; i= 2 && i <= 7) { + var val = cur[next][i]; + if (Array.isArray(val)) { + var addresses = [], inGroup = false, curGroup; + for (var j=0,len2=val.length; j 0) { - if (str.substr(0, 3) !== 'NIL') { - lastIndex = getNextIdxQuoted(str); - part.location = str.substring(1, lastIndex); - str = str.substr(lastIndex+1).trim(); - } else - str = str.substr(4); + if (i === 2) + part.envelope.from = val; + else if (i === 3) + part.envelope.sender = val; + else if (i === 4) + part.envelope.replyTo = val; + else if (i === 5) + part.envelope.to = val; + else if (i === 6) + part.envelope.cc = val; + else if (i === 7) + part.envelope.bcc = val; + } else if (i === 8) + part.envelope.inReplyTo = cur[next][i]; // message ID being replied to + else if (i === 9) + part.envelope.messageID = cur[next][i]; + else + break; } - } + } else + part.envelope = null; + ++next; + + // body + if (partLen > next && Array.isArray(cur[next])) { + part.body = parseBodyStructure(cur[next], prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1); + } else + part.body = null; + ++next; } + if ((part.type === 'text' + || (part.type === 'message' && part.subtype === 'rfc822')) + && partLen > next) + part.lines = cur[next++]; + if (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; +} - retVal.push(part); +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']] + if (Array.isArray(cur[next])) { + part.disposition = {}; + if (Array.isArray(cur[next][1])) { + for (var i=0,len=cur[next][1].length; i 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]; } - return retVal; } String.prototype.explode = function(delimiter, limit) { @@ -1315,20 +1178,52 @@ function up(str) { return str.toUpperCase(); } -function getNextIdxQuoted(str) { - var index = -1, countQuote = 0; - for (var i=0,len=str.length; i 0 && str[i-1] === "\\") - continue; - countQuote++; - } - if (countQuote === 2) { - index = i; - break; - } +function parseExpr(o, result, start) { + start = start || 0; + var inQuote = false, lastPos = start - 1, isTop = false; + if (!result) + result = new Array(); + if (typeof o === 'string') { + var state = new Object(); + state.str = o; + o = state; + isTop = true; + } + for (var i=start,len=o.str.length; i 0) + result.push(convStr(o.str.substring(lastPos+1, i))); + if (o.str[i] === ')') + return i; + lastPos = i; + } else if (o.str[i] === '(') { + var innerResult = []; + i = parseExpr(o, innerResult, i+1); + lastPos = i; + result.push(innerResult); + } + } else if (o.str[i] === '"' && + (o.str[i-1] && + (o.str[i-1] !== '\\' || (o.str[i-2] && o.str[i-2] === '\\')))) + inQuote = false; + if (i+1 === len && len - (lastPos+1) > 0) + result.push(convStr(o.str.substring(lastPos+1))); } - return index; + return (isTop ? result : start); +} + +function convStr(str) { + if (str[0] === '"') + return str.substring(1, str.length-1); + else if (str === 'NIL') + return null; + else if (/^\d+$/.test(str)) + return parseInt(str, 10); + else + return str; } function getNextIdxParen(str) {