* Body retrieval support (to go in thunderbird core, presumably)
authorAndrew Sutherland <asutherland@asutherland.org>
Wed, 23 Jul 2008 23:03:46 -0700
changeset 856 3d52bc12f0f5d699a465432dc577cd479e12e0e7
parent 855 77c5642b3712e8dc5eb59dc97bc6f3c66ead56db
child 857 b41840410c792a9a86579e340baa860f1ff2922d
push idunknown
push userunknown
push dateunknown
* Body retrieval support (to go in thunderbird core, presumably) * Refactoring required to support asynchrony required by body retrieval. * Handle updateFolder properly so that we wait for a folderLoaded event. This means we can open folders not yet opened! (read: have no .msf file)
components/jsmimeemitter.js
content/overlay.js
modules/explattr.js
modules/fundattr.js
modules/gloda.js
modules/indexer.js
modules/mimemsg.js
new file mode 100644
--- /dev/null
+++ b/components/jsmimeemitter.js
@@ -0,0 +1,297 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ * 
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ * 
+ * ***** END LICENSE BLOCK ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Custom nsIMimeEmitter to build a sub-optimal javascript representation of a
+ *  MIME message.  The intent is that a better mechanism than is evolved to
+ *  provide a javascript-accessible representation of the message.
+ *
+ * Processing occurs in two passes.  During the first pass, libmime is parsing
+ *  the stream it is receiving, and generating header and body events for all
+ *  MimeMessage instances it encounters.  This provides us with the knowledge
+ *  of each nested message in addition to the top level message, their headers
+ *  and sort-of their bodies.  The sort-of is that we may get more than
+ *  would normally be displayed in cases involving multipart/alternatives.
+ */
+function MimeMessageEmitter() {
+  this._mimeMsg = {};
+  Components.utils.import("resource://gloda/modules/mimemsg.js",
+                          this._mimeMsg);
+
+  this._url = null;
+  this._channel = null;
+
+  this._inputStream = null;
+  this._outputStream = null;
+  
+  this._outputListener = null;
+  
+  this._rootMsg = null;
+  this._messageStack = [];
+  this._parentMsg = null;
+  this._curMsg = null;
+  
+  this._messageIndex = 0;
+  this._allSubMessages = [];
+}
+
+MimeMessageEmitter.prototype = {
+  classDescription: "JS Mime Message Emitter",
+  classID: Components.ID("{80578315-7021-40f9-9717-413cacf2fa7d}"),
+  contractID: "@mozilla.org/steeldestined/jsmimeemitter;1",
+  
+  _xpcom_categories: [{
+    category: "mime-emitter",
+    entry:
+      "@mozilla.org/messenger/mimeemitter;1?type=application/x-js-mime-message",
+  }],
+  
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMimeEmitter]),
+
+  Initialize: function mime_emitter_Initialize(aUrl, aChannel, aFormat) {
+    this._partRE = new RegExp("^[^?]+\?(?:[^&]+&)*part=([^&]+)(?:&[^&]+)*$");
+
+    this._url = aUrl;
+    this._curMsg = this._parentMsg = this._rootMsg = new this._mimeMsg.MimeMessage();
+    
+    this._mimeMsg.MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aUrl.spec] =
+      this._rootMsg;
+    
+    this._channel = aChannel;
+  },
+  
+  Complete: function mime_emitter_Complete() {
+  },
+  
+  SetPipe: function mime_emitter_SetPipe(aInputStream, aOutputStream) {
+    this._inputStream = aInputStream;
+    this._outputStream = aOutputStream;
+  },
+  // can we use getters/setters to replace explicit functions on the interface?
+  SetOutputListener: function mime_emitter_SetOutputListener(aListener) {
+    this._outputListener = aListener;
+  },
+  GetOutputListener: function mime_emitter_GetOutputListener() {
+    return this._outputListener;
+  }, 
+  
+  // ----- Header Routines
+  StartHeader: function mime_emitter_StartHeader(aIsRootMailHeader,
+      aIsHeaderOnly, aMsgID, aOutputCharset) {
+    
+    if (aIsRootMailHeader) {
+      this.UpdateCharacterSet(aOutputCharset);
+      // nothing to do curMsg-wise, already initialized.
+    }
+    else {
+      this._curMsg = new this._mimeMsg.MimeMessage();
+      this._parentMsg.messages.push(this._curMsg);
+      this._allSubMessages.push(this._curMsg);
+    }
+  },
+  AddHeaderField: function mime_emitter_AddHeaderField(aField, aValue) {
+    let lowerField = aField.toLowerCase();
+    if (lowerField in this._curMsg.headers)
+      this._curMsg.headers[lowerField].push(aValue);
+    else
+      this._curMsg.headers[lowerField] = [aValue];
+  },
+  addAllHeaders: function mime_emitter_addAllHeaders(aAllHeaders, aHeaderSize) {
+    // This is called by the parsing code after the calls to AddHeaderField (or
+    //  AddAttachmentField if the part is an attachment), and seems to serve
+    //  a specialized, quasi-redundant purpose.  (nsMimeBaseEmitter creates a
+    //  nsIMimeHeaders instance and hands it to the nsIMsgMailNewsUrl.)
+    // nop
+  },
+  WriteHTMLHeaders: function mime_emitter_WriteHTMLHeaders() {
+    // It does't look like this should even be part of the interface; I think
+    //  only the nsMimeHtmlDisplayEmitter::EndHeader call calls this signature.
+    // nop
+  },
+  EndHeader: function mime_emitter_EndHeader() {
+  },
+  UpdateCharacterSet: function mime_emitter_UpdateCharacterSet(aCharset) {
+    // for non US-ASCII, ISO-8859-1, or UTF-8 charsets (case-insensitive),
+    //  nsMimeBaseEmitter grabs the channel's content type, nukes the "charset="
+    //  parameter if it exists, and tells the channel the updated content type
+    //  and new character set.
+    
+    // Disabling for now; we get a NS_ERROR_NOT_IMPLEMENTED from the channel
+    //  when we try and set the contentCharset... and I'm not totally up on the
+    //  intent of why we were doing this in the first place.
+    /*
+    let upperCharset = aCharset.toUpperCase();
+    
+    if ((upperCharset != "US-ASCII") && (upperCharset != "ISO-8859-1") &&
+        (upperCharset != "UTF-8")) {  
+    
+      let curContentType = this._channel.contentType;
+      let charsetIndex = curContentType.toLowerCase().indexOf("charset=");
+      if (charsetIndex >= 0) {
+        // assume a space or semicolon delimits
+        curContentType = curContentType.substring(0, charsetIndex-1);
+      }
+      
+      this._channel.contentType = curContentType;
+      this._channel.contentCharset = aCharset;
+    }
+    */
+  },
+  
+  /**
+   * Put a part at its proper location.  We rely on this method to be called
+   *  in the the sequence generated by StartAttachment (an in-order traversal
+   *  of the MIME structure).
+   */
+  _putPart: function(aPartPath, aPathSoFar, aPart, aParent) {
+    let dotIndex = aPartPath.indexOf(".");
+    let curPath, remPath;
+    if (dotIndex >= 0) {
+      curPath = aPartPath.substring(0, dotIndex);
+      remPath = aPartPath.substring(dotIndex+1);
+    }
+    else {
+      curPath = aPartPath;
+      remPath = null;
+    }
+    let newPathSoFar = aPathSoFar + "." + curPath;
+    let curIndex = parseInt(curPath) - 1;
+
+    // add MimeUnknowns for parts that should already exist
+    while (curIndex > aParent.parts.length) {
+      aParent.parts.push(new this._mimeMsg.MimeUnknown(newPathSoFar));
+    }
+    
+    // are we a leaf?
+    if (remPath !== null) {
+      // no, we are not a leaf
+      if (curIndex == aParent.parts.length) {
+        // and we need to add a container
+        aParent.parts.push(new this._mimeMsg.MimeContainer(newPathSoFar));
+      }
+      this._putPart(remPath, newPathSoFar, aPart, aParent.parts[curIndex]);
+    }
+    else {
+      // yes, we are a leaf, we just go here...
+      aParent.parts.push(aPart);
+    }
+  },
+  
+  // ----- Attachment Routines
+  // The attachment processing happens after the initial streaming phase (during
+  //  which time we receive the messages, both bodies and headers).  Our caller
+  //  traverses the libmime child object hierarchy, emitting an attachment for
+  //  each leaf object or sub-message.
+  StartAttachment: function mime_emitter_StartAttachment(aName, aContentType,
+      aUrl, aNotDownloaded) {
+    
+    // we need to strip our magic flags from the URL
+    aURl = aUrl.replace("header=filter&emitter=js&", "");
+    
+    // the url should contain a part= piece that tells us the part name, which
+    //  we then use to figure out where.
+    let partMatch = this._partRE.exec(aUrl);
+    let partName = partMatch[1];
+
+    let part;
+    if (aContentType == "message/rfc822") {
+      // since we are assuming an in-order traversal, it's safe to assume that
+      //  we will see the messages in the same order we previously saw them.
+      part = this._allSubMessages[this._messageIndex++];
+      part.partName = partName;
+    }
+    else {
+      // create the attachment
+      part = new this._mimeMsg.MimeMessageAttachment(partName,
+          aName, aContentType, aUrl, aNotDownloaded);
+    }
+    
+    this._putPart(part.partName.substring(2), "1",
+                  part, this._rootMsg);
+  },
+  AddAttachmentField: function mime_emitter_AddAttachmentField(aField, aValue) {
+    // this only gives us X-Mozilla-PartURL, which is the same as aUrl we
+    //  already got previously, so need to do anything with this.
+  },
+  EndAttachment: function mime_emitter_EndAttachment() {
+    // don't need to do anything here, since we don't care about the headers.
+  },
+  EndAllAttachments: function mime_emitter_EndAllAttachments() {
+    // nop
+  },
+  
+  // ----- Body Routines
+  StartBody: function mime_emitter_StartBody(aIsBodyOnly, aMsgID, aOutCharset) {
+    this._messageStack.push(this._curMsg);
+    this._parentMsg = this._curMsg;
+  },
+  
+  WriteBody: function mime_emitter_WriteBody(aBuf, aSize, aOutAmountWritten) {
+    this._curMsg.body += aBuf;
+  },
+  
+  EndBody: function mime_emitter_EndBody() {
+    this._messageStack.pop();
+    this._parentMsg = this._messageStack[this._messageStack.length - 1];
+  },
+  
+  // ----- Generic Write (confusing)
+  // (binary data writing...)
+  Write: function mime_emitter_Write(aBuf, aSize, aOutAmountWritten) {
+    // we don't actually ever get called because we don't have the attachment
+    //  binary payloads pass through us, but we do the following just in case
+    //  we did get called (otherwise the caller gets mad and throws exceptions).
+    aOutAmountWritten.value = aSize;
+  },
+  
+  // (string writing)
+  UtilityWrite: function mime_emitter_UtilityWrite(aBuf) {
+    this.Write(aBuf, aBuf.length, {});
+  },
+};
+
+var components = [MimeMessageEmitter];
+function NSGetModule(compMgr, fileSpec) {
+  return XPCOMUtils.generateModule(components);
+}
--- a/content/overlay.js
+++ b/content/overlay.js
@@ -38,22 +38,30 @@
 // get the core
 Components.utils.import("resource://gloda/modules/gloda.js");
 // make all the built-in plugins join the party
 Components.utils.import("resource://gloda/modules/everybody.js");
 
 Components.utils.import("resource://gloda/modules/indexer.js");
 
 var gloda = {
+  _mimeMsg: {},
+  
   onLoad: function() {
     // initialization code
     this.initialized = true;
     this.strings = document.getElementById("gloda-strings");
+    
+    // initialize the globals required for the JS Mime representation
+    Components.utils.import("resource://gloda/modules/mimemsg.js",
+                            this._mimeMsg);
+    this._mimeMsg.MsgHdrToMimeMessage.initGlobals(messenger, msgWindow);
+    
     GlodaInitModules(this.strings);
-    GlodaIndexer.init(window, msgWindow, this.strings);
+    GlodaIndexer.init(window, msgWindow, this.strings, messenger);
     GlodaIndexer.enabled = true;
   },
   onMenuItemCommand: function(e) {
     GlodaIndexer.indexEverything();
   },
 
 };
 window.addEventListener("load", function(e) { gloda.onLoad(e); }, false);
--- a/modules/explattr.js
+++ b/modules/explattr.js
@@ -90,16 +90,18 @@ let GlodaExplicitAttr = {
                         bind: true,
                         bindName: "tags",
                         singular: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_DATE,
                         parameterNoun: Gloda.NOUN_TAG,
                         explanation: this._strBundle.getString(
                                        "attrTagExplanation"),
+                        // Property change notifications that we care about:
+                        propertyChanges: ["keywords"],
                         });
     // Star
     this._attrStar = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrExplicit,
                         attributeName: "star",
                         bind: true,
@@ -123,17 +125,17 @@ let GlodaExplicitAttr = {
                         objectNoun: Gloda.NOUN_BOOLEAN,
                         parameterNoun: null,
                         explanation: this._strBundle.getString(
                                        "attrReadExplanation"),
                         });
     
   },
   
-  process: function Gloda_explattr_process(aGlodaMessage, aMsgHdr) {
+  process: function Gloda_explattr_process(aGlodaMessage, aMsgHdr, aMimeMsg) {
     let attribs = [];
     
     // -- Tag
     let keywords = aMsgHdr.getStringProperty("keywords");
     
     return attribs;
   },
 };
--- a/modules/fundattr.js
+++ b/modules/fundattr.js
@@ -171,17 +171,17 @@ let GlodaFundAttr = {
    * Specializations:
    * - Mailing Lists.  Replies to a message on a mailing list frequently only
    *   have the list-serve as the 'to', so we try to generate a synthetic 'to'
    *   based on the author of the parent message when possible.  (The 'possible'
    *   part is that we may not have a copy of the parent message at the time of
    *   processing.)
    * - Newsgroups.  Same deal as mailing lists.
    */
-  process: function gloda_fundattr_process(aGlodaMessage, aMsgHdr) {
+  process: function gloda_fundattr_process(aGlodaMessage, aMsgHdr, aMimeMsg) {
     let attribs = [];
     
     // -- From
     // Let's use replyTo if available.
     // er, since we are just dealing with mailing lists for now, forget the
     //  reply-to...
     // TODO: deal with default charset issues
     let author = null;
--- a/modules/gloda.js
+++ b/modules/gloda.js
@@ -448,25 +448,27 @@ let Gloda = {
     return attr;
   },
   
   getAttrDef: function gloda_ns_getAttrDef(aPluginName, aAttrName) {
     let compoundName = aPluginName + ":" + aAttrName;
     return GlodaDatastore._attributes[compoundName];
   },
   
-  processMessage: function gloda_ns_processMessage(aMessage, aMsgHdr) {
+  processMessage: function gloda_ns_processMessage(aMessage, aMsgHdr,
+                                                   aMimeMsg) {
     // For now, we are ridiculously lazy and simply nuke all existing attributes
     //  before applying the new attributes.
     aMessage._datastore.clearMessageAttributes(aMessage);
     
     let allAttribs = [];
   
     for(let i = 0; i < this._attrProviderOrder.length; i++) {
-      let attribs = this._attrProviderOrder[i].process(aMessage, aMsgHdr);
+      let attribs = this._attrProviderOrder[i].process(aMessage, aMsgHdr,
+                                                       aMimeMsg);
       allAttribs = allAttribs.concat(attribs);
     }
     
     let outAttribs = [];
     
     for(let iAttrib=0; iAttrib < allAttribs.length; iAttrib++) {
       let attribDesc = allAttribs[iAttrib];
       
--- a/modules/indexer.js
+++ b/modules/indexer.js
@@ -43,16 +43,18 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gloda/modules/log4moz.js");
 
 Cu.import("resource://gloda/modules/utils.js");
 Cu.import("resource://gloda/modules/datastore.js");
 Cu.import("resource://gloda/modules/gloda.js");
 
+Cu.import("resource://gloda/modules/mimemsg.js");
+
 function range(begin, end) {
   for (let i = begin; i < end; ++i) {
     yield i;
   }
 }
 
 // FROM STEEL
 /**
@@ -130,38 +132,44 @@ function IndexingJob(aJobType, aDeltaTyp
   this.offset = 0;
   this.goal = null;
 }
 
 let GlodaIndexer = {
   _datastore: GlodaDatastore,
   _log: Log4Moz.Service.getLogger("gloda.indexer"),
   _strBundle: null,
+  _messenger: null,
   _msgwindow: null,
   _domWindow: null,
 
   _inited: false,
-  init: function gloda_index_init(aDOMWindow, aMsgWindow, aStrBundle) {
+  init: function gloda_index_init(aDOMWindow, aMsgWindow, aStrBundle,
+                                  aMessenger) {
     if (this._inited)
       return;
     
     this._inited = true;
     
     this._domWindow = aDOMWindow;
     
-    // topmostMsgWindow explodes for un-clear reasons if we have multiple
-    //  windows open.  very sad.
-    /*
-    let mailSession = Cc["@mozilla.org/messenger/services/session;1"].
-                        getService(Ci.nsIMsgMailSession);
-    this._msgWindow = mailSession.topmostMsgWindow;
-    */
     this._msgWindow = aMsgWindow;
     
     this._strBundle = aStrBundle;
+    
+    this._messenger = aMessenger;
+    
+    // topmostMsgWindow explodes for un-clear reasons if we have multiple
+    //  windows open.  very sad.
+    let mailSession = Cc["@mozilla.org/messenger/services/session;1"].
+                        getService(Ci.nsIMsgMailSession);
+
+    this._folderListener._init(this);
+    mailSession.AddFolderListener(this._folderListener,
+                                  Ci.nsIFolderListener.event);
   },
   
   /**
    * Are we enabled, read: are we processing change events?
    */
   _enabled: false,
   get enabled() { return this._enabled; },
   set enabled(aEnable) {
@@ -275,37 +283,46 @@ let GlodaIndexer = {
     if (!this.indexing)
       aListener(this._strBundle ? this._strBundle.getString("actionIdle") : "",
                 null, 0, 1, 0, 1);
     return aListener;
   },
   removeListener: function gloda_index_removeListener(aListener) {
     let index = this._indexListeners.indexOf(aListener);
     if (index != -1)
-      this._indexListeners(index, 1);
+      this._indexListeners.splice(index, 1);
   },
   _notifyListeners: function gloda_index_notifyListeners(aStatus, aFolderName,
       aFolderIndex, aFoldersTotal, aMessageIndex, aMessagesTotal) {
     for (let iListener=this._indexListeners.length-1; iListener >= 0; 
          iListener--) {
       let listener = this._indexListeners[iListener];
       listener(aStatus, aFolderName, aFolderIndex, aFoldersTotal, aMessageIndex,
                aMessagesTotal);
     }
   },
   
   _indexingFolderID: null,
   _indexingFolder: null,
   _indexingDatabase: null,
   _indexingIterator: null,
   
+  _pendingAsyncOps: 0,
+  /** folder whose entry we are pending on */
+  _pendingFolderEntry: null,
+  
   /**
    * Common logic that we want to deal with the given folder ID.  Besides
    *  cutting down on duplicate code, this ensures that we are listening on
    *  the folder in case it tries to go away when we are using it.
+   *
+   * @return true when the folder was successfully entered, false when we need
+   *     to pend on notification of updating of the folder (due to re-parsing
+   *     or what have you).  In the event of an actual problem, an exception
+   *     will escape.
    */
   _indexerEnterFolder: function gloda_index_indexerEnterFolder(aFolderID,
                                                                aNeedIterator) {
     // if leave folder was't cleared first, remove the listener; everyone else
     //  will be nulled out in the exception handler below if things go south
     //  on this folder.
     if (this._indexingFolder !== null) {
       this._indexingDatabase.RemoveListener(this._databaseAnnouncerListener);
@@ -324,17 +341,27 @@ let GlodaIndexer = {
 
     // The msf may need to be created or otherwise updated, updateFolder will
     //  do this for us.  (GetNewMessages would also do it, but we would be
     //  triggering new message retrieval in that case, which we don't actually
     //  desire.
     // TODO: handle password-protected local cache potentially triggering a
     //  password prompt here...
     try {
-      this._indexingFolder.updateFolder(this._msgWindow);
+      try {
+        this._indexingFolder.updateFolder(this._msgWindow);
+      }
+      catch ( e if e.result == 0xc1f30001) {
+        // this means that we need to pend on the update.
+        this._log.debug("Pending on folder load...");
+        this._pendingFolderEntry = this._indexingFolder;
+        this._indexingFolder = null;
+        this._indexingFolderID = null;
+        return false;
+      }
       // we get an nsIMsgDatabase out of this (unsurprisingly) which
       //  explicitly inherits from nsIDBChangeAnnouncer, which has the
       //  AddListener call we want.
       this._indexingDatabase = folder.getMsgDatabase(this._msgWindow);
       if (aNeedIterator)
         this._indexingIterator = Iterator(fixIterator(
                                    //folder.getMessages(this._msgWindow),
                                    this._indexingDatabase.EnumerateMessages(),
@@ -350,36 +377,67 @@ let GlodaIndexer = {
       this._indexingFolderID = null;
       this._indexingDatabase = null;
       this._indexingIterator = null;
       
       // re-throw, we just wanted to make sure this junk is cleaned up and
       //  get localized error logging...
       throw ex;
     }
+    
+    return true;
   },
   
   _indexerLeaveFolder: function gloda_index_indexerLeaveFolder(aExpected) {
     if (this._indexingFolder !== null) {
       // remove our listener!
       this._indexingDatabase.RemoveListener(this._databaseAnnouncerListener);
       // null everyone out
       this._indexingFolder = null;
       this._indexingFolderID = null;
       this._indexingDatabase = null;
       this._indexingIterator = null;
       // ...including the active job:
       this._curIndexingJob = null;
     }
   },
   
+  /**
+   * Event fed to us by our nsIFolderListener when a folder is loaded.  We use
+   *  this event to two ends:
+   *
+   * - Know when a folder we were trying to open to index is actually ready to
+   *   be indexed.  (The summary may have not existed, may have been out of
+   *   date, or otherwise.)
+   * - Know when 
+   *
+   * @param aFolder An nsIMsgFolder, already QI'd.
+   */
+  _onFolderLoaded: function gloda_index_onFolderLoaded(aFolder) {
+    if ((this._pendingFolderEntry !== null) &&
+        (aFolder.URI == this._pendingFolderEntry.URI)) {
+      this._log.debug("...Folder Loaded!");
+      this._pendingFolderEntry = null;
+      this.incrementalIndex();
+    }
+  },
+  
+  /**
+   * A simple wrapper to make 'this' be right for incrementalIndex.
+   */
   _wrapIncrementalIndex: function gloda_index_wrapIncrementalIndex(aThis) {
     aThis.incrementalIndex();
   },
   
+  /**
+   * The incremental indexing core logic, responsible for performing work on
+   *  the current job and de-queueing new jobs as needed, while trying to
+   *  keeping our time-slice reasonable.  We use 'tokens' to track cost/activity
+   *  now, but could move to something more explicit such as using a timer.
+   */
   incrementalIndex: function gloda_index_incrementalIndex() {
     this._log.debug("index wake-up!");
   
     GlodaDatastore._beginTransaction();
     try {
       let job = this._curIndexingJob;
       for (let tokensLeft=this._indexTokens; tokensLeft > 0; tokensLeft--) {
         // --- Do we need a job?
@@ -408,17 +466,25 @@ let GlodaIndexer = {
               job = this._curIndexingJob = this._indexQueue.shift();
               this._indexingJobCount++;
               this._log.debug("Pulled job: " + job.jobType + ", " +
                               job.deltaType + ", " + job.id);
               // (Prepare for the job...)
               if (job.jobType == "folder") {
                 // -- FOLDER ADD
                 if (job.deltaType > 0) {
-                  this._indexerEnterFolder(job.id, true)
+                  if(!this._indexerEnterFolder(job.id, true)) {
+                    // so, we need to wait for the folder to load, so we need
+                    //  to back out things a little...
+                    this._log.debug("Pending on folder load; job goes back.");
+                    this._indexingJobCount--;
+                    this._indexQueue.unshift(job);
+                    this._curIndexingJob = null;
+                    return;
+                  }
                   job.goal = this._indexingFolder.getTotalMessages(false);
                 }
                 // -- FOLDER DELETE
                 else {
                   // nuke the folder id
                   this._datastore.deleteFolderByID(job.id);
                   // and we're done!
                   job = this._curIndexingJob = null;
@@ -456,35 +522,45 @@ let GlodaIndexer = {
             else if (job.jobType == "message") {
               let item = job.items[job.offset++];
               // -- MESSAGE ADD (batch steady state)
               if (job.deltaType > 0) {
                 // item is either [folder ID, message key] or
                 //                [folder ID, message ID]
 
                 // get in the folder
-                if (this._indexingFolderID != item[0])
-                  this._indexerEnterFolder(item[0], false);
+                if (this._indexingFolderID != item[0]) {
+                  if (!this._indexerEnterFolder(item[0], false)) {
+                    // need to pend on the folder...
+                    job.offset--;
+                    return;
+                  }
+                }
                 let msgHdr;
                 if (typeof item[1] == "number")
                   msgHdr = this._indexingFolder.GetMessageHeader(item[1]);
                 else
                   // same deal as in move processing.
                   // TODO fixme to not assume singular message-id's.
                   msgHdr = this._indexingDatabase.getMsgHdrForMessageID(item[1]);
                 if (msgHdr)
                   this._indexMessage(msgHdr);
               }
               // -- MESSAGE MOVE (batch steady state)
               else if (job.deltaType == 0) {
                 // item must be [folder ID, header message-id]
                 
                 // get in the folder
-                if (this._indexingFolderID != item[0])
-                  this._indexerEnterFolder(item[0], false);
+                if (this._indexingFolderID != item[0]) {
+                  if (!this._indexerEnterFolder(item[0], false)) {
+                    // need to pend on the folder
+                    job.offset--;
+                    return
+                  }
+                }
                 // process everyone with the message-id.  yeck.
                 // uh, except nsIMsgDatabase only thinks there should be one, so
                 //  let's pretend that this assumption is not a bad idea for now
                 // TODO: stop pretending this assumption is not a bad idea
                 let msgHdr = this._indexingDatabase.getMsgHdrForMessageID(item[1]);
                 if (msgHdr) {
                   this._indexMessage(msgHdr);
                 }
@@ -496,17 +572,21 @@ let GlodaIndexer = {
                 // remember to eat extra tokens... when we get more than one...
               }
               // -- MESSAGE DELETE (batch steady state)
               else { // job.deltaType < 0
                 // item is either: a message id
                 //              or [folder ID, message key]
                 let message;
                 if (item instanceof Array) {
-                  this._indexerEnterFolder(item[0], false);
+                  if (!this._indexerEnterFolder(item[0], false)) {
+                    // need to pend on the folder
+                    job.offset--;
+                    return;
+                  }
                   message = this_indexingFolder.GetMessageHeader(item[1]);
                 }
                 else {
                   message = GlodaDatastore.getMessageByID(item);
                 }
                 // delete the message!
                 if (message !== null)
                   this._deleteMessage(message);
@@ -548,24 +628,30 @@ let GlodaIndexer = {
                                   this._indexingJobGoal,
                                   job.offset,
                                   job.goal);
           }
         }
       }
     }
     finally {
-      GlodaDatastore._commitTransaction();
-    
-      if (this.indexing)
-        this._domWindow.setTimeout(this._wrapIncrementalIndex, this._indexInterval,
-                                this);
+      if (this._pendingAsyncOps <= 0) {
+        this._pendingAsyncOpsCompleted();
+      }
     }
   },
 
+  _pendingAsyncOpsCompleted: function gloda_index_pendingAsyncOpsCompleted() {
+    GlodaDatastore._commitTransaction();
+  
+    if (this.indexing && (this._pendingFolderEntry === null))
+      this._domWindow.setTimeout(this._wrapIncrementalIndex, this._indexInterval,
+                              this);
+  },
+
   indexEverything: function glodaIndexEverything() {
     this._log.info("Queueing all accounts for indexing.");
     let msgAccountManager = Cc["@mozilla.org/messenger/account-manager;1"].
                             getService(Ci.nsIMsgAccountManager);
     
     GlodaDatastore._beginTransaction();
     let sideEffects = [this.indexAccount(account) for each
                        (account in fixIterator(msgAccountManager.accounts,
@@ -799,16 +885,78 @@ let GlodaIndexer = {
     },
     
     itemEvent: function gloda_indexer_itemEvent(aItem, aEvent, aData) {
       // nop.  this is an expansion method on the part of the interface and has
       //  no known events that we need to handle.
     },
   },
   
+  /**
+   * A nsIFolderListener (listening on nsIMsgMailSession so we get all of
+   *  these events) PRIMARILY to get folder loaded notifications.  Because of
+   *  deficiencies in the nsIMsgFolderListener's events at this time, we also
+   *  get our folder-added and newsgroup notifications from here for now.  (This
+   *  will be rectified.)  
+   */
+  _folderListener: {
+    indexer: null,
+    _kFolderLoadedAtom: null,
+    
+    _init: function gloda_indexer_fl_init(aIndexer) {
+      this.indexer = aIndexer;
+      let atomService = Cc["@mozilla.org/atom-service;1"].
+                        getService(Ci.nsIAtomService);
+      this._kFolderLoadedAtom = atomService.getAtom("FolderLoaded");
+    },
+  
+    /**
+     * Find out when folders are added or new messages show up in a newsgroup.
+     */
+    OnItemAdded: function gloda_indexer_OnItemAdded(aParentItem, aItem) {
+    },
+    
+    /**
+     * Find out when messages disappear from a newsgroup.
+     */
+    OnItemRemoved: function gloda_indexer_OnItemRemoved(aParentItem, aItem) {
+    },
+    
+    /**
+     * Do nothing, we get our header change notifications directly from the
+     *  nsMsgDatabase.
+     */
+    OnItemPropertyChanged: function gloda_indexer_OnItemPropertyChanged(
+                             aItem, aProperty, aOldValue, aNewValue) {
+    },
+    OnItemIntPropertyChanged: function gloda_indexer_OnItemIntPropertyChanged(
+                                aItem, aProperty, aOldValue, aNewValue) {
+    },
+    OnItemBoolPropertyChanged: function gloda_indexer_OnItemBoolPropertyChanged(
+                                aItem, aProperty, aOldValue, aNewValue) {
+    },
+    OnItemUnicharPropertyChanged:
+        function gloda_indexer_OnItemUnicharPropertyChanged(
+          aItem, aProperty, aOldValue, aNewValue) {
+      
+    },
+    OnItemPropertyFlagChanged: function gloda_indexer_OnItemPropertyFlagChanged(
+                                aItem, aProperty, aOldValue, aNewValue) {
+    },
+    
+    /**
+     * Get folder loaded notifications for folders that had to do some
+     *  (asynchronous) processing before they could be opened.
+     */
+    OnItemEvent: function gloda_indexer_OnItemEvent(aFolder, aEvent) {
+      if (aEvent == this._kFolderLoadedAtom)
+        this.indexer._onFolderLoaded(aFolder);
+    },
+  },
+  
   /* ***** Rebuilding / Reindexing ***** */
   // TODO: implement a folder observer doodad to handle rebuilding / reindexing
   /**
    * Allow us to invalidate an outstanding folder traversal because the
    *  underlying database is going away.  We use other means for detecting 
    *  modifications of the message (labeling, marked (un)read, starred, etc.)
    *
    * This is an nsIDBChangeListener listening to an nsIDBChangeAnnouncer.  To
@@ -880,19 +1028,34 @@ let GlodaIndexer = {
    *  variants), or in a Microsoft specific-ism, from the Thread-Topic header.
    * Since we are using the nsIMsgDBHdr's subject field, this is already done
    *  for us, and we don't actually need to do any extra work.  Hooray!
    */
   _extractOriginalSubject: function glodaIndexExtractOriginalSubject(aMsgHdr) {
     return aMsgHdr.mime2DecodedSubject;
   },
   
-  _indexMessage: function gloda_index_indexMessage(aMsgHdr) {
+  _indexMessage: function gloda_indexMessage(aMsgHdr) {
+    MsgHdrToMimeMessage(aMsgHdr, this, this._indexMessageWithBody);
+    this._pendingAsyncOps++;
+  },
+  
+  _indexMessageWithBody: function gloda_index_indexMessageWithBody(
+       aMsgHdr, aMimeMsg) {
     this._log.debug("*** Indexing message: " + aMsgHdr.messageKey + " : " +
                     aMsgHdr.subject);
+
+    /* for now, let's be okay if there's no mime; but the plugins will be sad :(
+    if (aMimeMsg === null) {
+      if (--this._pendingAsyncOps == 0)
+       this._pendingAsyncOpsCompleted();
+      return;
+    } 
+    */
+    
     // -- Find/create the conversation the message belongs to.
     // Our invariant is that all messages that exist in the database belong to
     //  a conversation.
     
     // - See if any of the ancestors exist and have a conversationID...
     // (references are ordered from old [0] to new [n-1])
     let references = [aMsgHdr.getStringReference(i) for each
                       (i in range(0, aMsgHdr.numReferences))];
@@ -1010,17 +1173,20 @@ let GlodaIndexer = {
                                              null); // no snippet
      }
      else {
         curMsg._folderID = this._datastore._mapFolderURI(aMsgHdr.folder.URI);
         curMsg._messageKey = aMsgHdr.messageKey;
         this._datastore.updateMessage(curMsg);
      }
      
-     Gloda.processMessage(curMsg, aMsgHdr);
+     Gloda.processMessage(curMsg, aMsgHdr, aMimeMsg);
+     
+     if (--this._pendingAsyncOps == 0)
+       this._pendingAsyncOpsCompleted();
   },
   
   /**
    * Wipe a message out of existence from our index.  This is slightly more
    *  tricky than one would first expect because there are potentially
    *  attributes not immediately associated with this message that reference
    *  the message.  Not only that, but deletion of messages may leave a
    *  conversation posessing only ghost messages, which we don't want, so we
new file mode 100644
--- /dev/null
+++ b/modules/mimemsg.js
@@ -0,0 +1,338 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ * 
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ * 
+ * ***** END LICENSE BLOCK ***** */
+
+EXPORTED_SYMBOLS = ['MsgHdrToMimeMessage',
+                    'MimeMessage', 'MimeContainer', 'MimeUnknown',
+                    'MimeMessageAttachment'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const EMITTER_MIME_CODE = "application/x-js-mime-message";
+
+/**
+ * The URL listener is surplus because the CallbackStreamListener ends up
+ *  getting the same set of events, effectively.
+ */
+let dumbUrlListener = {
+  OnStartRunningUrl: function (aUrl) {
+  },
+  OnStopRunningUrl: function (aUrl, aExitCode) {
+  },
+};
+
+let gCallbacks = {};
+
+function CallbackStreamListener(aMsgHdr, aCallbackThis, aCallback) {
+  this._msgHdr = aMsgHdr;
+  if (aCallback === undefined) {
+    this._callbackThis = null;
+    this._callback = aCallbackThis;
+  }
+  else {
+    this._callbackThis = aCallbackThis;
+    this._callback = aCallback;
+  }
+}
+
+CallbackStreamListener.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]),
+
+  // nsIRequestObserver part
+  onStartRequest: function (aRequest, aContext) {
+  },
+  onStopRequest: function (aRequest, aContext, aStatusCode) {
+    let message = MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aContext.spec];
+    if (message === undefined)
+      message = null;
+    
+    if (this._callbackThis)
+      this._callback.call(this._callbackThis, this._msgHdr, message);
+    else
+      this._callback.call(null, this._msgHdr, message);
+    
+    delete MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aContext.spec];
+  },
+
+  /* okay, our onDataAvailable should actually never be called.  the stream
+     converter is actually eating everything except the start and stop
+     notification. */
+  // nsIStreamListener part
+  onDataAvailable: function (aRequest,aContext,aInputStream,aOffset,aCount) {
+    /*
+    if (this._stream === null) {
+      this._stream = Cc["@mozilla.org/scriptableinputstream;1"].
+                    createInstance(Ci.nsIScriptableInputStream);
+      this._stream.init(aInputStream);
+    }
+    this._stream.read(aCount);
+    */
+  },
+};
+
+let gMessenger = null;
+let gMsgWindow = null;
+
+/**
+ * Starts retrieval of a MimeMessage instance for the given message header.
+ *  Your callback will be called with the message header you provide and the
+ *  
+ * @param aMsgHdr The message header to retrieve the body for and build a MIME
+ *     representation of the message.
+ * @param aCallbackThis The (optional) 'this' to use for your callback function.
+ * @param aCallback The callback function to invoke on completion of message
+ *     parsing or failure.  The first argument passed will be the nsIMsgDBHdr
+ *     you passed to this function.  The second argument will be the MimeMessage
+ *     instance resulting from the processing on success, and null on failure. 
+ */
+function MsgHdrToMimeMessage(aMsgHdr, aCallbackThis, aCallback) {
+  let msgURI = aMsgHdr.folder.getUriForMsg(aMsgHdr);
+  let msgService = gMessenger.messageServiceFromURI(msgURI);
+  
+  let streamListener = new CallbackStreamListener(aMsgHdr,
+                                                  aCallbackThis, aCallback);
+  
+  let streamURI = msgService.streamMessage(msgURI,
+                                           streamListener, // consumer
+                                           gMsgWindow, // nsIMsgWindow
+                                           dumbUrlListener, // nsIUrlListener
+                                           true, // have them create the converter
+      // additional uri payload, note that "header=" is prepended automatically 
+                                           "filter&emitter=js"); 
+}
+
+/**
+ * Let the jsmimeemitter provide us with results.  The poor emitter (if I am
+ *  understanding things correctly) is evaluated outside of the C.u.import
+ *  world, so if we were to import him, we would not see him, but rather a new
+ *  copy of him.  This goes for his globals, etc.  (and is why we live in this
+ *  file right here).  Also, it appears that the XPCOM JS wrappers aren't
+ *  magically unified so that we can try and pass data as expando properties
+ *  on things like the nsIUri instances either.  So we have the jsmimeemitter
+ *  import us and poke things into RESULT_RENDEVOUZ.  We put it here on this
+ *  function to try and be stealthy and avoid polluting the namespaces (or
+ *  encouraging bad behaviour) of our importers.
+ *
+ * If you can come up with a prettier way to shuttle this data, please do.
+ */
+MsgHdrToMimeMessage.RESULT_RENDEVOUZ = {};
+/**
+ * Someone with access to these globals needs to push them into us.
+ */
+MsgHdrToMimeMessage.initGlobals = function(aMessenger, aMsgWindow) {
+  gMessenger = aMessenger;
+  gMsgWindow = aMsgWindow;
+}
+
+/**
+ * @ivar partName The MIME part, ex "1.2.2.1".  The partName of a (top-level)
+ *     message is "1", its first child is "1.1", its second child is "1.2",
+ *     its first child's first child is "1.1.1", etc.
+ * @ivar headers Maps lower-cased header field names to a list of the values
+ *     seen for the given header.  Use get or getAll as convenience helpers.
+ * @ivar body The body of the message.
+ * @ivar messages A list of the sub-message children of this message.  Strict
+ *     MIME part hierarchy is not maintained; a sub-message's parent is the
+ *     closest sub-message above it.  Sub-messages can also be found in the
+ *     parts list, if you want a more strict traversal.
+ * @ivar parts The list of the MIME part children of this message.  Children
+ *     will be either MimeMessage instances, MimeMessageAttachment instances,
+ *     MimeContainer instances, or MimeUnknown instances.  The latter two are
+ *     the result of limitations in the Javascript representation generation  
+ *     at this time, combined with the need to most accurately represent the
+ *     MIME structure.
+ */
+function MimeMessage() {
+  this.partName = null;
+  this.headers = {};
+  this.body = "";
+
+  this.messages = [];
+  this.attachments = [];
+
+  this.parts = [];
+}
+
+MimeMessage.prototype = {
+  /**
+   * Look-up a header that should be present at most once.
+   *
+   * @param aHeaderName The header name to retrieve, case does not matter.
+   * @param aDefaultValue The value to return if the header was not found, null
+   *     if left unspecified.
+   * @return the value of the header if present, and the default value if not
+   *  (defaults to null).  If the header was present multiple times, the first
+   *  instance of the header is returned.  Use getAll if you want all of the
+   *  values for the multiply-defined header.
+   */
+  get: function MimeMessage_get(aHeaderName, aDefaultValue) {
+    if (aDefaultValue === undefined) {
+      aDefaultValue = null;
+    }
+    let lowerHeader = aHeaderName.toLowerCase();
+    if (lowerHeader in this.headers)
+      // we require that the list cannot be empty if present
+      return this.headers[lowerHeader][0];
+    else
+      return aDefaultValue;
+  },
+  /**
+   * Look-up a header that can be present multiple times.  Use get for headers
+   *  that you only expect to be present at most once.
+   *
+   * @param aHeaderName The header name to retrieve, case does not matter.
+   * @return An array containing the values observed, which may mean a zero
+   *     length array.
+   */
+  getAll: function MimeMessage_getAll(aHeaderName) {
+    let lowerHeader = aHeaderName.toLowerCase();
+    if (lowerHeader in this.headers)
+      return this.headers[lowerHeader];
+    else
+      return [];
+  },
+  /**
+   * @param aHeaderName Header name to test for its presence.
+   * @return true if the message has (at least one value for) the given header
+   *     name.
+   */
+  has: function MimeMessage_has(aHeaderName) {
+    let lowerHeader = aHeaderName.toLowerCase();
+    return lowerHeader in this.headers;
+  },
+  /**
+   * @return a list of all attachments contained in this message and all its
+   *     sub-messages.  Only MimeMessageAttachment instances will be present in
+   *     the list (no sub-messages).
+   */
+  allAttachments: function MimeMessage_allAttachments() {
+    let results = []; // messages are not attachments, don't include self
+    for (let iChild=0; iChild < this.parts.length; iChild++) {
+      let child = this.parts[iChild];
+      results = results.concat(child.allAttachments);
+    }
+    return results;
+  },
+  /**
+   * Convert the message and its hierarchy into a "pretty string".  The message
+   *  and each MIME part get their own line.  The string never ends with a
+   *  newline.  For a non-multi-part message, only a single line will be
+   *  returned.
+   * Messages have their subject displayed, attachments have their filename and
+   *  content-type (ex: image/jpeg) displayed.  "Filler" classes simply have
+   *  their class displayed.
+   */
+  prettyString: function MimeMessage_prettyString(aIndent) {
+    if (aIndent === undefined)
+      aIndent = ""; 
+    let nextIndent = aIndent + "  ";
+  
+    let s = "Message: " + this.headers.subject;
+    
+    for (let iPart=0; iPart < this.parts.length; iPart++) {
+      let part = this.parts[iPart];
+      s += "\n" + nextIndent + (iPart+1) + " " + part.prettyString(nextIndent);
+    }
+    
+    return s;
+  },
+};
+
+function MimeContainer(aPartName) {
+  this.partName = aPartName;
+  this.parts = [];
+}
+
+MimeContainer.prototype = {
+  allAttachments: function MimeContainer_allAttachments() {
+    let results = [];
+    for (let iChild=0; iChild < this.parts.length; iChild++) {
+      let child = this.parts[iChild];
+      results = results.concat(child.allAttachments);
+    }
+    return results;
+  },
+  prettyString: function MimeContainer_prettyString(aIndent) {
+    let nextIndent = aIndent + "  ";
+  
+    let s = "Container";
+    
+    for (let iPart=0; iPart < this.parts.length; iPart++) {
+      let part = this.parts[iPart];
+      s += "\n" + nextIndent + (iPart+1) + " " + part.prettyString(nextIndent);
+    }
+    
+    return s;
+  },
+}
+
+function MimeUnknown(aPartName) {
+  this.partName = aPartName;
+}
+
+MimeUnknown.prototype = {
+  allAttachments: function MimeUniknown_allAttachments() {
+    return []; // we are a leaf
+  },
+  prettyString: function MimeUnknown_prettyString(aIndent) {
+    return "Unknown";
+  },
+}
+
+function MimeMessageAttachment(aPartName, aName, aContentType, aUrl,
+                               aIsExternal) {
+  this.partName = aPartName;
+  this.name = aName;
+  this.contentType = aContentType;
+  this.url = aUrl;
+  this.isExternal = aIsExternal;
+  
+  this.fields = {};
+}
+
+MimeMessageAttachment.prototype = {
+  allAttachments: function MimeMessageAttachment_allAttachments() {
+    return [this]; // we are a leaf, so just us.
+  },
+  prettyString: function MimeMessageAttachment_prettyString(aIndent) {
+    return "Attachment: " + this.name + ", " + this.contentType;
+  },
+};