Simplify and fix parsing of message structures and mailbox namespaces

fork
Brian White 14 years ago
parent 375fae406b
commit ab6403735c

@ -104,13 +104,11 @@ A message structure with multiple parts might look something like the following:
, params: { boundary: '000e0cd294e80dc83c0475bf339b' } , params: { boundary: '000e0cd294e80dc83c0475bf339b' }
, disposition: null , disposition: null
, language: null , language: null
, location: null
} }
, [ { partID: '1.1' , [ { partID: '1.1'
, type: , type: 'text'
{ name: 'text/plain' , subtype: 'plain'
, params: { charset: 'ISO-8859-1' } , params: { charset: 'ISO-8859-1' }
}
, id: null , id: null
, description: null , description: null
, encoding: '7BIT' , encoding: '7BIT'
@ -119,14 +117,12 @@ A message structure with multiple parts might look something like the following:
, md5: null , md5: null
, disposition: null , disposition: null
, language: null , language: null
, location: null
} }
] ]
, [ { partID: '1.2' , [ { partID: '1.2'
, type: , type: 'text'
{ name: 'text/html' , subtype: 'html'
, params: { charset: 'ISO-8859-1' } , params: { charset: 'ISO-8859-1' }
}
, id: null , id: null
, description: null , description: null
, encoding: 'QUOTED-PRINTABLE' , encoding: 'QUOTED-PRINTABLE'
@ -135,15 +131,13 @@ A message structure with multiple parts might look something like the following:
, md5: null , md5: null
, disposition: null , disposition: null
, language: null , language: null
, location: null
} }
] ]
] ]
, [ { partID: '2' , [ { partID: '2'
, type: , type: 'application'
{ name: 'application/octet-stream' , subtype: 'octet-stream'
, params: { name: 'somefile' } , params: { name: 'somefile' }
}
, id: null , id: null
, description: null , description: null
, encoding: 'BASE64' , 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: The structure of a message with only one part will simply look something like this:
[ { partID: '1' [ { partID: '1'
, type: , type: 'text'
{ name: 'text/plain' , subtype: 'plain'
, params: { charset: 'ISO-8859-1' } , params: { charset: 'ISO-8859-1' }
}
, id: null , id: null
, description: null , description: null
, encoding: '7BIT' , encoding: '7BIT'
@ -177,7 +170,6 @@ The structure of a message with only one part will simply look something like th
, md5: null , md5: null
, disposition: null , disposition: null
, language: null , language: null
, location: null
} }
] ]
Therefore, an easy way to check for a multipart message is to check if the structure length is >1. 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): * **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 { 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 , 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 { 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 , params: [ 'BAZ' ] // An Array of Strings containing the parameters for this extension, or null if none are specified

@ -906,79 +906,31 @@ function validateUIDList(uids) {
} }
function parseNamespaces(str, namespaces) { function parseNamespaces(str, namespaces) {
// str contains 3 parenthesized lists (or NIL) describing the personal, other users', and shared namespaces available var result = parseExpr(str);
var idxNext, idxNextName, idxNextVal, strNamespace, strList, details, types = Object.keys(namespaces), curType = 0; for (var grp=0; grp<3; ++grp) {
while (str.length > 0) { if (Array.isArray(result[grp])) {
if (str.substr(0, 3) === 'NIL') var vals = [];
idxNext = 3; for (var i=0,len=result[grp].length; i<len; ++i) {
else { var val = { prefix: result[grp][i][0], delim: result[grp][i][1] };
idxNext = getNextIdxParen(str)+1; if (result[grp][i].length > 2) {
// extension data
// examples: (...) val.extensions = [];
// (...)(...) for (var j=2,len2=result[grp][i].length; j<len2; j+=2) {
strList = str.substring(1, idxNext-1); val.extensions.push({
name: result[grp][i][j],
// parse each namespace for the current type flags: result[grp][i][j+1]
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(); vals.push(val);
details.extensions.push(extension);
} }
} else if (grp === 0)
details.extensions = null; namespaces.personal = vals;
else if (grp === 1)
namespaces[types[curType]].push(details); namespaces.other = vals;
strList = strList.substr(idxNextName).trim(); else if (grp === 2)
namespaces.shared = vals;
} }
curType++;
}
str = str.substr(idxNext).trim();
} }
} }
@ -1032,251 +984,162 @@ function parseFetch(str, literalData, fetchData) {
} }
} }
function parseBodyStructure(str, prefix, partID) { function parseBodyStructure(cur, prefix, partID) {
var retVal = [], lastIndex; var ret = [];
prefix = (prefix !== undefined ? prefix : ''); if (typeof cur === 'string') {
partID = (partID !== undefined ? partID : 1); var result = parseExpr(cur);
if (str[0] === '(') { // multipart if (result.length)
var extensionData = { ret = parseBodyStructure(result, '', 1);
type: null, // required } else {
params: null, disposition: null, language: null, location: null // optional and may be omitted completely var part, partLen = cur.length, next;
}; if (Array.isArray(cur[0])) { // multipart
// Recursively parse each part next = -1;
while (str[0] === '(') { while (Array.isArray(cur[++next]))
lastIndex = getNextIdxParen(str); ret.push(parseBodyStructure(cur[next], prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1));
retVal.push(parseBodyStructure(str.substr(1, lastIndex-1), prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1)); part = { type: cur[next++].toLowerCase() };
str = str.substr(lastIndex+1).trim(); if (partLen > next) {
} if (Array.isArray(cur[next])) {
part.params = {};
// multipart type for (var i=0,len=cur[next].length; i<len; i+=2)
lastIndex = getNextIdxQuoted(str); part.params[cur[next][i].toLowerCase()] = cur[next][i+1];
extensionData.type = str.substring(1, lastIndex).toLowerCase();
str = str.substr(lastIndex+1).trim();
// [parameters]
if (str.length > 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();
} else
str = str.substr(4);
// [language]
if (str.length > 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 } else
str = str.substr(4); part.params = cur[next];
}
} }
}
}
retVal.unshift(extensionData);
} else { // single part } else { // single part
var part = { next = 7;
partID: (prefix !== '' ? prefix : '1'), // the path identifier for this part, useful for fetching specific parts of a message part = {
type: { name: null, params: null }, // content type and parameters (NIL or otherwise) // the path identifier for this part, useful for fetching specific
id: null, description: null, encoding: null, size: null, lines: null, // required -- NIL or otherwise // parts of a message
md5: null, disposition: null, language: null, location: null // optional extension data that may be omitted entirely partID: (prefix !== '' ? prefix : '1'),
},
lastIndex = getNextIdxQuoted(str), // required fields as per RFC 3501 -- null or otherwise
contentTypeMain = str.substring(1, lastIndex), type: cur[0].toLowerCase(), subtype: cur[1].toLowerCase(),
contentTypeSub; params: null, id: cur[3], description: cur[4], encoding: cur[5],
str = str.substr(lastIndex+1).trim(); size: cur[6]
lastIndex = getNextIdxQuoted(str); }
contentTypeSub = str.substring(1, lastIndex); if (Array.isArray(cur[2])) {
str = str.substr(lastIndex+1).trim(); part.params = {};
for (var i=0,len=cur[2].length; i<len; i+=2)
// content type part.params[cur[2][i].toLowerCase()] = cur[2][i+1];
part.type.name = contentTypeMain.toLowerCase() + '/' + contentTypeSub.toLowerCase(); }
if (part.type === 'message' && part.subtype === 'rfc822') {
// content type parameters // envelope
if (str[0] === '(') { if (partLen > next && Array.isArray(cur[next])) {
var isKey = true, key; part.envelope = {};
str = str.substr(1); for (var i=0,field,len=cur[next].length; i<len; ++i) {
part.type.params = {}; if (i === 0)
while (str[0] !== ')') { part.envelope.date = cur[next][i];
lastIndex = getNextIdxQuoted(str); else if (i === 1)
if (isKey) part.envelope.subject = cur[next][i];
key = str.substring(1, lastIndex).toLowerCase(); else if (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<len2; ++j) {
if (val[j][3] === null) { // start group addresses
inGroup = true;
curGroup = {
group: val[j][2],
addresses: []
};
} else if (val[j][2] === null) { // end of group addresses
inGroup = false;
addresses.push(curGroup);
} else { // regular user address
var info = {
name: val[j][0],
mailbox: val[j][2],
host: val[j][3]
};
if (inGroup)
curGroup.addresses.push(info);
else
addresses.push(info);
}
}
val = addresses;
}
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 else
part.type.params[key] = str.substring(1, lastIndex); break;
str = str.substr(lastIndex+1).trim();
isKey = !isKey;
} }
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 } else
str = str.substr(4); part.envelope = null;
++next;
// content description // body
if (str.substr(0, 3) !== 'NIL') { if (partLen > next && Array.isArray(cur[next])) {
lastIndex = getNextIdxQuoted(str); part.body = parseBodyStructure(cur[next], prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1);
part.description = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
} else } else
str = str.substr(4); part.body = null;
++next;
// content encoding }
if (str.substr(0, 3) !== 'NIL') { if ((part.type === 'text'
lastIndex = getNextIdxQuoted(str); || (part.type === 'message' && part.subtype === 'rfc822'))
part.encoding = str.substring(1, lastIndex); && partLen > next)
str = str.substr(lastIndex+1).trim(); 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;
}
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<len; i+=2)
part.disposition[cur[next][1][i].toLowerCase()] = cur[next][1][i+1];
} else } else
str = str.substr(4); part.disposition[cur[next][0]] = cur[next][1];
// 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 } else
str = str.substr(4); part.disposition = cur[next];
++next;
// [# of lines] }
if (part.type.name.indexOf('text/') === 0) { if (partLen > next) {
if (str.substr(0, 3) !== 'NIL') { // language can be a string or a list of one or more strings, so let's
lastIndex = 0; // make this more consistent ...
while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57) if (cur[next] !== null)
lastIndex++; part.language = (Array.isArray(cur[next]) ? cur[next] : [cur[next]]);
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 else
part.disposition.params[key] = str.substring(1, lastIndex); part.language = null;
str = str.substr(lastIndex+1).trim(); ++next;
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();
}
} else {
lastIndex = getNextIdxQuoted(str);
part.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);
part.location = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
} else
str = str.substr(4);
}
} }
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];
} }
} }
retVal.push(part);
}
return retVal;
}
String.prototype.explode = function(delimiter, limit) { String.prototype.explode = function(delimiter, limit) {
if (arguments.length < 2 || arguments[0] === undefined || arguments[1] === undefined || if (arguments.length < 2 || arguments[0] === undefined || arguments[1] === undefined ||
!delimiter || delimiter === '' || typeof delimiter === 'function' || typeof delimiter === 'object') !delimiter || delimiter === '' || typeof delimiter === 'function' || typeof delimiter === 'object')
@ -1315,20 +1178,52 @@ function up(str) {
return str.toUpperCase(); return str.toUpperCase();
} }
function getNextIdxQuoted(str) { function parseExpr(o, result, start) {
var index = -1, countQuote = 0; start = start || 0;
for (var i=0,len=str.length; i<len; i++) { var inQuote = false, lastPos = start - 1, isTop = false;
if (str[i] === '"') { if (!result)
if (i > 0 && str[i-1] === "\\") result = new Array();
continue; if (typeof o === 'string') {
countQuote++; var state = new Object();
} state.str = o;
if (countQuote === 2) { o = state;
index = i; isTop = true;
break; }
} for (var i=start,len=o.str.length; i<len; ++i) {
} if (!inQuote) {
return index; if (o.str[i] === '"')
inQuote = true;
else if (o.str[i] === ' ' || o.str[i] === ')') {
if (i - (lastPos+1) > 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 (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) { function getNextIdxParen(str) {

Loading…
Cancel
Save