Simplify and fix parsing of message structures and mailbox namespaces

fork
Brian White 13 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();
details.extensions.push(extension);
} }
} else }
details.extensions = null; vals.push(val);
namespaces[types[curType]].push(details);
strList = strList.substr(idxNextName).trim();
} }
curType++; if (grp === 0)
namespaces.personal = vals;
else if (grp === 1)
namespaces.other = vals;
else if (grp === 2)
namespaces.shared = vals;
} }
str = str.substr(idxNext).trim();
} }
} }
@ -1032,249 +984,160 @@ 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 } else
str = str.substr(4); part.params = cur[next];
// [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
str = str.substr(4);
}
}
} }
} } else { // single part
next = 7;
retVal.unshift(extensionData); part = {
} else { // single part // the path identifier for this part, useful for fetching specific
var part = { // parts of a message
partID: (prefix !== '' ? prefix : '1'), // the path identifier for this part, useful for fetching specific parts of a message partID: (prefix !== '' ? prefix : '1'),
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 // required fields as per RFC 3501 -- null or otherwise
md5: null, disposition: null, language: null, location: null // optional extension data that may be omitted entirely type: cur[0].toLowerCase(), subtype: cur[1].toLowerCase(),
}, params: null, id: cur[3], description: cur[4], encoding: cur[5],
lastIndex = getNextIdxQuoted(str), size: cur[6]
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;
} }
str = str.substr(2); if (Array.isArray(cur[2])) {
} else part.params = {};
str = str.substr(4); for (var i=0,len=cur[2].length; i<len; i+=2)
part.params[cur[2][i].toLowerCase()] = cur[2][i+1];
// content id }
if (str.substr(0, 3) !== 'NIL') { if (part.type === 'message' && part.subtype === 'rfc822') {
lastIndex = getNextIdxQuoted(str); // envelope
part.id = str.substring(1, lastIndex); if (partLen > next && Array.isArray(cur[next])) {
str = str.substr(lastIndex+1).trim(); part.envelope = {};
} else for (var i=0,field,len=cur[next].length; i<len; ++i) {
str = str.substr(4); if (i === 0)
part.envelope.date = cur[next][i];
// content description else if (i === 1)
if (str.substr(0, 3) !== 'NIL') { part.envelope.subject = cur[next][i];
lastIndex = getNextIdxQuoted(str); else if (i >= 2 && i <= 7) {
part.description = str.substring(1, lastIndex); var val = cur[next][i];
str = str.substr(lastIndex+1).trim(); if (Array.isArray(val)) {
} else var addresses = [], inGroup = false, curGroup;
str = str.substr(4); for (var j=0,len2=val.length; j<len2; ++j) {
if (val[j][3] === null) { // start group addresses
// content encoding inGroup = true;
if (str.substr(0, 3) !== 'NIL') { curGroup = {
lastIndex = getNextIdxQuoted(str); group: val[j][2],
part.encoding = str.substring(1, lastIndex); addresses: []
str = str.substr(lastIndex+1).trim(); };
} else } else if (val[j][2] === null) { // end of group addresses
str = str.substr(4); inGroup = false;
addresses.push(curGroup);
// size of content encoded in bytes } else { // regular user address
if (str.substr(0, 3) !== 'NIL') { var info = {
lastIndex = 0; name: val[j][0],
while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57) mailbox: val[j][2],
lastIndex++; host: val[j][3]
part.size = parseInt(str.substring(0, lastIndex)); };
str = str.substr(lastIndex).trim(); if (inGroup)
} else curGroup.addresses.push(info);
str = str.substr(4); else
addresses.push(info);
// [# of lines] }
if (part.type.name.indexOf('text/') === 0) { }
if (str.substr(0, 3) !== 'NIL') { val = addresses;
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();
} }
} else { if (i === 2)
lastIndex = getNextIdxQuoted(str); part.envelope.from = val;
part.language = [str.substring(1, lastIndex)]; else if (i === 3)
str = str.substr(lastIndex+1).trim(); part.envelope.sender = val;
} else if (i === 4)
} else part.envelope.replyTo = val;
str = str.substr(4); else if (i === 5)
part.envelope.to = val;
// [location] else if (i === 6)
if (str.length > 0) { part.envelope.cc = val;
if (str.substr(0, 3) !== 'NIL') { else if (i === 7)
lastIndex = getNextIdxQuoted(str); part.envelope.bcc = val;
part.location = str.substring(1, lastIndex); } else if (i === 8)
str = str.substr(lastIndex+1).trim(); part.envelope.inReplyTo = cur[next][i]; // message ID being replied to
} else else if (i === 9)
str = str.substr(4); 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<len; i+=2)
part.disposition[cur[next][1][i].toLowerCase()] = cur[next][1][i+1];
} else
part.disposition[cur[next][0]] = cur[next][1];
} else
part.disposition = cur[next];
++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];
} }
return retVal;
} }
String.prototype.explode = function(delimiter, limit) { String.prototype.explode = function(delimiter, limit) {
@ -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) {
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 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) { function getNextIdxParen(str) {

Loading…
Cancel
Save