Merge gloda into comm-central
authorBenjamin Smedberg <benjamin@smedbergs.us>
Tue, 04 Nov 2008 13:18:20 -0500
changeset 1003 1f72e947eeb15896260fd4937ec3a1e647e10bf0
parent 816 13d566bc917639757cf5df84f9e71d406ebae39f (current diff)
parent 1002 4e7c04d63423dfecc7710173f6bafd9d8cf979c2 (diff)
child 1004 a79b923a9cba395cb3911b27c9599ffb8c997caf
push idunknown
push userunknown
push dateunknown
Merge gloda into comm-central
.hgtags
--- a/.hgtags
+++ b/.hgtags
@@ -3,8 +3,20 @@ 474f19a1b5fccec46bbeeeacd6f3cd368f3543a4
 d7ce1e64f1cc52003e3f3f326bd8f5260ce03733 SEAMONKEY_2_0a1_BUILD1
 d7ce1e64f1cc52003e3f3f326bd8f5260ce03733 SEAMONKEY_2_0a1_RELEASE
 7fe8dc6fc848247b3df68fb7c6cbcd7c4665d497 THUNDERBIRD_3_0a3_RELEASE
 7fe8dc6fc848247b3df68fb7c6cbcd7c4665d497 THUNDERBIRD_3_0a3_BUILD1
 841a3d525cd73daac28e3d49b016bb46e6fcccbd THUNDERBIRD_3_0a3_RELEASE
 841a3d525cd73daac28e3d49b016bb46e6fcccbd THUNDERBIRD_3_0a3_BUILD1
 0000000000000000000000000000000000000000 THUNDERBIRD_3_0a3_RELEASE
 0000000000000000000000000000000000000000 THUNDERBIRD_3_0a3_BUILD1
+93bb9e91d8cd4caa82602289230c380e10ceb3b1 gloda-milestone-0
+a3ff3e9b9edd130cebdf4ac48c7155265589a5d5 stable
+4b665f55dc96f043ac129be3f1c886546b5b9e6d gloda-milestone-1
+cb593cb6c3e57a01ac066c50362b37f8bdb2c2fc stable
+9f65871c06614dbec00b278cd60321713c01e028 stable
+c1902489c55f67799d41e7b032497ca04de6aa7c stable
+fe9ea5855aee1bfd0825edeaadc7547fd87504e4 stable
+8dfd8299e8a5f146d81040a844320e473e8a0fc2 stable
+3d52bc12f0f5d699a465432dc577cd479e12e0e7 stable-with-mods
+08c2e01d2acf8333311f497d050b22b01e72e460 stable-with-mods
+1936d4ec8044fd4cfdc636b43ec5e7488f86388c stable
+8e9fe32109b8a7232a5844320ca273bf5c3a4528 unstable
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/.project
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>gloda</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+	</buildSpec>
+	<natures>
+	</natures>
+</projectDescription>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/Makefile.in
@@ -0,0 +1,53 @@
+#
+# ***** 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 mozilla.org code.
+#
+# 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):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of 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 *****
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS		= modules components
+#DIRS        = components
+
+ifdef ENABLE_TESTS
+DIRS += test
+endif
+
+include $(topsrcdir)/config/rules.mk
new file mode 100755
--- /dev/null
+++ b/mailnews/db/gloda/build.sh
@@ -0,0 +1,128 @@
+#!/bin/bash
+# build.sh -- builds JAR and XPI files for mozilla extensions
+#   by Nickolay Ponomarev <asqueella@gmail.com>
+#   (original version based on Nathan Yergler's build script)
+# Most recent version is at <http://kb.mozillazine.org/Bash_build_script>
+
+# This script assumes the following directory structure:
+# ./
+#   chrome.manifest (optional - for newer extensions)
+#   install.rdf
+#   (other files listed in $ROOT_FILES)
+#
+#   content/    |
+#   locale/     |} these can be named arbitrary and listed in $CHROME_PROVIDERS
+#   skin/       |
+#
+#   defaults/   |
+#   components/ |} these must be listed in $ROOT_DIRS in order to be packaged
+#   ...         |
+#
+# It uses a temporary directory ./build when building; don't use that!
+# Script's output is:
+# ./$APP_NAME.xpi
+# ./$APP_NAME.jar  (only if $KEEP_JAR=1)
+# ./files -- the list of packaged files
+#
+# Note: It modifies chrome.manifest when packaging so that it points to 
+#       chrome/$APP_NAME.jar!/*
+
+#
+# default configuration file is ./config_build.sh, unless another file is 
+# specified in command-line. Available config variables:
+APP_NAME=          # short-name, jar and xpi files name. Must be lowercase with no spaces
+CHROME_PROVIDERS=  # which chrome providers we have (space-separated list)
+CLEAN_UP=          # delete the jar / "files" when done?       (1/0)
+ROOT_FILES=        # put these files in root of xpi (space separated list of leaf filenames)
+ROOT_DIRS=         # ...and these directories       (space separated list)
+BEFORE_BUILD=      # run this before building       (bash command)
+AFTER_BUILD=       # ...and this after the build    (bash command)
+
+if [ -z $1 ]; then
+  . ./config_build.sh
+else
+  . $1
+fi
+
+if [ -z $APP_NAME ]; then
+  echo "You need to create build config file first!"
+  echo "Read comments at the beginning of this script for more info."
+  exit;
+fi
+
+ROOT_DIR=`pwd`
+TMP_DIR=build
+
+#uncomment to debug
+#set -x
+
+# remove any left-over files from previous build
+rm -f $APP_NAME.jar $APP_NAME.xpi files
+rm -rf $TMP_DIR
+
+$BEFORE_BUILD
+
+mkdir --parents --verbose $TMP_DIR/chrome
+
+# generate the JAR file, excluding CVS and temporary files
+JAR_FILE=$TMP_DIR/chrome/$APP_NAME.jar
+echo "Generating $JAR_FILE..."
+for CHROME_SUBDIR in $CHROME_PROVIDERS; do
+  find $CHROME_SUBDIR -path '*CVS*' -prune -o -type f -print | grep -v \~ >> files
+done
+
+zip -0 -r $JAR_FILE `cat files`
+# The following statement should be used instead if you don't wish to use the JAR file
+#cp --verbose --parents `cat files` $TMP_DIR/chrome
+
+# prepare components and defaults
+echo "Copying various files to $TMP_DIR folder..."
+for DIR in $ROOT_DIRS; do
+  mkdir $TMP_DIR/$DIR
+  FILES="`find $DIR -path '*CVS*' -prune -o -type f -print | grep -v \~`"
+  echo $FILES >> files
+  cp --verbose --parents $FILES $TMP_DIR
+done
+
+# Copy other files to the root of future XPI.
+for ROOT_FILE in $ROOT_FILES install.rdf chrome.manifest; do
+  cp --verbose $ROOT_FILE $TMP_DIR
+  if [ -f $ROOT_FILE ]; then
+    echo $ROOT_FILE >> files
+  fi
+done
+
+cd $TMP_DIR
+
+if [ -f "chrome.manifest" ]; then
+  echo "Preprocessing chrome.manifest..."
+  # You think this is scary?
+  #s/^(content\s+\S*\s+)(\S*\/)$/\1jar:chrome\/$APP_NAME\.jar!\/\2/
+  #s/^(skin|locale)(\s+\S*\s+\S*\s+)(.*\/)$/\1\2jar:chrome\/$APP_NAME\.jar!\/\3/
+  #
+  # Then try this! (Same, but with characters escaped for bash :)
+  sed -i -r s/^\(content\\s+\\S*\\s+\)\(\\S*\\/\)$/\\1jar:chrome\\/$APP_NAME\\.jar!\\/\\2/ chrome.manifest
+  sed -i -r s/^\(skin\|locale\)\(\\s+\\S*\\s+\\S*\\s+\)\(.*\\/\)$/\\1\\2jar:chrome\\/$APP_NAME\\.jar!\\/\\3/ chrome.manifest
+
+  # (it simply adds jar:chrome/whatever.jar!/ at appropriate positions of chrome.manifest)
+fi
+
+# generate the XPI file
+echo "Generating $APP_NAME.xpi..."
+zip -r ../$APP_NAME.xpi *
+
+cd "$ROOT_DIR"
+
+echo "Cleanup..."
+if [ $CLEAN_UP = 0 ]; then
+  # save the jar file
+  mv $TMP_DIR/chrome/$APP_NAME.jar .
+else
+  rm ./files
+fi
+
+# remove the working files
+rm -rf $TMP_DIR
+echo "Done!"
+
+$AFTER_BUILD
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/chrome.manifest
@@ -0,0 +1,5 @@
+content	gloda	content/
+locale	gloda	en-US	locale/en-US/
+skin	gloda	classic/1.0	skin/
+overlay	chrome://messenger/content/messenger.xul	chrome://gloda/content/thunderbirdOverlay.xul
+resource gloda ./
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/components/Makefile.in
@@ -0,0 +1,50 @@
+#
+# ***** 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 mozilla.org code.
+#
+# 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):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of 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 *****
+
+DEPTH		= ../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = mailnewsglobaldb
+
+EXTRA_COMPONENTS = $(wildcard $(srcdir)/*.js)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/components/glautocomp.js
@@ -0,0 +1,494 @@
+/* ***** 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>
+ *   Mark Banner <bugzilla@standard8.plus.com>
+ *
+ * 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;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var LOG = null;
+
+var Gloda = null;
+var GlodaUtils = null;
+var MultiSuffixTree = null;
+var TagNoun = null;
+var FreeTagNoun = null;
+
+function ResultRowSingle(aItem, aCriteriaType, aCriteria, aExplicitNounID) {
+  this.nounID = aExplicitNounID || aItem.NOUN_ID;
+  this.nounDef = Gloda._nounIDToDef[this.nounID];
+  this.criteriaType = aCriteriaType;
+  this.criteria = aCriteria;
+  this.item = aItem;
+}
+ResultRowSingle.prototype = {
+  multi: false
+};
+
+function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
+  this.nounID = aNounID;
+  this.nounDef = Gloda._nounIDToDef[aNounID];
+  this.criteriaType = aCriteriaType;
+  this.criteria = aCriteria;
+  this.collection = aQuery.getCollection(this);
+  this.renderer = null;
+}
+ResultRowMulti.prototype = {
+  multi: true,
+  onItemsAdded: function(aItems) {
+    LOG.debug("RRM onItemsAdded: " + aItems.length + ": " + aItems);
+    if (this.renderer) {
+      LOG.debug("RRM rendering...");
+      for each (let [iItem, item] in Iterator(aItems)) {
+        LOG.debug("RRM ..." + item);
+        this.renderer.renderItem(item);
+      }
+    }
+  },
+  onItemsModified: function(aItems) {
+  },
+  onItemsRemoved: function(aItems) {
+  },
+  onQueryCompleted: function() {
+  }
+}
+
+function nsAutoCompleteGlodaResult(aListener, aCompleter, aString) {
+  this.listener = aListener;
+  this.completer = aCompleter;
+  this.searchString = aString;
+  this._results = [];
+  this._pendingCount = 0;
+  this._problem = false;
+  
+  this.wrappedJSObject = this;
+}
+nsAutoCompleteGlodaResult.prototype = {
+  getObjectAt: function(aIndex) {
+    return this._results[aIndex];
+  },
+  markPending: function ACGR_markPending(aCompleter) {
+    this._pendingCount++;
+  },
+  markCompleted: function ACGR_markCompleted(aCompleter) {
+    if (--this._pendingCount == 0) {
+      LOG.debug("Notifying completion.");
+      this.listener.onSearchResult(this.completer, this);
+    }
+  },
+  addRows: function ACGR_addRows(aRows) {
+    if (!aRows.length)
+      return;
+    LOG.debug("Adding " + aRows.length + " rows (" + this._pendingCount +
+              " jobs still pending)");
+    this._results.push.apply(this._results, aRows); 
+    this.listener.onSearchResult(this.completer, this);
+  },
+  // ==== nsIAutoCompleteResult
+  searchString: null,
+  get searchResult() {
+    if (this._problem)
+      return Ci.nsIAutoCompleteResult.RESULT_FAILURE;
+    if (this._results.length)
+      return (!this._pendingCount) ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
+                          : Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING;
+    else
+      return (!this._pendingCount) ? Ci.nsIAutoCompleteResult.RESULT_NOMATCH
+                          : Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING;
+  },
+  defaultIndex: -1,
+  errorDescription: null,
+  get matchCount() {
+    return (this._results === null) ? 0 : this._results.length;
+  },
+  // this is the lower text, (shows the url in firefox)
+  // we try and show the contact's name here.
+  getValueAt: function(aIndex) {
+    let thing = this._results[aIndex];
+    return thing.name || thing.value || thing.subject;
+  },
+  // rich uses this to be the "title".  it is the upper text
+  // we try and show the identity here.
+  getCommentAt: function(aIndex) {
+    let thing = this._results[aIndex];
+    if (thing.value) // identity
+      return thing.contact.name;
+    else
+      return thing.name || thing.subject;
+  },
+  // rich uses this to be the "type"
+  getStyleAt: function(aIndex) {
+    let row = this._results[aIndex];
+    if (row.multi)
+      return "gloda-multi";
+    else
+      return "gloda-single-" + row.nounDef.name;
+  },
+  // rich uses this to be the icon
+  getImageAt: function(aIndex) {
+    let thing = this._results[aIndex];
+    if (!thing.value)
+      return null;
+
+    let md5hash = GlodaUtils.md5HashString(thing.value);
+    let gravURL = "http://www.gravatar.com/avatar/" + md5hash +
+                                "?d=identicon&s=32&r=g";
+    return gravURL;
+  },
+  removeValueAt: function() {},
+
+  _stop: function() {
+  },
+};
+
+const MAX_POPULAR_CONTACTS = 200;
+
+/**
+ * Complete contacts/identities based on name/email.  Instant phase is based on
+ *  a suffix-tree built of popular contacts/identities.  Delayed phase relies
+ *  on a LIKE search of all known contacts.
+ */
+function ContactIdentityCompleter() {
+  // get all the contacts
+  let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
+  contactQuery.orderBy("-popularity").limit(MAX_POPULAR_CONTACTS);
+  this.contactCollection = contactQuery.getCollection(this, null);
+}
+ContactIdentityCompleter.prototype = {
+  _popularitySorter: function(a, b){ return b.popularity - a.popularity; },
+  complete: function ContactIdentityCompleter_complete(aResult, aString) {
+    if (aString.length < 3)
+      return false;
+
+    let matches;
+    if (this.suffixTree) {
+      matches = this.suffixTree.findMatches(aString.toLowerCase());
+      LOG.debug("CIC: Suffix Tree found " + matches.length + " matches.")
+    }
+    else
+      matches = [];
+
+    // let's filter out duplicates due to identity/contact double-hits by
+    //  establishing a map based on the contact id for these guys.
+    // let's also favor identities as we do it, because that gets us the
+    //  most accurate gravat, potentially
+    let contactToThing = {};
+    for (let iMatch = 0; iMatch < matches.length; iMatch++) {
+      let thing = matches[iMatch];
+      if (thing.NOUN_ID == Gloda.NOUN_CONTACT && !(thing.id in contactToThing))
+        contactToThing[thing.id] = thing;
+      else if (thing.NOUN_ID == Gloda.NOUN_IDENTITY)
+        contactToThing[thing.contactID] = thing;
+    }
+    // and since we can now map from contacts down to identities, map contacts
+    //  to the first identity for them that we find...
+    matches = [val.NOUN_ID == Gloda.NOUN_IDENTITY ? val : val.identities[0]
+               for each ([iVal, val] in Iterator(contactToThing))];
+
+    let rows = [new ResultRowSingle(match, "text", aResult.searchString)
+                for each ([iMatch, match] in Iterator(matches))];
+    aResult.addRows(rows);
+
+    // - match against database contacts / identities
+    let pending = {contactToThing: contactToThing, pendingCount: 2};
+    
+    LOG.debug("CIC: issuing contact LIKE query");
+    let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
+    contactQuery.nameLike(contactQuery.WILD, aString, contactQuery.WILD);
+    pending.contactColl = contactQuery.getCollection(this, aResult);
+
+    LOG.debug("CIC: issuing identity LIKE query");
+    let identityQuery = Gloda.newQuery(Gloda.NOUN_IDENTITY);
+    identityQuery.kind("email").valueLike(identityQuery.WILD, aString,
+        identityQuery.WILD);
+    pending.identityColl = identityQuery.getCollection(this, aResult);
+    
+    aResult._contactCompleterPending = pending;
+
+    return true;
+  },
+  onItemsAdded: function(aItems, aCollection) {
+  },
+  onItemsModified: function(aItems, aCollection) {
+  },
+  onItemsRemoved: function(aItems, aCollection) {
+  },
+  onQueryCompleted: function(aCollection) {
+    // handle the initial setup case...
+    if (aCollection.data == null) {
+      LOG.debug("CIC: Initial query found " + aCollection.items.length);
+      // cheat and explicitly add our own contact...
+      if (!(Gloda.myContact.id in this.contactCollection._idMap))
+        this.contactCollection._onItemsAdded([Gloda.myContact]);
+        
+      // the set of identities owned by the contacts is automatically loaded as part
+      //  of the contact loading...
+      // (but only if we actually have any contacts)
+      this.identityCollection =
+        this.contactCollection.subCollections[Gloda.NOUN_IDENTITY];
+
+      let contactNames = [(c.name.replace(" ", "").toLowerCase() || "x") for each
+                          ([, c] in Iterator(this.contactCollection.items))];
+      // if we had no contacts, we will have no identity collection!
+      let identityMails;
+      if (this.identityCollection)
+        identityMails = [i.value.toLowerCase() for each
+                         ([, i] in Iterator(this.identityCollection.items))];
+
+      this.suffixTree = new MultiSuffixTree(contactNames.concat(identityMails),
+        this.contactCollection.items.concat(this.identityCollection.items));
+      
+      return;
+    }
+    
+    LOG.debug("CIC: LIKE query found " + aCollection.items.length);
+    
+    // handle the completion case
+    let result = aCollection.data;
+    let pending = result._contactCompleterPending;
+    
+    if (--pending.pendingCount == 0) {
+      let possibleDudes = [];
+      
+      let contactToThing = pending.contactToThing;
+      
+      let items;
+      
+      // check identities first because they are better than contacts in terms
+      //  of display
+      items = pending.identityColl.items;
+      for (let iIdentity = 0; iIdentity < items.length; iIdentity++){
+        let identity = items[iIdentity];
+        if (!(identity.contactID in contactToThing)) {
+          contactToThing[identity.contactID] = identity;
+          possibleDudes.push(identity);
+          // augment the identity with its contact's popularity
+          identity.popularity = identity.contact.popularity;
+        }
+      }
+      items = pending.contactColl.items;
+      for (let iContact = 0; iContact < items.length; iContact++) {
+        let contact = items[iContact];
+        if (!(contact.id in contactToThing)) {
+          contactToThing[contact.id] = contact;
+          possibleDudes.push(contact.identities[0]);
+        }
+      }
+      
+      // sort in order of descending popularity
+      possibleDudes.sort(this._popularitySorter);
+      let rows = [new ResultRowSingle(dude, "text", result.searchString)
+                  for each ([iDude, dude] in Iterator(possibleDudes))];
+      result.addRows(rows);
+      result.markCompleted(this);
+      
+      // the collections no longer care about the result, make it clear.
+      delete pending.identityColl.data;
+      delete pending.contactColl.data;
+      // the result object no longer needs us or our data
+      delete result._contactCompleterPending;
+    }
+    else {
+      LOG.debug("ignoring... pending is still: " + pending.pendingCount);
+    }
+  }
+};
+
+/**
+ * Complete tags that are used on contacts.
+ */
+function ContactTagCompleter() {
+  FreeTagNoun.populateKnownFreeTags();
+  this._buildSuffixTree();
+  FreeTagNoun.addListener(this);
+}
+ContactTagCompleter.prototype = {
+  _buildSuffixTree: function() {
+    let tagNames = [], tags = [];
+    for (let [tagName, tag] in Iterator(FreeTagNoun.knownFreeTags)) {
+      tagNames.push(tagName.toLowerCase());
+      tags.push(tag);
+      LOG.debug("contact tag: " + tagName);
+    }
+    this._suffixTree = new MultiSuffixTree(tagNames, tags);
+    this._suffixTreeDirty = false;
+  },
+  onFreeTagAdded: function(aTag) {
+    this._suffixTreeDirty = true;
+  },
+  complete: function ContactTagCompleter_complete(aResult, aString) {
+    // now is not the best time to do this; have onFreeTagAdded use a timer.
+    if (this._suffixTreeDirty)
+      this._buildSuffixTree();
+    
+    if (aString.length < 2)
+      return false; // no async mechanism that will add new rows
+    
+    LOG.debug("Completing on contact tags...");
+    
+    tags = this._suffixTree.findMatches(aString.toLowerCase());
+    let rows = [];
+    for each (let [iTag, tag] in Iterator(tags)) {
+      let query = Gloda.newQuery(Gloda.NOUN_CONTACT);
+      LOG.debug("  checking for contact tag: " + tag.name);
+      query.freeTags(tag);
+      let resRow = new ResultRowMulti(Gloda.NOUN_CONTACT, "tag", tag.name,
+                                      query);
+      rows.push(resRow);
+    }
+    aResult.addRows(rows);
+    
+    return false; // no async mechanism that will add new rows
+  }
+};
+
+/**
+ * Complete tags that are used on messages
+ */
+function MessageTagCompleter() {
+  this._buildSuffixTree();
+}
+MessageTagCompleter.prototype = {
+  _buildSuffixTree: function MessageTagCompleter__buildSufficeTree() {
+    let tagNames = [], tags = [];
+    let tagArray = TagNoun.getAllTags();
+    for (let iTag = 0; iTag < tagArray.length; iTag++) {
+      let tag = tagArray[iTag];
+      tagNames.push(tag.tag.toLowerCase());
+      tags.push(tag);
+      LOG.debug("message tag: " + tag.tag);
+    }
+    this._suffixTree = new MultiSuffixTree(tagNames, tags);
+    this._suffixTreeDirty = false;
+  },
+  complete: function MessageTagCompleter_complete(aResult, aString) {
+    if (aString.length < 2)
+      return false;
+    
+    LOG.debug("Completing on message tags...");
+    
+    tags = this._suffixTree.findMatches(aString.toLowerCase());
+    let rows = [];
+    for each (let [, tag] in Iterator(tags)) {
+      LOG.debug(" found message tag: " + tag.tag);
+      let resRow = new ResultRowSingle(tag, "tag", tag.tag, TagNoun.id);
+      rows.push(resRow);
+    }
+    aResult.addRows(rows);
+    
+    return false; // no async mechanism that will add new rows
+  }
+};
+
+function nsAutoCompleteGloda() {
+  this.wrappedJSObject = this;
+
+  // set up our awesome globals!
+  if (Gloda === null) {
+    let loadNS = {};
+
+    Cu.import("resource://gloda/modules/public.js", loadNS);
+    Gloda = loadNS.Gloda;
+
+    Cu.import("resource://gloda/modules/utils.js", loadNS);
+    GlodaUtils = loadNS.GlodaUtils;
+    Cu.import("resource://gloda/modules/suffixtree.js", loadNS);
+    MultiSuffixTree = loadNS.MultiSuffixTree;
+    Cu.import("resource://gloda/modules/noun_tag.js", loadNS);
+    TagNoun = loadNS.TagNoun;
+    Cu.import("resource://gloda/modules/noun_freetag.js", loadNS);
+    FreeTagNoun = loadNS.FreeTagNoun;
+
+    Cu.import("resource://gloda/modules/log4moz.js", loadNS);
+    LOG = loadNS["Log4Moz"].Service.getLogger("gloda.autocomp");
+  }
+
+  LOG.debug("initializing completers");
+
+  this.completers = [];
+  
+  this.curResult = null;
+
+dump("init CIC\n");
+  LOG.debug("initializing ContactIdentityCompleter");
+  try {
+  this.completers.push(new ContactIdentityCompleter());
+  } catch (ex) {dump("CICEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
+dump("init CTC\n");
+  LOG.debug("initializing ContactTagCompleter");
+  this.completers.push(new ContactTagCompleter());
+dump("init MTC\n");
+  LOG.debug("initializing MessageTagCompleter");
+  try {
+  this.completers.push(new MessageTagCompleter());
+  } catch (ex) {dump("MTCEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
+  
+  LOG.debug("initialized completers");
+}
+
+nsAutoCompleteGloda.prototype = {
+  classDescription: "AutoCompleteGloda",
+  contractID: "@mozilla.org/autocomplete/search;1?name=gloda",
+  classID: Components.ID("{3bbe4d77-3f70-4252-9500-bc00c26f476c}"),
+  QueryInterface: XPCOMUtils.generateQI([
+      Components.interfaces.nsIAutoCompleteSearch]),
+
+  startSearch: function(aString, aParam, aResult, aListener) {
+    let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
+    // save this for hacky access to the search.  I somewhat suspect we simply
+    //  should not be using the formal autocomplete mechanism at all.
+    this.curResult = result;
+    
+    for each (let [iCompleter, completer] in Iterator(this.completers)) {
+      // they will return true if they have something pending.
+      if (completer.complete(result, aString))
+        result.markPending(completer);
+    }
+    
+    aListener.onSearchResult(this, result);
+  },
+
+  stopSearch: function() {
+  },
+};
+
+function NSGetModule(compMgr, fileSpec) {
+  return XPCOMUtils.generateModule([nsAutoCompleteGloda]);
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/components/jsmimeemitter.js
@@ -0,0 +1,456 @@
+/* ***** 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;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const kStateUnknown = 0;
+const kStateInHeaders = 1;
+const kStateInBody = 2;
+const kStateInAttachment = 3;
+
+/**
+ * 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.
+ * During the second pass, the libmime object model is traversed, generating
+ *  attachment notifications for all leaf nodes.  From our perspective, this
+ *  means file attachments and embedded messages (message/rfc822).  We use this
+ *  pass to create the attachment objects and properly structure the MIME part
+ *  hierarchy.  We extract the 'part name' (ex: 1.2.2.1) from the URL provided
+ *  with the attacment and rely on the fact that the attachment notifications
+ *  are generated as the result of an in-order traversal of the hierarchy.  We
+ *  generate MimeUnknown instances for apparent leaf nodes (nodes for whom
+ *  we did not hear about and do not know of any of their children), and
+ *  MimeContainer instances for apparent container nodes (nodes for whom we
+ *  know about one or more children).
+ */
+function MimeMessageEmitter() {
+  this._mimeMsg = {};
+  Cu.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 = [];
+  
+  this._partMap = {};
+  this._curPart = null;
+  this._curBodyPart = null;
+  
+  this._state = kStateUnknown;
+}
+
+MimeMessageEmitter.prototype = {
+  classDescription: "JS Mime Message Emitter",
+  classID: Components.ID("{8cddbbbc-7ced-46b0-a936-8cddd1928c24}"),
+  contractID: "@mozilla.org/gloda/jsmimeemitter;1",
+  
+  _partRE: new RegExp("^[^?]+\?(?:[^&]+&)*part=([^&]+)(?:&[^&]+)*$"),
+  
+  _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._url = aUrl;
+    this._curMsg = this._parentMsg = this._rootMsg = new this._mimeMsg.MimeMessage();
+    this._curMsg.partName = "";
+    this._partMap[""] = this._curMsg;
+    
+    this._mimeMsg.MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aUrl.spec] =
+      this._rootMsg;
+    
+    this._channel = aChannel;
+  },
+  
+  complete: function mime_emitter_complete() {
+    // dump("!!!!\n!!!!\n!!!!\n" + this._rootMsg.prettyString() + "\n");
+    this._url = null;
+    this._channel = null;
+    
+    this._inputStream = null;
+    this._outputStream = null;
+    
+    this._outputListener = null;
+
+    this._curMsg = this._parentMsg = this._messageStack = this._rootMsg = null;
+    this._messageIndex = null;
+    this._allSubMessages = null;
+    
+    this._partMap = null;
+    this._curPart = null;
+    this._curBodyPart = null;
+  },
+  
+  setPipe: function mime_emitter_setPipe(aInputStream, aOutputStream) {
+    this._inputStream = aInputStream;
+    this._outputStream = aOutputStream;
+  },
+  set outputListener(aListener) {
+    this._outputListener = aListener;
+  },
+  get outputListener() {
+    return this._outputListener;
+  }, 
+  
+  _beginPayload: function mime_emitter__beginPayload(aContentType, aIsPart) {
+    aContentType = aContentType.toLowerCase();
+    if (aContentType == "text/plain" || aContentType == "text/html") {
+      this._curBodyPart = new this._mimeMsg.MimeBody(aContentType, aIsPart);
+      this._parentMsg.bodyParts.push(this._curBodyPart);
+      this._curPart = aIsPart ? this._curBodyPart : null;
+    }
+    else if (aContentType == "message/rfc822") {
+      // startBody will take care of this
+      this._curPart = this._curBodyPart = null;
+    }
+    // this is going to fall-down with TNEF encapsulation and such, we really
+    //  need to just be consuming the object model.
+    else if (aContentType.indexOf("multipart/") == 0) {
+      this._curBodyPart = null;
+      // alternatives are always parts for part numbering purposes
+      this._curPart = aIsPart ? new this._mimeMsg.MimeContainer(aContentType)
+                              : null;
+    }
+    else {
+      this._curBodyPart = null;
+      this._curPart = aIsPart ?
+        new this._mimeMsg.MimeUnknown(aContentType, aIsPart) : null;
+    }
+  },
+  
+  // ----- Header Routines
+  startHeader: function mime_emitter_startHeader(aIsRootMailHeader,
+      aIsHeaderOnly, aMsgID, aOutputCharset) {
+    this._state = kStateInHeaders;
+    if (aIsRootMailHeader) {
+      this.updateCharacterSet(aOutputCharset);
+      // nothing to do curMsg-wise, already initialized.
+    }
+    else {
+      this._curMsg = new this._mimeMsg.MimeMessage();
+      
+      this._curMsg.partName = this._savedPartPath;
+      this._placePart(this._curMsg);
+      delete this._savedPartPath;
+      
+      this._parentMsg.messages.push(this._curMsg);
+      this._allSubMessages.push(this._curMsg);
+    }
+  },
+  addHeaderField: function mime_emitter_addHeaderField(aField, aValue) {
+    if (this._state == kStateInBody) {
+      aField = aField.toLowerCase();
+      let indexSemi = aValue.indexOf(";");
+      if (indexSemi >= 0)
+        aValue = aValue.substring(0, indexSemi);
+      if (aField == "content-type")
+        this._beginPayload(aValue, true);
+      else if (aField == "x-jsemitter-part-path") {
+        if (this._curPart) {
+          this._curPart.partName = aValue;
+          this._placePart(this._curPart);
+        }
+        else
+          this._savedPartPath = aValue;
+      }
+      return;
+    }
+    if (this._state != kStateInHeaders)
+      return;
+    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;
+    }
+    */
+  },
+  
+  /**
+   * Place a part in its proper location.  We know that we are called as a
+   *  result of in-order traversal, so this is wildly easy to deal with.
+   */
+  _placePart: function(aPart) {
+    let partName = aPart.partName;
+    this._partMap[partName] = aPart;
+    let parentName = partName.substring(0, partName.lastIndexOf("."));
+    let parentPart = this._partMap[parentName];
+    parentPart.parts.push(aPart);
+  },
+  
+  /**
+   * In the case of attachments, we need to replace an existing part with a
+   *  more representative part...
+   */
+  _replacePart: function(aPart) {
+    let partName = aPart.partName;
+    this._partMap[partName] = aPart;
+    
+    let parentName = partName.substring(0, partName.lastIndexOf("."));
+    let parentPart = this._partMap[parentName];
+    
+    let childNamePart = partName.substring(partName.lastIndexOf(".")+1);
+    let childIndex = parseInt(childNamePart) - 1;
+    
+    let oldPart = parentPart.parts[childIndex];
+    parentPart.parts[childIndex] = aPart;
+    aPart.parts = oldPart.parts;
+
+    // - remove it if it was a body part.  This can happen for text/plain
+    //  attachments.  Like patches.
+    // (climb the parents until we find a message/bodyparts holder...)
+    while (parentPart.partName && !parentPart.bodyParts) {
+      parentName = parentName.substring(0, parentName.lastIndexOf("."));
+      parentPart = this._partMap[parentName];
+    }
+    if (parentPart.bodyParts && parentPart.bodyParts.indexOf(oldPart) >= 0)
+      parentPart.bodyParts.splice(parentPart.bodyParts.indexOf(oldPart), 1);
+  },
+  
+  /**
+   * 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;
+
+    // for parts that should exist, try and find them in the part map, otherwise
+    //  create MimeUnknowns
+    while (curIndex > aParent.parts.length) {
+      let tempPath = aPathSoFar + "." + aParent.parts.length;
+      if (tempPath in this._partMap)
+        aParent.parts.push(this._partMap[tempPath]);
+      else {
+        let newPart = new this._mimeMsg.MimeUnknown("unknown/unknown", true);
+        newPart.partName = tempPath;
+        aParent.parts.push(newPart);
+      }
+    }
+    
+    // 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
+        if (newPathSoFar in this._partMap)
+          aParent.parts.push(this._partMap[newPathSoFar]);
+        else {
+          let newPart = new this._mimeMsg.MimeContainer("multipart/unknown",
+                                                        true);
+          newPart.partName = newPathSoFar;
+          aParent.parts.push(newPart);
+        }
+      }
+      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) {
+    this._state = kStateInAttachment;
+    // 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") {
+      // we already have all we need to know about the message, ignore it
+      return;
+    }
+    else {
+      // create the attachment
+      part = new this._mimeMsg.MimeMessageAttachment(partName,
+          aName, aContentType, aUrl, aNotDownloaded);
+    }
+    
+    if (part.isRealAttachment) {
+      // replace the existing part with the attachment...
+      this._replacePart(part);
+    }
+  },
+  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._state = kStateInBody;
+    
+    this._messageStack.push(this._curMsg);
+    this._parentMsg = this._curMsg;
+
+    // begin payload processing
+    let contentType = this._curMsg.get("content-type", "text/plain");
+    let indexSemi = contentType.indexOf(";");
+    if (indexSemi >= 0)
+      contentType = contentType.substring(0, indexSemi);
+    this._beginPayload(contentType, true);
+    if (this._parentMsg.partName == "")
+      this._curPart.partName = "1";
+    else
+      this._curPart.partName = this._curMsg.partName + ".1";
+    this._placePart(this._curPart);
+  },
+  
+  writeBody: function mime_emitter_writeBody(aBuf, aSize, aOutAmountWritten) {
+    if (this._curBodyPart)
+      this._curBodyPart.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);
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/config_build.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# Build config for build.sh
+APP_NAME=gloda
+CHROME_PROVIDERS="content locale skin"
+CLEAN_UP=1
+ROOT_FILES=
+ROOT_DIRS="defaults modules"
+BEFORE_BUILD=
+AFTER_BUILD=
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/glodacomplete.css
@@ -0,0 +1,42 @@
+textbox[type="glodacomplete"] {
+  -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete");
+}
+
+panel[type="glodacomplete-richlistbox"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#glodacomplete-rich-result-popup");
+}
+
+.autocomplete-richlistbox {
+  -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistbox");
+  -moz-user-focus: ignore;
+  -moz-appearance: none;
+}
+
+.autocomplete-richlistbox > scrollbox {
+  overflow-x: hidden !important;
+}
+
+.autocomplete-richlistitem[type="gloda-single-tag"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-tag-item");
+  overflow: -moz-hidden-unscrollable;
+}
+
+.autocomplete-richlistitem[type="gloda-single-identity"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-identity-item");
+  -moz-box-orient: vertical;
+  overflow: -moz-hidden-unscrollable;
+}
+
+richlistitem[type="gloda-contact-chunk"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-contact-chunk");
+  -moz-box-orient: vertical;
+  overflow: -moz-hidden-unscrollable;
+}
+
+.autocomplete-richlistitem[type="gloda-multi"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-multi-item");
+  -moz-box-orient: vertical;
+  overflow: -moz-hidden-unscrollable;
+}
+
+/* .autocomplete-history-dropmarker wants to be optional, but we don't care */
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/glodacomplete.xml
@@ -0,0 +1,624 @@
+<?xml version="1.0"?>
+
+<!-- ***** 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 mozilla.org browser.
+   -
+   - The Initial Developer of the Original Code is
+   - Joe Hewitt.
+   - Portions created by the Initial Developer are Copyright (C) 2003
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   - Pierre Chanial (p_ch@verizon.net)
+   - Dean Tessman   (dean_tessman@hotmail.com)
+   - Masayuki Nakano (masayuki@d-toybox.com)
+   - Pamela Greene (pamg.bugs@gmail.com)
+   - Edward Lee (edward.lee@engineering.uiuc.edu)
+   - 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 LGPL or the GPL. 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 ***** -->
+
+<bindings id="autocompleteBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:html="http://www.w3.org/1999/xhtml"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl">
+
+  <binding id="glodacomplete-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
+    <implementation implements="nsIAutoCompletePopup">
+      <method name="_appendCurrentResult">
+        <body>
+          <![CDATA[
+          var controller = this.mInput.controller;
+
+          // Process maxRows per chunk to improve performance and user experience
+          for (let i = 0; i < this.maxRows; i++) {
+            if (this._currentIndex >= this._matchCount)
+              return;
+
+            var existingItemsCount = this.richlistbox.childNodes.length;
+            var item;
+
+            // trim the leading/trailing whitespace
+            var trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
+
+            // Unescape the URI spec for showing as an entry in the popup
+            let url = Components.classes["@mozilla.org/intl/texttosuburi;1"].
+              getService(Components.interfaces.nsITextToSubURI).
+              unEscapeURIForUI("UTF-8", controller.getValueAt(this._currentIndex));
+
+            if (this._currentIndex < existingItemsCount) {
+              // re-use the existing item
+              item = this.richlistbox.childNodes[this._currentIndex];
+
+              // Completely re-use the existing richlistitem if it's the same
+              if (item.getAttribute("text") == trimmedSearchString &&
+                  item.getAttribute("url") == url) {
+                item.collapsed = false;
+                this._currentIndex++;
+                continue;
+              }
+            }
+            else {
+              // need to create a new item
+              item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem");
+            }
+            
+            var glodaCompleter =  Components.
+              classes["@mozilla.org/autocomplete/search;1?name=gloda"].
+              getService(). //Components.interfaces.nsIAutoCompleteSearch)
+              wrappedJSObject;
+            var result = glodaCompleter.curResult;
+
+            // set these attributes before we set the class
+            // so that we can use them from the contructor
+            var row = result.getObjectAt(this._currentIndex);
+            var obj = row.item;
+            item.setAttribute("text", trimmedSearchString);
+            item.setAttribute("type", result.getStyleAt(this._currentIndex));
+
+            item.row = row;
+
+            if (this._currentIndex < existingItemsCount) {
+              // re-use the existing item
+              item._adjustAcItem();
+              item.collapsed = false;
+            }
+            else {
+              // set the class at the end so we can use the attributes
+              // in the xbl constructor
+              item.className = "autocomplete-richlistitem";
+              this.richlistbox.appendChild(item);
+            }
+
+            this._currentIndex++;
+          }
+
+          // yield after each batch of items so that typing the url bar is responsive
+          setTimeout(function (self) { self._appendCurrentResult(); }, 0, this);
+        ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <!-- This is autocomplete.xml's autocomplete-richlistitem duplicated and
+       modified to include its useful helper functions, but eliminating anything
+       that assumes specific content sub-items.  Namely, url/title/etc.  -->
+  <binding id="glodacomplete-base-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <field name="_boundaryCutoff">null</field>
+
+      <property name="boundaryCutoff" readonly="true">
+        <getter>
+          <![CDATA[
+          if (!this._boundaryCutoff) {
+            this._boundaryCutoff =
+              Components.classes["@mozilla.org/preferences-service;1"].
+              getService(Components.interfaces.nsIPrefBranch).
+              getIntPref("toolkit.autocomplete.richBoundaryCutoff");
+          }
+          return this._boundaryCutoff;
+          ]]>
+        </getter>
+      </property>
+
+      <method name="_getBoundaryIndices">
+        <parameter name="aText"/>
+        <parameter name="aSearchTokens"/>
+        <body>
+          <![CDATA[
+          // Short circuit for empty search ([""] == "")
+          if (aSearchTokens == "")
+            return [0, aText.length];
+
+          // Find which regions of text match the search terms
+          let regions = [];
+          for each (let search in aSearchTokens) {
+            let matchIndex;
+            let startIndex = 0;
+            let searchLen = search.length;
+
+            // Find all matches of the search terms, but stop early for perf
+            let lowerText = aText.toLowerCase().substr(0, this.boundaryCutoff);
+            while ((matchIndex = lowerText.indexOf(search, startIndex)) >= 0) {
+              // Start the next search from where this one finished
+              startIndex = matchIndex + searchLen;
+              regions.push([matchIndex, startIndex]);
+            }
+          }
+
+          // Sort the regions by start position then end position
+          regions = regions.sort(function(a, b) let (start = a[0] - b[0])
+            start == 0 ? a[1] - b[1] : start);
+
+          // Generate the boundary indices from each region
+          let start = 0;
+          let end = 0;
+          let boundaries = [];
+          let len = regions.length;
+          for (let i = 0; i < len; i++) {
+            // We have a new boundary if the start of the next is past the end
+            let region = regions[i];
+            if (region[0] > end) {
+              // First index is the beginning of match
+              boundaries.push(start);
+              // Second index is the beginning of non-match
+              boundaries.push(end);
+
+              // Track the new region now that we've stored the previous one
+              start = region[0];
+            }
+
+            // Push back the end index for the current or new region
+            end = Math.max(end, region[1]);
+          }
+
+          // Add the last region
+          boundaries.push(start);
+          boundaries.push(end);
+
+          // Put on the end boundary if necessary
+          if (end < aText.length)
+            boundaries.push(aText.length);
+
+          // Skip the first item because it's always 0
+          return boundaries.slice(1);
+          ]]>
+        </body>
+      </method>
+
+      <method name="_getSearchTokens">
+        <parameter name="aSearch"/>
+        <body>
+          <![CDATA[
+          let search = aSearch.toLowerCase();
+          return search.split(/\s+/);
+          ]]>
+        </body>
+      </method>
+
+      <method name="_needsAlternateEmphasis">
+        <parameter name="aText"/>
+        <body>
+          <![CDATA[
+          for (let i = aText.length; --i >= 0; ) {
+            let charCode = aText.charCodeAt(i);
+            // Arabic, Syriac, Indic languages are likely to have ligatures
+            // that are broken when using the main emphasis styling
+            if (0x0600 <= charCode && charCode <= 0x109F)
+              return true;
+          }
+
+          return false;
+          ]]>
+        </body>
+      </method>
+
+      <method name="_setUpDescription">
+        <parameter name="aDescriptionElement"/>
+        <parameter name="aText"/>
+        <body>
+          <![CDATA[
+          // Get rid of all previous text
+          while (aDescriptionElement.hasChildNodes())
+            aDescriptionElement.removeChild(aDescriptionElement.firstChild);
+
+          // Get the indices that separate match and non-match text
+          let search = this.getAttribute("text");
+          let tokens = this._getSearchTokens(search);
+          let indices = this._getBoundaryIndices(aText, tokens);
+
+          // If we're searching for something that needs alternate emphasis,
+          // we'll need to check the text that we match
+          let checkAlt = this._needsAlternateEmphasis(search);
+
+          let next;
+          let start = 0;
+          let len = indices.length;
+          // Even indexed boundaries are matches, so skip the 0th if it's empty
+          for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
+            next = indices[i];
+            let text = aText.substr(start, next - start);
+            start = next;
+
+            if (i % 2 == 0) {
+              // Emphasize the text for even indices
+              let span = aDescriptionElement.appendChild(
+                document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+              span.className = checkAlt && this._needsAlternateEmphasis(text) ?
+                "ac-emphasize-alt" : "ac-emphasize-text";
+              span.textContent = text;
+            } else {
+              // Otherwise, it's plain text
+              aDescriptionElement.appendChild(document.createTextNode(text));
+            }
+          }
+          ]]>
+        </body>
+      </method>
+
+      <method name="_setUpOverflow">
+        <parameter name="aParentBox"/>
+        <parameter name="aEllipsis"/>
+        <body>
+          <![CDATA[
+          // Hide the ellipsis incase there's just enough to not underflow
+          aEllipsis.hidden = true;
+
+          // Start with the parent's width and subtract off its children
+          let tooltip = [];
+          let children = aParentBox.childNodes;
+          let widthDiff = aParentBox.boxObject.width;
+
+          for (let i = 0; i < children.length; i++) {
+            // Only consider a child if it actually takes up space
+            let childWidth = children[i].boxObject.width;
+            if (childWidth > 0) {
+              // Subtract a little less to account for subpixel rounding
+              widthDiff -= childWidth - .5;
+
+              // Add to the tooltip if it's not hidden and has text
+              let childText = children[i].textContent;
+              if (childText)
+                tooltip.push(childText);
+            }
+          }
+
+          // If the children take up more space than the parent.. overflow!
+          if (widthDiff < 0) {
+            // Re-show the ellipsis now that we know it's needed
+            aEllipsis.hidden = false;
+
+            // Separate text components with a ndash --
+            aParentBox.tooltipText = tooltip.join(" \u2013 ");
+          }
+          ]]>
+        </body>
+      </method>
+
+      <method name="_doUnderflow">
+        <parameter name="aName"/>
+        <body>
+          <![CDATA[
+          // Hide the ellipsis right when we know we're underflowing instead of
+          // waiting for the timeout to trigger the _setUpOverflow calculations
+          this[aName + "Box"].tooltipText = "";
+          this[aName + "OverflowEllipsis"].hidden = true;
+          ]]>
+        </body>
+      </method>
+
+    </implementation>
+  </binding>
+
+  <binding id="gloda-single-tag-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+    <content orient="vertical">
+      <xul:description anonid="explanation"/>
+    </content>
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+            this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            return "tag " + this.row.item.tag;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          this._explanation.value = "messages tagged " + this.row.item.tag;
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <binding id="gloda-single-identity-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+    <content>
+      <xul:hbox>
+	    <xul:image anonid="picture"/>
+	    <xul:vbox>
+	      <xul:hbox>
+	        <xul:hbox anonid="name-box" class="ac-title" flex="1"
+	                  onunderflow="_doUnderflow('_name');">
+	          <xul:description anonid="name" class="ac-normal-text ac-comment"
+	                           xbl:inherits="selected"/>
+	        </xul:hbox>
+	        <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
+	                   class="ac-ellipsis-after ac-comment" hidden="true"/>
+	      </xul:hbox>
+	      <xul:hbox>
+	        <xul:hbox anonid="identity-box" class="ac-url" flex="1"
+	                  onunderflow="_doUnderflow('_identity');">
+	          <xul:description anonid="identity" class="ac-normal-text ac-url-text"
+	                           xbl:inherits="selected"/>
+	        </xul:hbox>
+	        <xul:label anonid="identity-overflow-ellipsis" xbl:inherits="selected"
+	                   class="ac-ellipsis-after ac-url-text" hidden="true"/>
+	        <xul:image anonid="type-image" class="ac-type-icon"/>
+	      </xul:hbox>
+	    </xul:vbox>
+	  </xul:hbox>
+    </content>
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+            let ellipsis = "\u2026";
+            try {
+              ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
+                getService(Components.interfaces.nsIPrefBranch).
+                getComplexValue("intl.ellipsis",
+                  Components.interfaces.nsIPrefLocalizedString).data;
+            } catch (ex) {
+              // Do nothing.. we already have a default
+            }
+
+            this._identityOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "identity-overflow-ellipsis");
+            this._nameOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "name-overflow-ellipsis");
+
+            this._identityOverflowEllipsis.value = ellipsis;
+            this._nameOverflowEllipsis.value = ellipsis;
+
+            this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image");
+
+            this._identityBox = document.getAnonymousElementByAttribute(this, "anonid", "identity-box");
+            this._identity = document.getAnonymousElementByAttribute(this, "anonid", "identity");
+
+            this._nameBox = document.getAnonymousElementByAttribute(this, "anonid", "name-box");
+            this._name = document.getAnonymousElementByAttribute(this, "anonid", "name");
+            
+            this._picture = document.getAnonymousElementByAttribute(this, "anonid", "picture");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            var identity = this.row.item;
+            return identity.accessibleLabel;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          var identity = this.row.item;
+          
+          if (identity == null)
+            return;
+          
+          // I guess we should get the picture size from CSS or something?
+          this._picture.src = identity.pictureURL(32);
+          
+          // Emphasize the matching search terms for the description
+          this._setUpDescription(this._name, identity.contact.name);
+          this._setUpDescription(this._identity, identity.value);
+
+          // Set up overflow on a timeout because the contents of the box
+          // might not have a width yet even though we just changed them
+          setTimeout(this._setUpOverflow, 0, this._nameBox, this._nameOverflowEllipsis);
+          setTimeout(this._setUpOverflow, 0, this._identityBox, this._identityOverflowEllipsis);
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <binding id="gloda-contact-chunk" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+    <content orient="horizontal">
+      <xul:image anonid="picture"/>
+      <xul:vbox>
+        <xul:hbox>
+          <xul:hbox anonid="name-box" class="ac-title" flex="1"
+                    onunderflow="_doUnderflow('_name');">
+            <xul:description anonid="name" class="ac-normal-text ac-comment"
+                             xbl:inherits="selected"/>
+          </xul:hbox>
+          <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
+                     class="ac-ellipsis-after ac-comment" hidden="true"/>
+        </xul:hbox>
+        <xul:hbox>
+          <xul:hbox anonid="identity-box" class="ac-url" flex="1"
+                    onunderflow="_doUnderflow('_identity');">
+            <xul:description anonid="identity" class="ac-normal-text ac-url-text"
+                             xbl:inherits="selected"/>
+          </xul:hbox>
+          <xul:label anonid="identity-overflow-ellipsis" xbl:inherits="selected"
+                     class="ac-ellipsis-after ac-url-text" hidden="true"/>
+          <xul:image anonid="type-image" class="ac-type-icon"/>
+        </xul:hbox>
+      </xul:vbox>
+    </content>
+    <implementation>
+      <constructor>
+        <![CDATA[
+            let ellipsis = "\u2026";
+            try {
+              ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
+                getService(Components.interfaces.nsIPrefBranch).
+                getComplexValue("intl.ellipsis",
+                  Components.interfaces.nsIPrefLocalizedString).data;
+            } catch (ex) {
+              // Do nothing.. we already have a default
+            }
+
+            this._identityOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "identity-overflow-ellipsis");
+            this._nameOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "name-overflow-ellipsis");
+
+            this._identityOverflowEllipsis.value = ellipsis;
+            this._nameOverflowEllipsis.value = ellipsis;
+
+            this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image");
+
+            this._identityBox = document.getAnonymousElementByAttribute(this, "anonid", "identity-box");
+            this._identity = document.getAnonymousElementByAttribute(this, "anonid", "identity");
+
+            this._nameBox = document.getAnonymousElementByAttribute(this, "anonid", "name-box");
+            this._name = document.getAnonymousElementByAttribute(this, "anonid", "name");
+            
+            this._picture = document.getAnonymousElementByAttribute(this, "anonid", "picture");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            var identity = this.obj;
+            return identity.accessibleLabel;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          var contact = this.obj;
+          
+          if (contact == null)
+            return;
+          
+          var identity = contact.identities[0];
+          
+          // I guess we should get the picture size from CSS or something?
+          this._picture.src = identity.pictureURL(32);
+          
+          // Emphasize the matching search terms for the description
+          this._setUpDescription(this._name, contact.name);
+          this._setUpDescription(this._identity, identity.value);
+
+          // Set up overflow on a timeout because the contents of the box
+          // might not have a width yet even though we just changed them
+          setTimeout(this._setUpOverflow, 0, this._nameBox, this._nameOverflowEllipsis);
+          setTimeout(this._setUpOverflow, 0, this._identityBox, this._identityOverflowEllipsis);
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <binding id="gloda-multi-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+    <content orient="vertical">
+      <xul:description anonid="explanation"/>
+      <xul:hbox anonid="identity-holder" flex="1">
+      </xul:hbox>
+    </content>
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+            this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+            this._identityHolder = document.getAnonymousElementByAttribute(this, "anonid", "identity-holder");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            return this._explanation.value;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="renderItem">
+        <parameter name="aObj"/>
+        <body>
+          var node = document.createElementNS(
+            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+            "richlistitem");
+          
+          node.obj = aObj;
+          node.setAttribute("type",
+                            "gloda-" + this.row.nounDef.name + "-chunk");
+          
+          this._identityHolder.appendChild(node);
+        </body>
+      </method>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          // clear out any lingering children
+          while (this._identityHolder.hasChildNodes())
+            this._identityHolder.removeChild(this._identityHolder.firstChild);
+          
+          var row = this.row;
+          if (row == null)
+            return;
+          
+          this._explanation.value = row.nounDef.name + "s " +
+            row.criteriaType + "ed " + row.criteria;
+          
+          // render anyone already in there
+          for each (let item in row.collection.items) {
+            this.renderItem(item);
+          }
+          // listen up, yo.
+          row.renderer = this;
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+
+</bindings>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/options.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ***** 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 ***** -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<!DOCTYPE prefwindow SYSTEM "chrome://gloda/locale/prefwindow.dtd">
+<prefwindow id="glodaPreferences" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" title="&prefwindow.title;">
+  <prefpane id="pane1" label="&pane1.title;">
+    <preferences>
+      <preference id="boolpref1" name="extensions.gloda.boolpref" type="bool"/>
+      <preference id="intpref1" name="extensions.gloda.intpref" type="int"/>
+      <preference id="stringpref1" name="extensions.gloda.stringpref" type="string"/> <!-- note that this is only an ASCII string - use unichar for unicode strings -->
+    </preferences>
+    <checkbox id="checkboolpref" preference="boolpref1" label="&checkboolpref.label;" accesskey="&checkboolpref.accesskey;"/>
+    <label accesskey="&intpref.accesskey;" control="textintpref">&intpref.label;</label><textbox id="textintpref" preference="intpref1"/>
+    <label accesskey="&stringpref.accesskey;" control="textstringpref">&stringpref.label;</label><textbox id="textstringpref" preference="stringpref1"/>
+  </prefpane>
+</prefwindow>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/overlay.js
@@ -0,0 +1,68 @@
+/* ***** 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 ***** */
+
+// get the core
+Components.utils.import("resource://gloda/modules/public.js");
+
+var gloda = {
+  _mimeMsg: {},
+  
+  onLoad: function() {
+    // initialization code
+    this.initialized = true;
+    this.strings = document.getElementById("gloda-strings");
+  },
+  onIndexEverythingCommand: function(e) {
+    GlodaIndexer.indexEverything();
+  },
+  onIndexAddressBookCommand: function(e) {
+    // TODO support address-book indexing or something.
+  },  
+  indexSelectedMessages: function () {
+    var dbView = GetDBView();
+    var indices = GetSelectedIndices(dbView);
+    var toindex = [];
+    for (var iIndex=0; iIndex < indices.length; iIndex++) {
+      var actualIndex = indices[iIndex];
+      var folder = dbView.getFolderForViewIndex(actualIndex);
+      var msgKey = dbView.getKeyAt(actualIndex);
+      toindex.push([folder, msgKey]);
+    }
+    GlodaIndexer.indexMessages(toindex);
+  },
+};
+window.addEventListener("load", function(e) { gloda.onLoad(e); }, false);
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/thunderbirdOverlay.xul
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ***** 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 ***** -->
+
+<!DOCTYPE overlay SYSTEM "chrome://gloda/locale/gloda.dtd">
+<overlay id="gloda-overlay"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script src="overlay.js"/>
+  <stringbundleset id="stringbundleset">
+    <stringbundle id="gloda-strings" src="chrome://gloda/locale/gloda.properties"/>
+  </stringbundleset>
+
+  <menupopup id="taskPopup">
+    <menuitem id="gloda-hello" label="&glodaIndexEverything.label;" 
+              oncommand="gloda.onIndexEverythingCommand(event);"/>
+    <menuitem id="gloda-hello" label="&glodaIndexAddressBook.label;" 
+              oncommand="gloda.onIndexAddressBookCommand(event);"/>
+  </menupopup>
+  
+  <popup id="folderPaneContext">
+    <menuseparator id="folderPaneContext-sep-gloda"/>
+    <menuitem id="folderPaneContext-gloda-index"
+              label="&folderContextGlodaIndex.label;"
+              accesskey="&folderContextGlodaIndex.accesskey;"
+              oncommand="GlodaIndexer.indexFolderByURI(GetSelectedFolderURI());"/>
+  </popup>
+
+  <popup id="threadPaneContext">
+    <menuseparator id="threadPaneContext-sep-gloda"/>
+    <menuitem id="threadPaneContext-gloda-index"
+              label="&threadContextGlodaIndex.label;"
+              accesskey="&threadContextGlodaIndex.accesskey;"
+              oncommand="gloda.indexSelectedMessages();"/>
+  </popup>
+
+</overlay>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/defaults/preferences/gloda.js
@@ -0,0 +1,5 @@
+pref("extensions.gloda.boolpref", false);
+pref("extensions.gloda.intpref", 0);
+pref("extensions.gloda.stringpref", "A string");
+// See http://kb.mozillazine.org/Localize_extension_descriptions
+pref("extensions.gloda@mozillamessaging.com.description", "chrome://gloda/locale/gloda.properties");
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/install.rdf
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>gloda@mozillamessaging.com</em:id>
+    <em:name>Global Database</em:name>
+    <em:version>0.0.1</em:version>
+    <em:creator>Mozilla Messaging, Inc.</em:creator>
+    <em:contributor>Andrew Sutherland &lt;asutherland@asutherland.org&gt;</em:contributor>
+    <em:description>Adds a global database to Thunderbird</em:description>
+    <em:optionsURL>chrome://gloda/content/options.xul</em:optionsURL>
+    <em:targetApplication>
+      <Description>
+        <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id> <!-- thunderbird -->
+        <em:minVersion>3.0a2pre</em:minVersion>
+        <em:maxVersion>3.0.*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/jar.mn
@@ -0,0 +1,11 @@
+gloda.jar:
+% content gloda   %content/
+% locale  gloda   en-US   %locale/en-US/
+% overlay chrome://messenger/content/messenger.xul    chrome://gloda/content/thunderbirdOverlay.xul
+% resource gloda ../
+  locale/en-US/gloda.dtd           (locale/en-US/gloda.dtd)
+  locale/en-US/gloda.properties    (locale/en-US/gloda.properties)
+  content/overlay.js               (content/overlay.js)
+  content/thunderbirdOverlay.xul   (content/thunderbirdOverlay.xul)
+  content/glodacomplete.css        (content/glodacomplete.css)
+  content/glodacomplete.xml        (content/glodacomplete.xml)
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/locale/en-US/gloda.dtd
@@ -0,0 +1,6 @@
+<!ENTITY glodaIndexEverything.label "Index Everything">
+<!ENTITY glodaIndexAddressBook.label "Index Addressbook">
+<!ENTITY folderContextGlodaIndex.label "Gloda Index">
+<!ENTITY folderContextGlodaIndex.accesskey "x">
+<!ENTITY threadContextGlodaIndex.label "Gloda Index">
+<!ENTITY threadContextGlodaIndex.accesskey "x">
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/locale/en-US/gloda.properties
@@ -0,0 +1,8 @@
+prefMessage=Int Pref Value: %d
+extensions.gloda.description=Adds a global database to Thunderbird
+shutdownTaskName=Global Database Indexer
+actionIdle=Idle
+actionIndexing=Indexing
+actionDeindexing=De-Indexing
+actionMoving=Move Processing
+messageIndexingExplanation=some messages
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/locale/en-US/prefwindow.dtd
@@ -0,0 +1,8 @@
+<!ENTITY prefwindow.title "Global Database Has No Preferences">
+<!ENTITY pane1.title "Global Database Has No Preferences">
+<!ENTITY checkboolpref.label "I like cookies">
+<!ENTITY checkboolpref.accesskey "l">
+<!ENTITY intpref.label "I would eat this many cookies">
+<!ENTITY intpref.accesskey "e">
+<!ENTITY stringpref.label "Name of my favorite cookie">
+<!ENTITY stringpref.accesskey "f">
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/Makefile.in
@@ -0,0 +1,50 @@
+#
+# ***** 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 mozilla.org code.
+#
+# 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):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of 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 *****
+
+DEPTH		= ../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = mailnewsglobaldb
+
+EXTRA_JS_MODULES = $(wildcard $(srcdir)/*.js)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/collection.js
@@ -0,0 +1,718 @@
+/* ***** 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 = ['GlodaCollection', 'GlodaCollectionManager'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+const LOG = Log4Moz.Service.getLogger("gloda.collection");
+
+/**
+ * @namespace Central registry and logic for all collections. 
+ *
+ * The collection manager is a singleton that has the following tasks:
+ * - Let views of objects (nouns) know when their objects have changed.  For
+ *   example, an attribute has changed due to user action.
+ * - Let views of objects based on queries know when new objects match their
+ *   query, or when their existing objects no longer match due to changes.
+ * - Caching/object-identity maintenance.  It is ideal if we only ever have
+ *   one instance of an object at a time.  (More specifically, only one instance
+ *   per database row 'id'.)  The collection mechanism lets us find existing
+ *   instances to this end.  Caching can be directly integrated by being treated
+ *   as a special collection.
+ */
+var GlodaCollectionManager = {
+  _collectionsByNoun: {},
+  _cachesByNoun: {},
+
+  /**
+   * Registers the existence of a collection with the collection manager.  This
+   *  is done using a weak reference so that the collection can go away if it
+   *  wants to.
+   */
+  registerCollection: function gloda_colm_registerCollection(aCollection) {
+    let collections;
+    let nounID = aCollection.query._nounDef.id;
+    if (!(nounID in this._collectionsByNoun))
+      collections = this._collectionsByNoun[nounID] = [];
+    else {
+      // purge dead weak references while we're at it
+      collections = this._collectionsByNoun[nounID].filter(function (aRef) {
+        return aRef.get(); });
+      this._collectionsByNoun[nounID] = collections;
+    }
+    collections.push(Cu.getWeakReference(aCollection));
+  },
+  
+  getCollectionsForNounID: function gloda_colm_getCollectionsForNounID(aNounID){
+    if (!(aNounID in this._collectionsByNoun))
+      return [];
+    
+    // generator would be nice, but I suspect get() is too expensive to use
+    //  twice (guard/predicate and value)
+    let weakCollections = this._collectionsByNoun[aNounID];
+    let collections = [];
+    for (let iColl = 0; iColl < weakCollections.length; iColl++) {
+      let collection = weakCollections[iColl].get();
+      if (collection)
+        collections.push(collection);
+    }
+    return collections;
+  },
+  
+  defineCache: function gloda_colm_defineCache(aNounDef, aCacheSize) {
+    this._cachesByNoun[aNounDef.id] = new GlodaLRUCacheCollection(aNounDef,
+                                                                   aCacheSize);
+  },
+  
+  /**
+   * Attempt to locate an instance of the object of the given noun type with the
+   *  given id.  Counts as a cache hit if found.  (And if it was't in a cache,
+   *  but rather a collection, it is added to the cache.)
+   */
+  cacheLookupOne: function gloda_colm_cacheLookupOne(aNounID, aID, aDoCache) {
+    let cache = this._cachesByNoun[aNounID];
+    
+    if (cache) {
+      if (aID in cache._idMap) {
+        let item = cache._idMap[aID];
+        return cache.hit(item);
+      }
+    }
+    
+    if (aDoCache === false)
+      cache = null;
+  
+    for each (let [iCollection, collection] in
+              Iterator(this.getCollectionsForNounID(aNounID))) {
+      if (aID in collection._idMap) {
+        let item = collection._idMap[aID];
+        if (cache)
+          cache.add([item]);
+        return item;
+      }
+    }
+    
+    return null;
+  },
+
+  /**
+   * Lookup multiple nouns by ID from the cache/existing collections.
+   * @return [The number that were found, the number that were not found.]
+   */
+  cacheLookupMany: function gloda_colm_cacheLookupMany(aNounID, aIDMap,
+      aTargetMap, aDoCache) {
+    let foundCount = 0, notFoundCount = 0, notFound = {};
+    
+    let cache = this._cachesByNoun[aNounID];
+    
+    if (cache) {
+      for (let key in aIDMap) {
+        let cacheValue = cache._idMap[key];
+        if (cacheValue === undefined) {
+          notFoundCount++;
+          notFound[key] = null;
+        }
+        else {
+          foundCount++;
+          aTargetMap[key] = cacheValue;
+          cache.hit(cacheValue);
+        }
+      }
+    }
+
+    if (aDoCache === false)
+      cache = null;
+    
+    for each (let [iCollection, collection] in
+              Iterator(this.getCollectionsForNounID(aNounID))) {
+      for (let key in notFound) {
+        let collValue = collection._idMap[key];
+        if (collValue !== undefined) {
+          aTargetMap[key] = collValue;
+          delete notFound[key];
+          foundCount++;
+          notFoundCount--;
+          if (cache)
+            cache.add([collValue]);
+        }
+      }
+    }
+    
+    return [foundCount, notFoundCount, notFound];
+  },
+  
+  /**
+   * Attempt to locate an instance of the object of the given noun type with the
+   *  given id.  Counts as a cache hit if found.  (And if it was't in a cache,
+   *  but rather a collection, it is added to the cache.)
+   */
+  cacheLookupOneByUniqueValue:
+      function gloda_colm_cacheLookupOneByUniqueValue(aNounID, aUniqueValue,
+                                                      aDoCache) {
+    let cache = this._cachesByNoun[aNounID];
+    
+    if (cache) {
+      if (aUniqueValue in cache._uniqueValueMap) {
+        let item = cache._uniqueValueMap[aUniqueValue];
+        return cache.hit(item);
+      }
+    }
+    
+    if (aDoCache === false)
+      cache = null;
+  
+    for each (let [iCollection, collection] in
+              Iterator(this.getCollectionsForNounID(aNounID))) {
+      if (aUniqueValue in collection._uniqueValueMap) {
+        let item = collection._uniqueValueMap[aUniqueValue];
+        if (cache)
+          cache.add([item]);
+        return item;
+      }
+    }
+    
+    return null;
+  },
+  
+  /**
+   * Checks whether the provided item with the given id is actually a duplicate
+   *  of an instance that already exists in the cache/a collection.  If it is,
+   *  the pre-existing instance is returned and counts as a cache hit.  If it
+   *  is not, the passed-in instance is added to the cache and returned.
+   */
+  cacheLoadUnifyOne: function gloda_colm_cacheLoadUnifyOne(aItem) {
+    let items = [aItem];
+    this.cacheLoadUnify(aItem.NOUN_ID, items);
+    return items[0];
+  },
+
+  /**
+   * Given a list of items, check if any of them already have duplicate,
+   *  canonical, instances in the cache or collections.  Items with pre-existing
+   *  instances are replaced by those instances in the provided list, and each
+   *  counts as a cache hit.  Items without pre-existing instances are added
+   *  to the cache and left intact.
+   */
+  cacheLoadUnify: function gloda_colm_cacheLoadUnify(aNounID, aItems,
+      aCacheIfMissing) {
+    let cache = this._cachesByNoun[aNounID];
+    if (aCacheIfMissing === undefined)
+      aCacheIfMissing = true; 
+    
+    // track the items we haven't yet found in a cache/collection (value) and
+    //  their index in aItems (key).  We're somewhat abusing the dictionary
+    //  metaphor with the intent of storing tuples here.  We also do it because
+    //  it allows random-access deletion theoretically without cost.  (Since
+    //  we delete during iteration, that may be wrong, but it sounds like the
+    //  semantics still work?)
+    let unresolvedIndexToItem = {};
+    let numUnresolved = 0;
+    
+    if (cache) {
+      for (let iItem = 0; iItem < aItems.length; iItem++) {
+        let item = aItems[iItem];
+        
+        if (item.id in cache._idMap) {
+          let realItem = cache._idMap[item.id];
+          // update the caller's array with the reference to the 'real' item
+          aItems[iItem] = realItem;
+          cache.hit(realItem);
+        }
+        else {
+          unresolvedIndexToItem[iItem] = item;
+          numUnresolved++;
+        }
+      }
+      
+      // we're done if everyone was a hit.
+      if (numUnresolved == 0)
+        return;
+    }
+    else {
+      for (let iItem = 0; iItem < aItems.length; iItem++) {
+        unresolvedIndexToItem[iItem] = aItems[iItem];
+      }
+      numUnresolved = aItems.length;
+    }
+  
+    let needToCache = [];
+    // next, let's fall back to our collections
+    for each (let [iCollection, collection] in
+              Iterator(this.getCollectionsForNounID(aNounID))) {
+      for (let [iItem, item] in Iterator(unresolvedIndexToItem)) {
+        if (item.id in collection._idMap) {
+          let realItem = collection._idMap[item.id];
+          // update the caller's array to now have the 'real' object
+          aItems[iItem] = realItem;
+          // flag that we need to cache this guy (we use an inclusive cache)
+          needToCache.push(realItem);
+          // we no longer need to resolve this item...
+          delete unresolvedIndexToItem[iItem];
+          // stop checking collections if we got everybody
+          if (--numUnresolved == 0)
+            break;
+        }
+      }
+    }
+    
+    // anything left in unresolvedIndexToItem should be added to the cache
+    //  unless !aCacheIfMissing.  plus, we already have 'needToCache'
+    if (cache && aCacheIfMissing) {
+      cache.add(needToCache.concat([val for each
+                                    (val in unresolvedIndexToItem)]));
+    }
+    
+    return aItems;
+  },
+  
+  cacheCommitDirty: function glod_colm_cacheCommitDirty() {
+    for each (let cache in this._cachesByNoun) {
+      cache.commitDirty();
+    }
+  },
+
+  /**
+   * Notifies the collection manager that an item has been loaded and should
+   *  be cached, assuming caching is active.
+   */    
+  itemLoaded: function gloda_colm_itemsLoaded(aItem) {
+    let cache = this._cachesByNoun[aItem.NOUN_ID];
+    if (cache) {
+      cache.add([aItem]);
+    }
+  },
+
+  /**
+   * Notifies the collection manager that multiple items has been loaded and
+   *  should be cached, assuming caching is active.
+   */  
+  itemsLoaded: function gloda_colm_itemsLoaded(aNounID, aItems) {
+    let cache = this._cachesByNoun[aNounID];
+    if (cache) {
+      cache.add(aItems);
+    }
+  },
+  
+  /**
+   * This should be called when items are added to the global database.  This
+   *  should generally mean during indexing by indexers or an attribute
+   *  provider.
+   * We walk all existing collections for the given noun type and add the items
+   *  to the collection if the item meets the query that defines the collection.
+   */
+  itemsAdded: function gloda_colm_itemsAdded(aNounID, aItems) {
+    let cache = this._cachesByNoun[aNounID];
+    if (cache) {
+      cache.add(aItems);
+    }
+
+    for each (let [iCollection, collection] in
+              Iterator(this.getCollectionsForNounID(aNounID))) {
+      let addItems = [item for each ([i, item] in Iterator(aItems))
+                      if (collection.query.test(item))];
+      if (addItems.length)
+        collection._onItemsAdded(addItems);
+    }
+  },
+  /**
+   * This should be called when items in the global database are modified.  For
+   *  example, as a result of indexing.  This should generally only be called
+   *  by indexers or by attribute providers.
+   * We walk all existing collections for the given noun type.  For items
+   *  currently included in each collection but should no longer be (per the
+   *  collection's defining query) we generate onItemsRemoved events.  For items
+   *  not currently included in the collection but should now be, we generate
+   *  onItemsAdded events.  For items included that still match the query, we
+   *  generate onItemsModified events.
+   */
+  itemsModified: function gloda_colm_itemsModified(aNounID, aItems) {
+    for each (let [iCollection, collection] in
+              Iterator(this.getCollectionsForNounID(aNounID))) {
+      let added = [], modified = [], removed = [];
+      for each (let [iItem, item] in Iterator(aItems)) {
+        if (item.id in collection._idMap) {
+          // currently in... but should it still be there?
+          if (collection.query.test(item))
+            modified.push(item); // yes, keep it
+          else
+            removed.push(item); // no, bin it
+        }
+        else if (collection.query.test(item)) // not in, should it be?
+          added.push(item); // yep, add it
+      }
+      if (added.length)
+        collection._onItemsAdded(added);
+      if (modified.length)
+        collection._onItemsModified(modified);
+      if (removed.length)
+        collection._onItemsRemoved(removed);
+    }
+  },
+  /**
+   * This should be called when items in the global database are permanently
+   *  deleted.  (This is distinct from concepts like message deletion which may
+   *  involved trash folders or other modified forms of existence.  Deleted
+   *  means the data is gone and if it were to come back, it would come back
+   *  with a brand new unique id and we would get an itemsAdded event.)
+   * We walk all existing collections for the given noun type.  For items
+   *  currently in the collection, we generate onItemsRemoved events.
+   */
+  itemsDeleted: function gloda_colm_itemsDeleted(aNounID, aItems) {
+    // cache
+    let cache = this._cachesByNoun[aNounID];
+    if (cache) {
+      for each (let [iItem, item] in Iterator(aItem)) {
+        if (item.id in cache._idMap)
+          cache.delete(item);
+      }
+    }
+
+    // collections
+    for each (let [iCollection, collection] in
+              Iterator(this.getCollectionsForNounID(aNounID))) {
+      let removeItems = [item for each ([i, item] in Iterator(aItems))
+                         if (item.id in collection._idMap)];
+      if (removeItems.length)
+        collection._onItemsRemoved(removeItems);
+    }
+  },
+};
+
+/**
+ * @class A current view of the set of first-class nouns meeting a given query.
+ *  Assuming a listener is present, events are
+ *  generated when new objects meet the query, existing objects no longer meet
+ *  the query, or existing objects have experienced a change in attributes that
+ *  does not affect their ability to be present (but the listener may care about
+ *  because it is exposing those attributes).
+ * @constructor 
+ */
+function GlodaCollection(aNounDef, aItems, aQuery, aListener,
+      aMasterCollection) {
+  // if aNounDef is null, we are just being invoked for subclassing
+  if (aNounDef === undefined)
+    return;
+
+  this._nounDef = aNounDef;
+  // should we also maintain a unique value mapping...
+  if (this._nounDef.usesUniqueValue)
+    this._uniqueValueMap = {};
+
+  this.pendingItems = [];
+  this._pendingIdMap = {};
+  this.items = [];
+  this._idMap = {};
+  
+  // force the listener to null for our call to _onItemsAdded; no events for
+  //  the initial load-out.
+  this._listener = null;
+  if (aItems && aItems.length)
+    this._onItemsAdded(aItems);
+  
+  this.query = aQuery || null;
+  if (this.query)
+    this.query.collection = this;
+  this._listener = aListener || null;
+  
+  this.deferredCount = 0;
+  this.resolvedCount = 0;
+  
+  if (aMasterCollection) {
+    this.masterCollection = aMasterCollection.masterCollection;
+  }
+  else {
+    this.masterCollection = this;
+    /** a dictionary of dictionaries. at the top level, the keys are noun IDs.
+     * each of these sub-dictionaries maps the IDs of desired noun instances to
+     * the actual instance, or null if it has not yet been loaded.
+     */ 
+    this.referencesByNounID = {};
+    /**
+     * a dictionary of dictionaries. at the top level, the keys are noun IDs.
+     * each of the sub-dictionaries maps the IDs of the _recognized parent
+     * noun_ to the list of children, or null if the list has not yet been
+     * populated.
+     * 
+     * So if we have a noun definition A with ID 1 who is the recognized parent
+     *  noun of noun definition B with ID 2, AND we have an instance A(1) with
+     *  two children B(10), B(11), then an example might be: {2: {1: [10, 11]}}.
+     */
+    this.inverseReferencesByNounID = {};
+    this.subCollections = {};
+  }
+}
+
+GlodaCollection.prototype = {
+  get listener() { return this._listener; },
+  set listener(aListener) { this._listener = aListener; },
+  
+  /**
+   * Clear the contents of this collection.  This only makes sense for explicit
+   *  collections or wildcard collections.  (Actual query-based collections
+   *  should represent the state of the query, so unless we're going to delete
+   *  all the items, clearing the collection would violate that constraint.)
+   */
+  clear: function gloda_coll_clear() {
+    this._idMap = {};
+    if (this._uniqueValueMap)
+      this._uniqueValueMap = {};
+    this.items = [];
+  },
+
+  _onItemsAdded: function gloda_coll_onItemsAdded(aItems) {
+    this.items.push.apply(this.items, aItems);
+    if (this._uniqueValueMap) {
+      for each (let [iItem, item] in Iterator(this.items)) {
+        this._idMap[item.id] = item;
+        this._uniqueValueMap[item.uniqueValue] = item;
+      }
+    }
+    else {
+      for each (let [iItem, item] in Iterator(this.items)) {
+        this._idMap[item.id] = item;
+      }
+    }
+    if (this._listener) {
+      try {
+        this._listener.onItemsAdded(aItems, this);
+      }
+      catch (ex) {
+        LOG.error("caught exception from listener in onItemsAdded: " + 
+            ex.fileName + ":" + ex.lineNumber + ": " + ex);
+      }
+    }
+  },
+  
+  _onItemsModified: function gloda_coll_onItemsModified(aItems) {
+    if (this._listener) {
+      try {
+        this._listener.onItemsModified(aItems, this);
+      }
+      catch (ex) {
+        LOG.error("caught exception from listener in onItemsModified: " + 
+            ex.fileName + ":" + ex.lineNumber + ": " + ex);
+      }
+    }
+  },
+  
+  /**
+   * Given a list of items that definitely no longer belong in this collection,
+   *  remove them from the collection and notify the listener.  The 'tricky'
+   *  part is that we need to remove the deleted items from our list of items.
+   */
+  _onItemsRemoved: function gloda_coll_onItemsRemoved(aItems) {
+    // we want to avoid the O(n^2) deletion performance case, and deletion
+    //  should be rare enough that the extra cost of building the deletion map
+    //  should never be a real problem.
+    let deleteMap = {};
+    // build the delete map while also nuking from our id map/unique value map
+    for each (let [iItem, item] in Iterator(aItems)) {
+      deleteMap[item.id] = true;
+      delete this._idMap[item.id];
+      if (this._uniqueValueMap)
+        delete this._uniqueValueMap[item.uniqueValue];
+    }
+    let items = this.items;
+    // in-place filter.  probably needless optimization.
+    let iWrite=0;
+    for (let iRead = 0; iRead < items.length; iRead++) {
+      let item = items[iRead];
+      if (!(item.id in deleteMap))
+        items[iWrite++] = item;
+    }
+    items.slice(iWrite);
+    
+    if (this._listener) {
+      try {
+        this._listener.onItemsRemoved(aItems, this);
+      }
+      catch (ex) {
+        LOG.error("caught exception from listener in onItemsRemoved: " + 
+            ex.fileName + ":" + ex.lineNumber + ": " + ex);
+      }
+    }
+  },
+
+  _onQueryCompleted: function gloda_coll_onQueryCompleted() {
+    if (this._listener && this._listener.onQueryCompleted)
+      this._listener.onQueryCompleted(this);
+  }
+};
+
+/**
+ * Create an LRU cache collection for the given noun with the given size.
+ * @constructor
+ */
+function GlodaLRUCacheCollection(aNounDef, aCacheSize) {
+  GlodaCollection.call(this, aNounDef, null, null, null);
+  
+  this._head = null; // aka oldest!
+  this._tail = null; // aka newest!
+  this._size = 0;
+  // let's keep things sane, and simplify our logic a little...
+  if (aCacheSize < 32)
+    aCacheSize = 32;
+  this._maxCacheSize = aCacheSize;
+}
+/**
+ * @class A LRU-discard cache.  We use a doubly linked-list for the eviction
+ *  tracking.  Since we require that there is at most one LRU-discard cache per
+ *  noun class, we simplify our lives by adding our own attributes to the
+ *  cached objects.
+ * @augments GlodaCollection
+ */
+GlodaLRUCacheCollection.prototype = new GlodaCollection;
+GlodaLRUCacheCollection.prototype.add = function cache_add(aItems) {
+  for each (let [iItem, item] in Iterator(aItems)) {
+    if (item.id in this._idMap) {
+      // DEBUGME so, we're dealing with this, but it shouldn't happen.  need
+      //  trace-debuggage.
+      continue;
+    }
+    this._idMap[item.id] = item;
+    if (this._uniqueValueMap)
+      this._uniqueValueMap[item.uniqueValue] = item;
+    
+    item._lruPrev = this._tail;
+    // we do have to make sure that we will set _head the first time we insert
+    //  something
+    if (this._tail !== null)
+      this._tail._lruNext = item;
+    else
+      this._head = item;
+    item._lruNext = null;
+    this._tail = item;
+    
+    this._size++;
+  }
+  
+  while (this._size > this._maxCacheSize) {
+    let item = this._head;
+    
+    // we never have to deal with the possibility of needing to make _head/_tail
+    //  null.
+    this._head = item._lruNext;
+    this._head._lruPrev = null;
+    // (because we are nice, we will delete the properties...)
+    delete item._lruNext;
+    delete item._lruPrev;
+    
+    // nuke from our id map
+    delete this._idMap[item.id];
+    if (this._uniqueValueMap)
+      delete this._uniqueValueMap[item.uniqueValue];
+    
+    // flush dirty items to disk (they may not have this attribute, in which
+    //  case, this returns false, which is fine.)
+    if (item.dirty) {
+      this._nounDef.objUpdate.call(this._nounDef.datastore, item);
+      delete item.dirty;
+    }
+    
+    this._size--;
+  }
+};
+
+GlodaLRUCacheCollection.prototype.hit = function cache_hit(aItem) {
+  // don't do anything in the 0 or 1 items case, or if we're already
+  //  the last item
+  if ((this._head === this._tail) || (this._tail === aItem))
+    return aItem;
+
+  // - unlink the item  
+  if (aItem._lruPrev !== null)
+    aItem._lruPrev._lruNext = aItem._lruNext;
+  else
+    this._head = aItem._lruNext;
+  // (_lruNext cannot be null)
+  aItem._lruNext._lruPrev = aItem._lruPrev;
+  // - link it in to the end
+  this._tail._lruNext = aItem; 
+  aItem._lruPrev = this._tail;
+  aItem._lruNext = null;
+  // update tail tracking
+  this._tail = aItem;
+  
+  return aItem;
+};
+
+GlodaLRUCacheCollection.prototype.deleted = function cache_deleted(aItem) {
+  // unlink the item  
+  if (aItem._lruPrev !== null)
+    aItem._lruPrev._lruNext = aItem._lruNext;
+  else
+    this._head = aItem._lruNext;
+  if (aItem._lruNext !== null)
+    aItem._lruNext._lruPrev = aItem._lruPrev;
+  else
+    this._tail = aItem._lruPrev;
+
+  // (because we are nice, we will delete the properties...)
+  delete aItem._lruNext;
+  delete aItem._lruPrev;
+    
+  // nuke from our id map
+  delete this._idMap[aItem.id];
+  if (this._uniqueValueMap)
+    delete this._uniqueValueMap[aItem.uniqueValue];
+  
+  this._size--;
+}
+
+/**
+ * If any of the cached items are dirty, commit them, and make them no longer
+ *  dirty.
+ */
+GlodaLRUCacheCollection.prototype.commitDirty = function cache_commitDirty() {
+  // we can only do this if there is an update method available...
+  if (!this._nounDef.objUpdate)
+    return;
+
+  for each (let [iItem, item] in Iterator(this._idMap)) {
+    if (item.dirty) {
+      LOG.debug("flushing dirty: " + item);
+      this._nounDef.objUpdate.call(this._nounDef.datastore, item);
+      delete item.dirty;
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/connotent.js
@@ -0,0 +1,229 @@
+/* ***** 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 = ['GlodaContent'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+const LOG = Log4Moz.Service.getLogger("gloda.connotent");
+
+function GlodaContent() {
+  this._contentPriority = null;
+  this._producing = false;
+  this._hunks = [];
+}
+
+GlodaContent.prototype = {
+  kPriorityBase: 0,
+  kPriorityPerfect: 100,
+  
+  kHunkMeta: 1,
+  kHunkQuoted: 2,
+  kHunkContent: 3,
+  
+  _resetContent: function gloda_content__resetContent() {
+    this._keysAndValues = [];
+    this._keysAndDeltaValues = [];
+    this._hunks = [];
+    this._curHunk = null;
+  },
+  
+  /* ===== Consumer API ===== */
+  hasContent: function gloda_content_hasContent() {
+    return (this._contentPriority != null);
+  },
+  
+  /**
+   * Return content suitable for snippet display.  This means that no quoting
+   *  or meta-data should be returned.
+   * 
+   * @param aMaxLength The maximum snippet length desired.
+   */
+  getContentSnippet: function gloda_content_getContentSnippet(aMaxLength) {
+    let content = this.getContentString();
+    if (aMaxLength)
+      content = content.substring(0, aMaxLength);
+    return content;
+  },
+  
+  getContentString: function gloda_content_getContent(aIndexingPurposes) {
+    let data = "";
+    for each (let [, hunk] in Iterator(this._hunks)) {
+      if (hunk.hunkType == this.kHunkContent) {
+        if (data)
+          data += "\n" + hunk.data;
+        else
+          data = hunk.data;
+      }
+    }
+    
+    if (aIndexingPurposes) {
+      // append the values for indexing.  we assume the keywords are cruft.
+      // this may be crazy, but things that aren't a science aren't an exact
+      // science.
+      for each (let [, kv] in Iterator(this._keysAndValues)) {
+        data += "\n" + kv[1];
+      }
+      for each (let [, kon] in Iterator(this._keysAndValues)) {
+        data += "\n" + kon[1] + "\n" + kon[2];
+      }
+    }
+    
+    return data;
+  },
+  
+  /* ===== Producer API ===== */
+  /**
+   * Called by a producer with the priority they believe their interpretation
+   *  of the content comes in at.
+   * 
+   * @returns true if we believe the producer's interpretation will be
+   *     interesting and they should go ahead and generate events.  We return
+   *     false if we don't think they are interesting, in which case they should
+   *     probably not issue calls to us, although we don't care.  (We will
+   *     ignore their calls if we return false, this allows the simplification
+   *     of code that needs to run anyways.)
+   */
+  volunteerContent: function gloda_content_volunteerContent(aPriority) {
+    if (this._contentPriority === null || this._contentPriority < aPriority) {
+      this._contentPriority = aPriority;
+      this._resetContent();
+      this._producing = true;
+      return true;
+    }
+    this._producing = false;
+    return false;
+  },
+  
+  keyValue: function gloda_content_keyValue(aKey, aValue) {
+    if (!this._producing)
+      return;
+
+    this._keysAndValues.push([aKey, aValue]);
+  },
+  keyValueDelta: function gloda_content_keyValueDelta (aKey, aOldValue,
+      aNewValue) {
+    if (!this._producing)
+      return;
+
+    this._keysAndDeltaValues.push([aKey, aOldValue, aNewValue]);
+  },
+  
+  /**
+   * Meta lines are lines that have to do with the content but are not the
+   *  content and can generally be related to an attribute that has been derived
+   *  and stored on the item.
+   * For example, a bugzilla bug may note that an attachment was created; this
+   *  is not content and wouldn't be desired in a snippet, but is still
+   *  potentially interesting meta-data.
+   * 
+   * @param aLineOrLines The line or list of lines that are meta-data.
+   * @param aAttr The attribute this meta-data is associated with.
+   * @param aIndex If the attribute is non-singular, indicate the specific
+   *     index of the item in the attribute's bound list that the meta-data
+   *     is associated with.
+   */
+  meta: function gloda_content_meta(aLineOrLines, aAttr, aIndex) {
+    if (!this._producing)
+      return;
+    
+    let data;
+    if (typeof(aLineOrLines) == "string")
+      data = aLineOrLines;
+    else
+      data = aLineOrLines.join("\n");
+    
+    this._curHunk = {hunkType: this.kHunkMeta, attr: aAttr, index: aIndex,
+                     data: data};
+    this._hunks.push(this._curHunk);
+  },
+  /**
+   * Quoted lines reference previous messages or what not.
+   * 
+   * @param aLineOrLiens The line or list of lines that are quoted.
+   * @param aDepth The depth of the quoting.
+   * @param aOrigin The item that originated the original content, if known.
+   *     For example, perhaps a GlodaMessage?
+   * @param aTarget A reference to the location in the original content, if
+   *     known.  For example, the index of a line in a message or something?  
+   */
+  quoted: function gloda_content_quoted(aLineOrLines, aDepth, aOrigin,
+      aTarget) {
+    if (!this._producing)
+      return;
+    
+    let data;
+    if (typeof(aLineOrLines) == "string")
+      data = aLineOrLines;
+    else
+      data = aLineOrLines.join("\n");
+
+    if (!this._curHunk ||
+        this._curHunk.hunkType != this.kHunkQuoted ||
+        this._curHunk.depth != aDepth ||
+        this._curHunk.origin != aOrigin || this._curHunk.target != aTarget) {
+      this._curHunk = {hunkType: this.kHunkQuoted, data: data,
+                       depth: aDepth, origin: aOrigin, target: aTarget};
+      this._hunks.push(this._curHunk);
+    }
+    else
+      this._curHunk.data += "\n" + data; 
+  },
+  
+  content: function gloda_content_content(aLineOrLines) {
+    if (!this._producing)
+      return;
+
+    let data;
+    if (typeof(aLineOrLines) == "string")
+      data = aLineOrLines;
+    else
+      data = aLineOrLines.join("\n");
+    
+    if (!this._curHunk || this._curHunk.hunkType != this.kHunkContent) {
+      this._curHunk = {hunkType: this.kHunkContent, data: data};
+      this._hunks.push(this._curHunk);
+    }
+    else
+      this._curHunk.data += "\n" + data;
+  },
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/databind.js
@@ -0,0 +1,147 @@
+/* ***** 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 = ["GlodaDatabind"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+function DatabindCallback(aDatabind, aCallbackThis, aCallback, aOneShot) {
+  this._databind = aDatabind;
+  this._callbackThis = aCallbackThis;
+  this._callback = aCallback;
+  this._oneShot = aOneShot;
+  this._databind._datastore._pendingAsyncStatements++;
+}
+DatabindCallback.prototype = {
+  handleResult: function (aResultSet) {
+    let rows = [];
+    let rowResult;
+    let getVariant = this._databind._datastore._getVariant;
+    while (rowResult = aResultSet.getNextRow()) {
+      let row = {};
+      for each (let [iCol, colDef] in
+                Iterator(this._databind._tableDef.columns)) {
+        let colName = colDef[0];
+        row[colName] = getVariant(rowResult, iCol);
+      }
+      rows.push(row);
+    }
+    this._callback.call(this._callbackThis, rows, false);
+  },
+  handleError: function (aError) {
+  },
+  handleCompletion: function () {
+    this._callback.call(this._callbackThis, [], true);
+    this._databind._datastore._asyncCompleted();
+  },
+}
+
+function GlodaDatabind(aTableDef, aDatastore) {
+  this._tableDef = aTableDef;
+  this._datastore = aDatastore;
+  this._log = Log4Moz.Service.getLogger("gloda.databind." + aTableDef.name);
+  
+  let insertSql = "INSERT INTO " + this._tableDef._realName + " (" +
+    [coldef[0] for each
+     ([i, coldef] in Iterator(this._tableDef.columns))].join(", ") +
+    ") VALUES (" +
+    [(":" + coldef[0]) for each
+     ([i, coldef] in Iterator(this._tableDef.columns))].join(", ") +
+    ")";
+  
+  this._insertStmt = aDatastore._createAsyncStatement(insertSql);
+  
+  this._stmtCache = {};
+}
+
+GlodaDatabind.prototype = {
+  /*
+  getHighId: function(aLessThan) {
+    let sql = "select MAX(id) AS m_id FROM " + this._tableDef._realName;
+    if (aLessThan !== undefined)
+      sql += " WHERE id < " + aLessThan;
+  dump("SQL: " + sql);
+    let stmt = this._datastore._createStatement(sql);
+  dump("created\n");
+    let highId = 0;
+    if (stmt.step()) {
+      dump("stepped, retrieving\n");
+      highId = stmt.row["m_id"];
+    }
+    stmt.reset();
+    
+    return highId;
+  },
+  */
+    
+  select: function(aColName, aColValue, aCallbackThis, aCallback) {
+    let stmt;
+    if (!(aColName in this._stmtCache)) {
+      let sqlString = "SELECT * FROM " + this._tableDef._realName;
+      if (aColName)
+        sqlString += " WHERE " + aColName + " = :value";
+      stmt = this._datastore._createAsyncStatement(sqlString);
+      this._stmtCache[aColName] = stmt;
+    }
+    else
+      stmt = this._stmtCache[aColName];
+    
+    if (aColName)
+      this._datastore._bindVariant(stmt, 0, aColValue);
+    // so, we're tricky-like and lazy and actually return the row, so we don't
+    //  want to reset until the user tries to use the statement again, as I
+    //  fear we would otherwise lose our awesome row binding (and have to copy
+    //  it, etc.)
+    stmt.executeAsync(new DatabindCallback(this, aCallbackThis, aCallback));
+  },
+  
+  insert: function(aValueDicts) {
+    let stmt = this._insertStmt;
+    for each (let [,valueDict] in Iterator(aValueDicts)) {
+      for each (let [iColDef, colDef] in Iterator(this._tableDef.columns)) {
+        this._log.debug("insert arg: " + colDef[0] + "=" + valueDict[colDef[0]]);
+        stmt.params[colDef[0]] = valueDict[colDef[0]];
+      }
+      stmt.executeAsync(this._datastore.trackAsync());
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/datamodel.js
@@ -0,0 +1,470 @@
+/* ***** 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 = ["GlodaAttributeDBDef",
+                    "GlodaConversation", "GlodaFolder", "GlodaMessage",
+                    "GlodaContact", "GlodaIdentity"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+const LOG = Log4Moz.Service.getLogger("gloda.datamodel");
+
+Cu.import("resource://gloda/modules/utils.js");
+
+/**
+ * @class Represents a gloda attribute definition's DB form.  This class
+ *  stores the information in the database relating to this attribute
+ *  definition.  Access its attrDef attribute to get at the realy juicy data.
+ *  This main interesting thing this class does is serve as the keeper of the
+ *  mapping from parameters to attribute ids in the database if this is a 
+ *  parameterized attribute.
+ */
+function GlodaAttributeDBDef(aDatastore, aID, aCompoundName, aAttrType,
+                           aPluginName, aAttrName) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._compoundName = aCompoundName;
+  this._attrType = aAttrType;
+  this._pluginName = aPluginName;
+  this._attrName = aAttrName;
+  
+  this.attrDef = null;
+
+  /** Map parameter values to the underlying database id. */
+  this._parameterBindings = {};
+}
+
+GlodaAttributeDBDef.prototype = {
+  get id() { return this._id; },
+  get attributeName() { return this._attrName; },
+
+  get parameterBindings() { return this._parameterBindings; },
+  
+  /**
+   * Bind a parameter value to the attribute definition, allowing use of the
+   *  attribute-parameter as an attribute.
+   *
+   * @return
+   */
+  bindParameter: function gloda_attr_bindParameter(aValue) {
+    // people probably shouldn't call us with null, but handle it
+    if (aValue == null) {
+      return this._id;
+    }
+    if (aValue in this._parameterBindings) {
+      return this._parameterBindings[aValue];
+    }
+    // no database entry exists if we are here, so we must create it...
+    let id = this._datastore._createAttributeDef(this._attrType,
+                 this._pluginName, this._attrName, aValue);
+    this._parameterBindings[aValue] = id;
+    this._datastore.reportBinding(id, this, aValue);
+    return id;
+  },
+
+  /**
+   * Given a list of values (if non-singular) or a single value (if singular),
+   *  return a list (regardless of plurality) of database-ready [attribute id,
+   *  value] tuples.  This is intended to be used to directly convert the value
+   *  of a property on an object that corresponds to a bound attribute.
+   */
+  convertValuesToDBAttributes:
+      function gloda_attr_convertValuesToDBAttributes(aInstanceValues) {
+    let nounDef = this.attrDef.objectNounDef;
+    if (this._singular) {
+      if (nounDef.usesParameter) {
+        let [param, dbValue] = nounDef.toParamAndValue(aInstanceValues);
+        return [[this.bindParameter(param), dbValue]];
+      }
+      else {
+        return [[this._id, nounDef.toParamAndValue(aInstanceValues)[1]]];
+      }
+    }
+    else {
+      let dbAttributes = [];
+      if (nounDef.usesParameter) {
+        for each (let [, instanceValue] in Iterator(aInstanceValues)) {
+          let [param, dbValue] = nounDef.toParamAndValue(instanceValue);
+          dbAttributes.push([this.bindParameter(param), dbValue]);
+        }
+      }
+      else {
+        for each (let [, instanceValue] in Iterator(aInstanceValues)) {
+          dbAttributes.push([this._id,
+                             nounDef.toParamAndValue(instanceValue)[1]]);
+        }
+      }
+      return dbAttributes;
+    }
+  },
+
+  toString: function() {
+    return this._compoundName;
+  }
+};
+
+let GlodaHasAttributesMixIn = {
+  enumerateAttributes: function gloda_attrix_enumerateAttributes() {
+    let nounDef = this.NOUN_DEF;
+    for each (let [key, value] in Iterator(this)) {
+      let attrDef = nounDef.attribsByBoundName[key];
+      // we expect to not have attributes for underscore prefixed values (those
+      //  are managed by the instance's logic.  we also want to not explode
+      //  should someone crap other values in there, we get both birds with this
+      //  one stone.
+      if (attrDef === undefined)
+        continue;
+      if (attrDef.singular) {
+        // ignore attributes with null values
+        if (value != null)
+          yield [attrDef, [value]];
+      }
+      else {
+        // ignore attributes with no values
+        if (value.length)
+          yield [attrDef, value];
+      }
+    }
+  },
+  
+  domContribute: function gloda_attrix_domContribute(aDomNode) {
+    let nounDef = this.NOUN_DEF;
+    for each (let [attrName, attr] in
+        Iterator(nounDef.domExposeAttribsByBoundName)) {
+      if (this[attrName])
+        aDomNode.setAttribute(attr.domExpose, this[attrName]);
+    }
+  },
+};
+
+function MixIn(aConstructor, aMixIn) {
+  let proto = aConstructor.prototype;
+  for (let [name, func] in Iterator(aMixIn)) {
+    if (name.substring(0, 4) == "get_")
+      proto.__defineGetter__(name.substring(4), func);
+    else
+      proto[name] = func;
+  }
+}
+
+/**
+ * @class A gloda conversation (thread) exists so that messages can belong.
+ */
+function GlodaConversation(aDatastore, aID, aSubject, aOldestMessageDate,
+                           aNewestMessageDate) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._subject = aSubject;
+  this._oldestMessageDate = aOldestMessageDate;
+  this._newestMessageDate = aNewestMessageDate;
+}
+
+GlodaConversation.prototype = {
+  NOUN_ID: 101,
+  get id() { return this._id; },
+  get subject() { return this._subject; },
+  get oldestMessageDate() { return this._oldestMessageDate; },
+  get newestMessageDate() { return this._newestMessageDate; },
+  
+  getMessagesCollection: function gloda_conversation_getMessagesCollection(
+    aListener, aData) {
+    let query = new GlodaMessage.prototype.NOUN_DEF.queryClass();
+    query.conversation(this._id).orderBy("date");
+    return query.getCollection(aListener, aData);
+  },
+
+  toString: function gloda_conversation_toString() {
+    return "Conversation:" + this._id;
+  },
+};
+
+function GlodaFolder(aDatastore, aID, aURI, aDirtyStatus, aPrettyName) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._uri = aURI;
+  this._dirtyStatus = aDirtyStatus;
+  this._prettyName = aPrettyName;
+}
+
+GlodaFolder.prototype = {
+  NOUN_ID: 100,
+  /** The folder is believed to be up-to-date */
+  kFolderClean: 0,
+  /** The folder has some un-indexed or dirty messages */
+  kFolderDirty: 1,
+  /** The folder needs to be entirely re-indexed, regardless of the flags on
+   * the messages in the folder. This state will be downgraded to dirty */
+  kFolderFilthy: 2,
+  get id() { return this._id; },
+  get uri() { return this._uri; },
+  get dirtyStatus() { return this._dirtyStatus; },
+  set dirtyStatus(aNewStatus) {
+    if (aNewStatus != this._dirtyStatus) {
+      this._dirtyStatus = aNewStatus;
+      this._datastore.updateFolderDirtyStatus(this);
+    }
+  },
+  get name() { return this._prettyName; },
+  toString: function gloda_folder_toString() {
+    return "Folder:" + this._id;
+  }
+}
+
+/**
+ * @class A message representation.
+ */
+function GlodaMessage(aDatastore, aID, aFolderID, aMessageKey,
+                      aConversationID, aConversation, aDate,
+                      aHeaderMessageID, aDeleted, aJsonText) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._folderID = aFolderID;
+  this._messageKey = aMessageKey;
+  this._conversationID = aConversationID;
+  this._conversation = aConversation;
+  this._date = aDate;
+  this._headerMessageID = aHeaderMessageID;
+  if (aJsonText)
+    this._jsonText = aJsonText;
+
+  // only set _deleted if we're deleted, otherwise the undefined does our
+  //  speaking for us.
+  if (aDeleted)
+    this._deleted = aDeleted;
+}
+
+GlodaMessage.prototype = {
+  NOUN_ID: 102,
+  get id() { return this._id; },
+  get folderID() { return this._folderID; },
+  get messageKey() { return this._messageKey; },
+  get conversationID() { return this._conversationID; },
+  // conversation is special
+  get headerMessageID() { return this._headerMessageID; },
+  
+  get date() { return this._date; },
+  set date(aNewDate) { this._date = aNewDate; },
+
+  get folderURI() {
+    if (this._folderID != null)
+      return this._datastore._mapFolderID(this._folderID).uri;
+    else
+      return null;
+  },
+  get conversation() {
+    return this._conversation;
+  },
+
+  toString: function gloda_message_toString() {
+    // uh, this is a tough one...
+    return "Message:" + this._id;
+  },
+
+  _clone: function gloda_message_clone() {
+    return new GlodaMessage(this._datastore, this._id, this._folderID,
+      this._messageKey, this._conversationID, this._conversation, this._date,
+      this._headerMessageID, this._deleted);
+  },
+
+  _ghost: function gloda_message_ghost() {
+    this._folderID = null;
+    this._messageKey = null;
+  },
+
+  _nuke: function gloda_message_nuke() {
+    this._id = null;
+    this._folderID = null;
+    this._messageKey = null;
+    this._conversationID = null;
+    this._conversation = null;
+    this.date = null;
+    this._headerMessageID = null;
+
+    this._datastore = null;
+  },
+
+  /**
+   * Return the underlying nsIMsgDBHdr from the folder storage for this, or
+   *  null if the message does not exist for one reason or another.
+   * This method no longer caches the result, so it's up to you.
+   */
+  get folderMessage() {
+    if (this._folderID === null || this._messageKey === null)
+      return null;
+    let rdfService = Cc['@mozilla.org/rdf/rdf-service;1'].
+                     getService(Ci.nsIRDFService);
+    let folder = rdfService.GetResource(
+                   this._datastore._mapFolderID(this._folderID).uri);
+    if (folder instanceof Ci.nsIMsgFolder) {
+      let folderMessage = folder.GetMessageHeader(this._messageKey);
+      if (folderMessage !== null) {
+        // verify the message-id header matches what we expect...
+        if (folderMessage.messageId != this._headerMessageID) {
+          LOG.info("Message with message key does not match expected " +
+                   "header! (" + this._headerMessageID + " expected, got " +
+                   folderMessage.messageId + ")");
+          folderMessage = null;
+        }
+      }
+      return folderMessage;
+    }
+
+    // this only gets logged if things have gone very wrong.  we used to throw
+    //  here, but it's unlikely our caller can do anything more meaningful than
+    //  treating this as a disappeared message.
+    LOG.info("Unable to locate folder message for: " + this._folderID + ":" +
+             this._messageKey);
+    return null;
+  },
+  get folderMessageURI() {
+    let folderMessage = this.folderMessage;
+    if (folderMessage)
+      return folderMessage.folder.getUriForMsg(folderMessage);
+    else
+      return null;
+  }
+};
+MixIn(GlodaMessage, GlodaHasAttributesMixIn);
+
+/**
+ * @class Contacts correspond to people (one per person), and may own multiple
+ *  identities (e-mail address, IM account, etc.)
+ */
+function GlodaContact(aDatastore, aID, aDirectoryUUID, aContactUUID, aName,
+                      aPopularity, aFrecency, aJsonText) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._directoryUUID = aDirectoryUUID;
+  this._contactUUID = aContactUUID;
+  this._name = aName;
+  this._popularity = aPopularity;
+  this._frecency = aFrecency;
+  if (aJsonText)
+    this._jsonText = aJsonText;
+
+  this._identities = null;
+}
+
+GlodaContact.prototype = {
+  NOUN_ID: 103,
+
+  get id() { return this._id; },
+  get directoryUUID() { return this._directoryUUID; },
+  get contactUUID() { return this._contactUUID; },
+  get name() { return this._name; },
+  set name(aName) { this._name = aName; },
+
+  get popularity() { return this._popularity; },
+  set popularity(aPopularity) {
+    this._popularity = aPopularity;
+    this.dirty = true;
+  },
+
+  get frecency() { return this._frecency; },
+  set frecency(aFrecency) {
+    this._frecency = aFrecency;
+    this.dirty = true;
+  },
+
+  get identities() {
+    return this._identities;
+  },
+
+  toString: function gloda_contact_toString() {
+    return "Contact:" + this._id;
+  },
+  
+  get accessibleLabel() {
+    return "Contact: " + this._name;
+  },
+
+  _clone: function gloda_contact_clone() {
+    return new GlodaContact(this._datastore, this._id, this._directoryUUID,
+      this._contactUUID, this._name, this._popularity, this._frecency);
+  },
+};
+MixIn(GlodaContact, GlodaHasAttributesMixIn);
+
+
+/**
+ * @class A specific means of communication for a contact.
+ */
+function GlodaIdentity(aDatastore, aID, aContactID, aContact, aKind, aValue,
+                       aDescription, aIsRelay) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._contactID = aContactID;
+  this._contact = aContact;
+  this._kind = aKind;
+  this._value = aValue;
+  this._description = aDescription;
+  this._isRelay = aIsRelay;
+}
+
+GlodaIdentity.prototype = {
+  NOUN_ID: 104,
+  get id() { return this._id; },
+  get contactID() { return this._contactID; },
+  get contact() { return this._contact; },
+  get kind() { return this._kind; },
+  get value() { return this._value; },
+  get description() { return this._description; },
+  get isRelay() { return this._isRelay; },
+
+  get uniqueValue() {
+    return this._kind + "@" + this._value;
+  },
+
+  toString: function gloda_identity_toString() {
+    return "Identity:" + this._kind + ":" + this._value;
+  },
+
+  get abCard() {
+    return GlodaUtils.getCardForEmail(this._value);
+  },
+  
+  pictureURL: function(aSize) {
+    let md5hash = GlodaUtils.md5HashString(this._value);
+    let gravURL = "http://www.gravatar.com/avatar/" + md5hash +
+                                "?d=identicon&s=" + aSize + "&r=g";
+    return gravURL;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/datastore.js
@@ -0,0 +1,3135 @@
+/* ***** 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 ***** */
+
+/* This file looks to Myk Melez <myk@mozilla.org>'s Mozilla Labs snowl
+ * project's (http://hg.mozilla.org/labs/snowl/) modules/datastore.js
+ * for inspiration and idioms (and also a name :).
+ */
+
+EXPORTED_SYMBOLS = ["GlodaDatastore"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+Cu.import("resource://gloda/modules/datamodel.js");
+Cu.import("resource://gloda/modules/databind.js");
+Cu.import("resource://gloda/modules/collection.js");
+
+let MBM_LOG = Log4Moz.Service.getLogger("gloda.ds.mbm");
+
+/**
+ * @class This callback handles processing the asynchronous query results of
+ *  GlodaDatastore.getMessagesByMessageID.  Because that method is only
+ *  called as part of the indexing process, we are guaranteed that there will
+ *  be no real caching ramifications.  Accordingly, we can also defer our cache
+ *  processing (via GlodaCollectionManager) until the query completes.
+ *
+ * @param aMsgIDToIndex Map from message-id to the desired
+ *
+ * @constructor
+ */
+function MessagesByMessageIdCallback(aMsgIDToIndex, aResults,
+                                     aCallback, aCallbackThis) {
+  this.msgIDToIndex = aMsgIDToIndex;
+  this.results = aResults;
+  this.callback = aCallback;
+  this.callbackThis = aCallbackThis;
+}
+
+MessagesByMessageIdCallback.prototype = {
+  onItemsAdded: function gloda_ds_mbmi_onItemsAdded(aItems, aCollection) {
+    MBM_LOG.debug("getting results...");
+    for each (let [, message] in Iterator(aItems)) {
+      this.results[this.msgIDToIndex[message.headerMessageID]].push(message);
+    }
+  },
+  onItemsModified: function () {},
+  onItemsRemoved: function () {},
+  onQueryCompleted: function gloda_ds_mbmi_onQueryCompleted(aCollection) {
+    MBM_LOG.debug("query completed, notifying... " + this.results);
+    // we no longer need to unify; it is done for us.
+
+    this.callback.call(this.callbackThis, this.results);
+  }
+};
+
+let PCH_LOG = Log4Moz.Service.getLogger("gloda.ds.pch");
+
+function PostCommitHandler(aCallbacks) {
+  this.callbacks = aCallbacks;
+  GlodaDatastore._pendingAsyncStatements++;
+}
+
+PostCommitHandler.prototype = {
+  handleResult: function gloda_ds_pch_handleResult(aResultSet) {
+  },
+  
+  handleError: function gloda_ds_pch_handleError(aError) {
+    PCH_LOG.error("database error:" + aError)
+  },
+  
+  handleCompletion: function gloda_ds_pch_handleCompletion(aReason) {
+    if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+      for each (let [iCallback, callback] in Iterator(this.callbacks)) {
+        try {
+          callback();
+        }
+        catch (ex) {
+          dump("PostCommitHandler callback (" + ex.fileName + ":" +
+               ex.lineNumber + ") threw: " + ex);
+        }
+      }
+    }
+    GlodaDatastore._asyncCompleted();
+  }
+};
+
+let QFQ_LOG = Log4Moz.Service.getLogger("gloda.ds.qfq");
+
+let QueryFromQueryResolver = {
+  onItemsAdded: function(aIgnoredItems, aCollection, aFake) {
+    let originColl = aCollection.dataStack ? aCollection.dataStack.pop()
+                                           : aCollection.data;
+    if (aCollection.completionShifter)
+      aCollection.completionShifter.push(originColl);
+    else
+      aCollection.completionShifter = [originColl];
+
+    if (!aFake) {
+      originColl.deferredCount--;
+      originColl.resolvedCount++;
+    }
+    
+    // bail if we are still pending on some other load completion
+    if (originColl.deferredCount > 0) {
+      //QFQ_LOG.debug("QFQR: bailing " + originColl._nounDef.name);
+      return;
+    }
+    
+    let referencesByNounID = originColl.masterCollection.referencesByNounID;
+    let inverseReferencesByNounID = 
+      originColl.masterCollection.inverseReferencesByNounID;
+
+    if (originColl.pendingItems) {
+      for (let [, item] in Iterator(originColl.pendingItems)) {
+        //QFQ_LOG.debug("QFQR: loading deferred " + item.NOUN_ID + ":" + item.id);
+        GlodaDatastore.loadNounDeferredDeps(item, referencesByNounID,
+            inverseReferencesByNounID);
+      }
+      
+      // we need to consider the possibility that we are racing a collection very
+      //  much like our own.  as such, this means we need to perform cache
+      //  unification as our last step.
+      GlodaCollectionManager.cacheLoadUnify(originColl._nounDef.id,
+        originColl.pendingItems, false);
+  
+      // just directly tell the collection about the items.  we know the query
+      //  matches (at least until we introduce predicates that we cannot express
+      //  in SQL.)
+      //QFQ_LOG.debug(" QFQR: about to trigger listener: " + originColl._listener +
+      //    "with collection: " + originColl._nounDef.name);
+      originColl._onItemsAdded(originColl.pendingItems);
+      delete originColl.pendingItems;
+      delete originColl._pendingIdMap;
+    }
+  },
+  onItemsModified: function() {
+  },
+  onItemsRemoved: function() {
+  },
+  onQueryCompleted: function(aCollection) {
+    let originColl = aCollection.completionShifter ?
+      aCollection.completionShifter.shift() : aCollection.data;
+    //QFQ_LOG.debug(" QFQR about to trigger completion with collection: " +
+    //  originColl._nounDef.name);
+    if (originColl.deferredCount <= 0) {
+      originColl._onQueryCompleted();
+    }
+  },
+};
+
+/**
+ * @class Handles the results from a GlodaDatastore.queryFromQuery call.
+ * @constructor
+ */
+function QueryFromQueryCallback(aStatement, aNounDef, aCollection) {
+  this.statement = aStatement;
+  this.nounDef = aNounDef;
+  this.collection = aCollection;
+  
+  //QFQ_LOG.debug("Creating QFQCallback for noun: " + aNounDef.name);
+  
+  // the master collection holds the referencesByNounID
+  this.referencesByNounID = {};
+  this.masterReferencesByNounID =
+    this.collection.masterCollection.referencesByNounID;
+  this.inverseReferencesByNounID = {};
+  this.masterInverseReferencesByNounID =
+    this.collection.masterCollection.inverseReferencesByNounID;
+  // we need to contribute our references as we load things; we need this 
+  //  because of the potential for circular dependencies and our inability to
+  //  put things into the caching layer (or collection's _idMap) until we have
+  //  fully resolved things.
+  if (this.nounDef.id in this.masterReferencesByNounID)
+    this.selfReferences = this.masterReferencesByNounID[this.nounDef.id];
+  else
+    this.selfReferences = this.masterReferencesByNounID[this.nounDef.id] = {};
+  if (this.nounDef.parentColumnAttr) {
+    if (this.nounDef.id in this.masterInverseReferencesByNounID)
+      this.selfInverseReferences =
+        this.masterInverseReferencesByNounID[this.nounDef.id];
+    else
+      this.selfInverseReferences =
+        this.masterInverseReferencesByNounID[this.nounDef.id] = {};
+  }
+  
+  this.needsLoads = false;
+  
+  GlodaDatastore._pendingAsyncStatements++;
+}
+
+QueryFromQueryCallback.prototype = {
+  handleResult: function gloda_ds_qfq_handleResult(aResultSet) {
+    let pendingItems = this.collection.pendingItems;
+    let pendingIdMap = this.collection._pendingIdMap;
+    let row;
+    let nounDef = this.nounDef;
+    let nounID = nounDef.id;
+    while (row = aResultSet.getNextRow()) {
+      let item = nounDef.objFromRow.call(nounDef.datastore, row);
+      // try and replace the item with one from the cache, if we can
+      let cachedItem = GlodaCollectionManager.cacheLookupOne(nounID, item.id,
+                                                             false);
+      
+      // if we already have a copy in the pending id map, skip it
+      if (item.id in pendingIdMap)
+        continue;
+      
+      //QFQ_LOG.debug("loading item " + nounDef.id + ":" + item.id + " existing: " +
+      //    this.selfReferences[item.id] + " cached: " + cachedItem);
+      if (cachedItem)
+        item = cachedItem;
+      // we may already have been loaded by this process
+      else if (this.selfReferences[item.id] != null)
+        item = this.selfReferences[item.id];
+      // perform loading logic which may produce reference dependencies
+      else
+        this.needsLoads = 
+          GlodaDatastore.loadNounItem(item, this.referencesByNounID,
+                                      this.inverseReferencesByNounID) ||
+          this.needsLoads;
+      
+      // add ourself to the references by our id
+      // QFQ_LOG.debug("saving item " + nounDef.id + ":" + item.id + " to self-refs");
+      this.selfReferences[item.id] = item;
+      
+      // if we're tracking it, add ourselves to our parent's list of children
+      //  too
+      if (this.selfInverseReferences) {
+        let parentID = item[nounDef.parentColumnAttr.idStorageAttributeName];
+        let childrenList = this.selfInverseReferences[parentID];
+        if (childrenList === undefined)
+          childrenList = this.selfInverseReferences[parentID] = [];
+        childrenList.push(item);
+      }
+      
+      pendingItems.push(item);
+      pendingIdMap[item.id] = item;
+    }
+  },
+
+  handleError: function gloda_ds_qfq_handleError(aError) {
+    GlodaDatastore._log.error("Async queryFromQuery error: " +
+      aError.result + ": " + aError.message);
+  },
+
+  handleCompletion: function gloda_ds_qfq_handleCompletion(aReason) {
+    this.statement.finalize();
+    this.statement = null;
+    
+    //QFQ_LOG.debug("handleCompletion: " + this.collection._nounDef.name);
+    
+    if (this.needsLoads) {
+      for each (let [nounID, references] in Iterator(this.referencesByNounID)) {
+        if (nounID == this.nounDef.id)
+          continue;
+        let nounDef = GlodaDatastore._nounIDToDef[nounID];
+        //QFQ_LOG.debug("  have references for noun: " + nounDef.name);
+        // try and load them out of the cache/existing collections.  items in the
+        //  cache will be fully formed, which is nice for us.
+        // XXX this mechanism will get dubious when we have multiple paths to a
+        //  single noun-type.  For example, a -> b -> c, a-> c; two paths to c
+        //  and we're looking at issuing two requests to c, the latter of which
+        //  will be a superset of the first one.  This does not currently pose
+        //  a problem because we only have a -> b -> c -> b, and sequential
+        //  processing means no alarms and no surprises.
+        let masterReferences = this.masterReferencesByNounID[nounID];
+        if (masterReferences === undefined)
+          masterReferences = this.masterReferencesByNounID[nounID] = {};
+        let outReferences;
+        if (nounDef.parentColumnAttr)
+          outReferences = {};
+        else
+          outReferences = masterReferences;
+        let [foundCount, notFoundCount, notFound] =
+          GlodaCollectionManager.cacheLookupMany(nounDef.id, references,
+              outReferences);
+
+        if (nounDef.parentColumnAttr) {
+          let inverseReferences;
+          if (nounDef.id in this.masterInverseReferencesByNounID)
+            inverseReferences =
+              this.masterInverseReferencesByNounID[nounDef.id];
+          else
+            inverseReferences =
+              this.masterInverseReferencesByNounID[nounDef.id] = {};
+          
+          for each (let item in outReferences) {
+            masterReferences[item.id] = item;
+            let parentID = item[nounDef.parentColumnAttr.idStorageAttributeName];
+            let childrenList = inverseReferences[parentID];
+            if (childrenList === undefined)
+              childrenList = inverseReferences[parentID] = [];
+            childrenList.push(item);
+          }
+        }
+        
+        //QFQ_LOG.debug("  found: " + foundCount + " not found: " + notFoundCount);
+        if (notFoundCount === 0) {
+          this.collection.resolvedCount++;
+        }
+        else {
+          this.collection.deferredCount++;
+          let query = new nounDef.queryClass();
+          query.id.apply(query, [id for (id in notFound)]);
+          
+          this.collection.masterCollection.subCollections[nounDef.id] = 
+            GlodaDatastore.queryFromQuery(query, QueryFromQueryResolver, 
+              this.collection,
+              // we fully expect/allow for there being no such subcollection yet.
+              this.collection.masterCollection.subCollections[nounDef.id],
+              this.collection.masterCollection);
+        }
+      }
+      
+      for each (let [nounID, inverseReferences] in
+          Iterator(this.inverseReferencesByNounID)) {
+        this.collection.deferredCount++;
+        let nounDef = GlodaDatastore._nounIDToDef[nounID];
+        
+        //QFQ_LOG.debug("Want to load inverse via " + nounDef.parentColumnAttr.boundName);
+  
+        let query = new nounDef.queryClass();
+        // we want to constrain using the parent column
+        let queryConstrainer = query[nounDef.parentColumnAttr.boundName];
+        queryConstrainer.apply(query, [pid for (pid in inverseReferences)]);
+        this.collection.masterCollection.subCollections[nounDef.id] = 
+          GlodaDatastore.queryFromQuery(query, QueryFromQueryResolver,
+            this.collection,
+            // we fully expect/allow for there being no such subcollection yet.
+            this.collection.masterCollection.subCollections[nounDef.id],
+            this.collection.masterCollection);
+      }
+    }
+    else {
+      this.collection.deferredCount--;
+      this.collection.resolvedCount++;
+    }
+    
+    //QFQ_LOG.debug("  defer: " + this.collection.deferredCount +
+    //              " resolved: " + this.collection.resolvedCount);
+    
+    // process immediately and kick-up to the master collection...
+    try {
+      if (this.collection.deferredCount <= 0) {
+        // this guy will resolve everyone using referencesByNounID and issue the
+        //  call to this.collection._onItemsAdded to propagate things to the
+        //  next concerned subCollection or the actual listener if this is the
+        //  master collection.  (Also, call _onQueryCompleted).
+        QueryFromQueryResolver.onItemsAdded(null, {data: this.collection}, true);
+        QueryFromQueryResolver.onQueryCompleted({data: this.collection});
+      }
+    }
+    finally {
+      GlodaDatastore._asyncCompleted();
+    }
+  }
+};
+
+
+/**
+ * Database abstraction layer.  Contains explicit SQL schemas for our
+ *  fundamental representations (core 'nouns', if you will) as well as
+ *  specialized functions for then dealing with each type of object.  At the
+ *  same time, we are beginning to support extension-provided tables, which
+ *  call into question whether we really need our hand-rolled code, or could
+ *  simply improve the extension-provided table case to work for most of our
+ *  hand-rolled cases.
+ * For now, the argument can probably be made that our explicit schemas and code
+ *  is readable/intuitive (not magic) and efficient (although generic stuff
+ *  could also be made efficient, if slightly evil through use of eval or some
+ *  other code generation mechanism.)
+ *
+ * === Data Model Interaction / Dependencies
+ *
+ * Dependent on and assumes limited knowledge of the datamodel.js
+ *  implementations.  datamodel.js actually has an implicit dependency on
+ *  our implementation, reaching back into the datastore via the _datastore
+ *  attribute which we pass into every instance we create.
+ * We pass a reference to ourself as we create the datamodel.js instances (and
+ *  they store it as _datastore) because of a half-implemented attempt to make
+ *  it possible to live in a world where we have multiple datastores.  This
+ *  would be desirable in the cases where we are dealing with multiple SQLite
+ *  databases.  This could be because of per-account global databases or
+ *  some other segmentation.  This was abandoned when the importance of
+ *  per-account databases was diminished following public discussion, at least
+ *  for the short-term, but no attempted was made to excise the feature or
+ *  preclude it.  (Merely a recognition that it's too much to try and implement
+ *  correct right now, especially because our solution might just be another
+ *  (aggregating) layer on top of things, rather than complicating the lower
+ *  levels.)
+ *
+ * === Object Identity / Caching
+ *
+ * The issue of object identity is handled by integration with the collection.js
+ *  provided GlodaCollectionManager.  By "Object Identity", I mean that we only
+ *  should ever have one object instance alive at a time that corresponds to
+ *  an underlying database row in the database.  Where possible we avoid
+ *  performing database look-ups when we can check if the object is already
+ *  present in memory; in practice, this means when we are asking for an object
+ *  by ID.  When we cannot avoid a database query, we attempt to make sure that
+ *  we do not return a duplicate object instance, instead replacing it with the
+ *  'live' copy of the object.  (Ideally, we would avoid any redundant
+ *  construction costs, but that is not currently the case.)
+ * Although you should consult the GlodaCollectionManager for details, the
+ *  general idea is that we have 'collections' which represent views of the
+ *  database (based on a query) which use a single mechanism for double duty.
+ *  The collections are registered with the collection manager via weak
+ *  reference.  The first 'duty' is that since the collections may be desired
+ *  to be 'live views' of the data, we want them to update as changes occur.
+ *  The weak reference allows the collection manager to track the 'live'
+ *  collections and update them.  The second 'duty' is the caching/object
+ *  identity duty.  In theory, every live item should be referenced by at least
+ *  one collection, making it reachable for object identity/caching purposes.
+ * There is also an explicit (inclusive) caching layer present to both try and
+ *  avoid poor performance from some of the costs of this strategy, as well as
+ *  to try and keep track of objects that are being worked with that are not
+ *  (yet) tracked by a collection.  Using a size-bounded cache is clearly not
+ *  a guarantee of correctness for this, but is suspected will work quite well.
+ *  (Well enough to be dangerous because the inevitable failure case will not be
+ *  expected.)
+ *
+ * The current strategy may not be the optimal one, feel free to propose and/or
+ *  implement better ones, especially if you have numbers.
+ * The current strategy is not fully implemented in this file, but the common
+ *  cases are believed to be covered.  (Namely, we fail to purge items from the
+ *  cache as they are purged from the database.)
+ *
+ * === Things That May Not Be Obvious (Gotchas)
+ *
+ * Although the schema includes "triggers", they are currently not used
+ *  and were added when thinking about implementing the feature.  We will
+ *  probably implement this feature at some point, which is why they are still
+ *  in there.
+ *
+ * We, and the layers above us, are not sufficiently thorough at cleaning out
+ *  data from the database, and may potentially orphan it _as new functionality
+ *  is added in the future at layers above us_.  That is, currently we should
+ *  not be leaking database rows, but we may in the future.  This is because
+ *  we/the layers above us lack a mechanism to track dependencies based on
+ *  attributes.  Say a plugin exists that extracts recipes from messages and
+ *  relates them via an attribute.  To do so, it must create new recipe rows
+ *  in its own table as new recipes are discovered.  No automatic mechanism
+ *  will purge recipes as their source messages are purged, nor does any
+ *  event-driven mechanism explicitly inform the plugin.  (It could infer
+ *  such an event from the indexing/attribute-providing process, or poll the
+ *  states of attributes to accomplish this, but that is not desirable.)  This
+ *  needs to be addressed, and may be best addressed at layers above
+ *  datastore.js.
+ * @namespace
+ */
+var GlodaDatastore = {
+  _log: null,
+
+  /* see Gloda's documentation for these constants */
+  kSpecialNotAtAll: 0,
+  kSpecialColumn: 16,
+  kSpecialColumnChildren: 16|1,
+  kSpecialColumnParent: 16|2,
+  kSpecialString: 32,
+  kSpecialFulltext: 64,
+  
+  kConstraintIdIn: 0,
+  kConstraintIn: 1,
+  kConstraintRanges: 2,
+  kConstraintEquals: 3,
+  kConstraintStringLike: 4,
+  kConstraintFulltext: 5,
+
+  /* ******************* SCHEMA ******************* */
+
+  _schemaVersion: 10,
+  _schema: {
+    tables: {
+
+      // ----- Messages
+      folderLocations: {
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "folderURI TEXT NOT NULL",
+          "dirtyStatus INTEGER NOT NULL",
+          "name TEXT NOT NULL",
+        ],
+
+        triggers: {
+          delete: "DELETE from messages WHERE folderID = OLD.id",
+        },
+      },
+
+      conversations: {
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "subject TEXT NOT NULL",
+          "oldestMessageDate INTEGER",
+          "newestMessageDate INTEGER",
+        ],
+
+        indices: {
+          subject: ['subject'],
+          oldestMessageDate: ['oldestMessageDate'],
+          newestMessageDate: ['newestMessageDate'],
+        },
+
+        fulltextColumns: [
+          "subject TEXT",
+        ],
+
+        triggers: {
+          delete: "DELETE from messages WHERE conversationID = OLD.id",
+        },
+      },
+
+      /**
+       * A message record correspond to an actual message stored in a folder
+       *  somewhere, or is a ghost record indicating a message that we know
+       *  should exist, but which we have not seen (and which we may never see).
+       *  We represent these ghost messages by storing NULL values in the
+       *  folderID and messageKey fields; this may need to change to other
+       *  sentinel values if this somehow impacts performance.
+       */
+      messages: {
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "folderID INTEGER REFERENCES folderLocations(id)",
+          "messageKey INTEGER",
+          "conversationID INTEGER NOT NULL REFERENCES conversations(id)",
+          "date INTEGER",
+          // we used to have the parentID, but because of the very real
+          //  possibility of multiple copies of a message with a given
+          //  message-id, the parentID concept is unreliable.
+          "headerMessageID TEXT",
+          "deleted INTEGER NOT NULL default 0",
+          "jsonAttributes TEXT",
+        ],
+
+        indices: {
+          messageLocation: ['folderID', 'messageKey'],
+          headerMessageID: ['headerMessageID'],
+          conversationID: ['conversationID'],
+          date: ['date'],
+          deleted: ['deleted'],
+        },
+
+        fulltextColumns: [
+          "subject TEXT",
+          "body TEXT",
+          "attachmentNames TEXT",
+        ],
+
+        triggers: {
+          delete: "DELETE FROM messageAttributes WHERE messageID = OLD.id",
+        },
+      },
+
+      // ----- Attributes
+      attributeDefinitions: {
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "attributeType INTEGER NOT NULL",
+          "extensionName TEXT NOT NULL",
+          "name TEXT NOT NULL",
+          "parameter BLOB",
+        ],
+
+        triggers: {
+          delete: "DELETE FROM messageAttributes WHERE attributeID = OLD.id",
+        },
+      },
+
+      messageAttributes: {
+        columns: [
+          "conversationID INTEGER NOT NULL REFERENCES conversations(id)",
+          "messageID INTEGER NOT NULL REFERENCES messages(id)",
+          "attributeID INTEGER NOT NULL REFERENCES attributeDefinitions(id)",
+          "value NUMERIC",
+        ],
+
+        indices: {
+          attribQuery: [
+            "attributeID", "value",
+            /* covering: */ "conversationID", "messageID"],
+        },
+      },
+
+      // ----- Contacts / Identities
+
+      /**
+       * Corresponds to a human being and roughly to an address book entry.
+       *  Constrast with an identity, which is a specific e-mail address, IRC
+       *  nick, etc.  Identities belong to contacts, and this relationship is
+       *  expressed on the identityAttributes table.
+       */
+      contacts: {
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "directoryUUID TEXT",
+          "contactUUID TEXT",
+          "popularity INTEGER",
+          "frecency INTEGER",
+          "name TEXT",
+          "jsonAttributes TEXT",
+        ],
+        indices: {
+          popularity: ["popularity"],
+          frecency: ["frecency"],
+        },
+      },
+
+      contactAttributes: {
+        columns: [
+          "contactID INTEGER NOT NULL REFERENCES contacts(id)",
+          "attributeID INTEGER NOT NULL REFERENCES attributeDefinitions(id)",
+          "value NUMERIC"
+        ],
+        indices: {
+          contactAttribQuery: [
+            "attributeID", "value",
+            /* covering: */ "contactID"],
+        }
+      },
+
+      /**
+       * Identities correspond to specific e-mail addresses, IRC nicks, etc.
+       */
+      identities: {
+        columns: [
+          "id INTEGER PRIMARY KEY",
+          "contactID INTEGER NOT NULL REFERENCES contacts(id)",
+          "kind TEXT NOT NULL", // ex: email, irc, etc.
+          "value TEXT NOT NULL", // ex: e-mail address, irc nick/handle, etc.
+          "description NOT NULL", // what makes this identity different from the
+          // others? (ex: home, work, etc.)
+          "relay INTEGER NOT NULL", // is the identity just a relay mechanism?
+          // (ex: mailing list, twitter 'bouncer', IRC gateway, etc.)
+        ],
+
+        indices: {
+          contactQuery: ["contactID"],
+          valueQuery: ["kind", "value"]
+        }
+      },
+
+      //identityAttributes: {
+      //},
+
+    },
+  },
+
+
+  /* ******************* LOGIC ******************* */
+  /**
+   * Our synchronous connection, primarily intended for read-only use, so as to
+   *  avoid stepping on the toes of our asynchronous connection that will do
+   *  most/all of our updating.
+   */
+  syncConnection: null,
+  /**
+   * Our connection reused for asynchronous usage, intended for database write
+   *  purposes.
+   */
+  asyncConnection: null,
+
+  /**
+   * Initialize logging, create the database if it doesn't exist, "upgrade" it
+   *  if it does and it's not up-to-date, fill our authoritative folder uri/id
+   *  mapping.
+   */
+  _init: function gloda_ds_init(aNsJSON, aNounIDToDef) {
+    this._log = Log4Moz.Service.getLogger("gloda.datastore");
+    this._log.debug("Beginning datastore initialization.");
+    
+    this._json = aNsJSON;
+    this._nounIDToDef = aNounIDToDef;
+
+    // Get the path to our global database
+    var dirService = Cc["@mozilla.org/file/directory_service;1"].
+                     getService(Ci.nsIProperties);
+    var dbFile = dirService.get("ProfD", Ci.nsIFile);
+    dbFile.append("global-messages-db.sqlite");
+
+    // Get the storage (sqlite) service
+    var dbService = Cc["@mozilla.org/storage/service;1"].
+                    getService(Ci.mozIStorageService);
+
+    var dbConnection;
+
+    // Create the file if it does not exist
+    if (!dbFile.exists()) {
+      this._log.debug("Creating database because it does't exist.");
+      dbConnection = this._createDB(dbService, dbFile);
+    }
+    // It does exist, but we (someday) might need to upgrade the schema
+    else {
+      // (Exceptions may be thrown if the database is corrupt)
+      { // try {
+        dbConnection = dbService.openUnsharedDatabase(dbFile);
+
+        if (dbConnection.schemaVersion != this._schemaVersion) {
+          this._log.debug("Need to migrate database.  (DB version: " +
+            dbConnection.schemaVersion + " desired version: " +
+            this._schemaVersion);
+          dbConnection = this._migrate(dbService, dbFile,
+                                       dbConnection,
+                                       dbConnection.schemaVersion,
+                                       this._schemaVersion);
+          this._log.debug("Migration completed.");
+        }
+      }
+      // Handle corrupt databases, other oddities
+      // ... in the future. for now, let us die
+    }
+
+    this.syncConnection = dbConnection;
+    this.asyncConnection = dbConnection;
+
+    this._log.debug("Initializing folder mappings.");
+    this._getAllFolderMappings();
+    // we need to figure out the next id's for all of the tables where we
+    //  manage that.
+    this._log.debug("Populating managed id counters.");
+    this._populateAttributeDefManagedId();
+    this._populateConversationManagedId();
+    this._populateMessageManagedId();
+    this._populateContactManagedId();
+    this._populateIdentityManagedId();
+    
+    this._log.debug("Completed datastore initialization.");
+  },
+
+  /**
+   * Initiate database shutdown; because this might requiring waiting for
+   *  outstanding synchronous events to drain, we allow the caller to pass in
+   *  a callback to invoke if we are unable to complete shutdown within this
+   *  call.
+   * @return true if we were able to shutdown fully, false if we were not.  The
+   *   callback, if provided, will be notified if we return false.  It will
+   *   not be called if we return true.
+   */
+  shutdown: function gloda_ds_shutdown(aCallback, aCallbackThis) {
+    // clear out any transaction
+    while (this._transactionDepth) {
+      this._log.info("Closing pending transaction out for shutdown.");
+      // just schedule this function to be run again once the transaction has
+      //  been closed out.
+      this._commitTransaction();
+    }
+
+    let datastore = this;
+
+    function finish_cleanup() {
+      datastore._cleanupAsyncStatements();
+      datastore._cleanupSyncStatements();
+      datastore._log.info("Closing db connection");
+      datastore.asyncConnection.close();
+      datastore.asyncConnection = null;
+      datastore.syncConnection = null;
+
+      if (aCallback) {
+        aCallback.call(aCallbackThis);
+      }
+    }
+
+    if (this._pendingAsyncStatements) {
+      this._pendingAsyncCompletedListener = finish_cleanup;
+      return false;
+    }
+    else {
+      this._log.debug("There are no pending async statements, finishing now.");
+      aCallback = null;
+      finish_cleanup();
+      return true;
+    }
+  },
+
+  /**
+   * Create our database; basically a wrapper around _createSchema.
+   */
+  _createDB: function gloda_ds_createDB(aDBService, aDBFile) {
+    var dbConnection = aDBService.openUnsharedDatabase(aDBFile);
+
+    dbConnection.beginTransaction();
+    try {
+      this._createSchema(dbConnection);
+      dbConnection.commitTransaction();
+    }
+    catch(ex) {
+      dbConnection.rollbackTransaction();
+      throw ex;
+    }
+
+    return dbConnection;
+  },
+
+  _createTableSchema: function gloda_ds_createTableSchema(aDBConnection,
+      aTableName) {
+    let table = this._schema.tables[aTableName];
+
+    // - Create the table
+    aDBConnection.createTable(aTableName, table.columns.join(", "));
+
+    // - Create the fulltext table if applicable
+    if ("fulltextColumns" in table) {
+      let createFulltextSQL = "CREATE VIRTUAL TABLE " + aTableName + "Text" +
+        " USING fts3(tokenize porter, " + table.fulltextColumns.join(", ") +
+        ")";
+      this._log.info("Create fulltext: " + createFulltextSQL);
+      aDBConnection.executeSimpleSQL(createFulltextSQL);
+    }
+
+    // - Create its indices
+    for (let indexName in table.indices) {
+      let indexColumns = table.indices[indexName];
+
+      aDBConnection.executeSimpleSQL(
+        "CREATE INDEX " + indexName + " ON " + aTableName +
+        "(" + indexColumns.join(", ") + ")");
+    }
+  },
+
+  /**
+   * Create our database schema assuming a newly created database.  This
+   *  comes down to creating normal tables, their full-text variants (if
+   *  applicable), and their indices.
+   */
+  _createSchema: function gloda_ds_createSchema(aDBConnection) {
+    // -- For each table...
+    for (let tableName in this._schema.tables) {
+      this._createTableSchema(aDBConnection, tableName);
+    }
+
+    aDBConnection.schemaVersion = this._schemaVersion;
+  },
+
+  /**
+   * Our table definition used here is slightly different from that used
+   *  internally, because we are potentially creating a sort of crappy ORM and
+   *  we don't want to have to parse the column names out.
+   */
+  createTableIfNotExists: function gloda_ds_createTableIfNotExists(aTableDef) {
+    aTableDef._realName = "ext_" + aTableDef.name;
+
+    // first, check if the table exists
+    if (!this.asyncConnection.tableExists(aTableDef._realName)) {
+      try {
+        this.asyncConnection.createTable(aTableDef._realName,
+          [coldef.join(" ") for each
+           ([i, coldef] in Iterator(aTableDef.columns))].join(", "));
+      }
+      catch (ex) {
+         this._log.error("Problem creating table " + aTableDef.name + " " +
+           "because: " + ex + " at " + ex.fileName + ":" + ex.lineNumber);
+         return null;
+      }
+
+      for (let indexName in aTableDef.indices) {
+        let indexColumns = aTableDef.indices[indexName];
+
+        try {
+          let indexSql = "CREATE INDEX " + indexName + " ON " +
+            aTableDef._realName + " (" + indexColumns.join(", ") + ")";
+          this.asyncConnection.executeSimpleSQL(indexSql);
+        }
+        catch (ex) {
+          this._log.error("Problem creating index " + indexName + " for " +
+            "table " + aTableDef.name + " because " + ex + " at " +
+            ex.fileName + ":" + ex.lineNumber);
+        }
+      }
+    }
+
+    return new GlodaDatabind(aTableDef, this);
+  },
+
+  _migrate: function gloda_ds_migrate(aDBService, aDBFile, aDBConnection,
+                                      aCurVersion, aNewVersion) {
+    // we purged our way up to version 8, so we can/must purge prior to 8.
+    if (aCurVersion < 8) {
+      aDBConnection.close();
+      aDBFile.remove(false);
+      this._log.warn("Global database has been purged due to schema change.");
+      return this._createDB(aDBService, aDBFile);
+    }
+    // version 9 just adds the contactAttributes table
+    if (aCurVersion < 9) {
+      this._createTableSchema(aDBConnection, "contactAttributes");
+    }
+    // version 10:
+    // we have so many changes here, not to mention semantic changes, that
+    //  purging is the right answer.
+    // - adds dirtyStatus, name to folderLocations
+    // - removes messageAttribFetch index from messageAttributes
+    // - removes conversationAttribFetch index from messageAttributes
+    // - removes contactAttribFetch index from contactAttributes
+    // - adds jsonAttributes column to messages table
+    // - adds jsonAttributes column to contacts table
+    if (aCurVersion < 10) {
+      aDBConnection.close();
+      aDBFile.remove(false);
+      this._log.warn("Global database has been purged due to schema change.");
+      return this._createDB(aDBService, aDBFile);
+    }
+    
+    aDBConnection.schemaVersion = aNewVersion;
+    
+    return aDBConnection;
+  },
+
+  _outstandingAsyncStatements: [],
+
+  _createAsyncStatement: function gloda_ds_createAsyncStatement(aSQLString,
+                                                                aWillFinalize) {
+    let statement = null;
+    try {
+      statement = this.asyncConnection.createStatement(aSQLString);
+    }
+    catch(ex) {
+       throw("error creating async statement " + aSQLString + " - " +
+             this.asyncConnection.lastError + ": " +
+             this.asyncConnection.lastErrorString + " - " + ex);
+    }
+
+    if (!aWillFinalize)
+      this._outstandingAsyncStatements.push(statement);
+
+    return statement;
+  },
+
+  _cleanupAsyncStatements: function gloda_ds_cleanupAsyncStatements() {
+    [stmt.finalize() for each
+     ([i, stmt] in Iterator(this._outstandingAsyncStatements))];
+  },
+
+  _outstandingSyncStatements: [],
+
+  _createSyncStatement: function gloda_ds_createSyncStatement(aSQLString,
+                                                              aWillFinalize) {
+    let statement = null;
+    try {
+      statement = this.syncConnection.createStatement(aSQLString);
+    }
+    catch(ex) {
+       throw("error creating sync statement " + aSQLString + " - " +
+             this.syncConnection.lastError + ": " +
+             this.syncConnection.lastErrorString + " - " + ex);
+    }
+
+    if (!aWillFinalize)
+      this._outstandingSyncStatements.push(statement);
+
+    return statement;
+  },
+
+  _cleanupSyncStatements: function gloda_ds_cleanupSyncStatements() {
+    [stmt.finalize() for each
+     ([i, stmt] in Iterator(this._outstandingSyncStatements))];
+  },
+
+  /**
+   * Perform a synchronous executeStep on the statement, handling any
+   *  SQLITE_BUSY fallout that could conceivably happen from a collision on our
+   *  read with the async writes.
+   * Basically we keep trying until we succeed or run out of tries.
+   * We believe this to be a reasonable course of action because we don't
+   *  expect this to happen much.
+   */
+  _syncStep: function gloda_ds_syncStep(aStatement) {
+    let tries = 0;
+    while (tries < 32000) {
+      try {
+        return aStatement.executeStep();
+      }
+      // SQLITE_BUSY becomes NS_ERROR_FAILURE
+      catch (e if e.result == 0x80004005) {
+        tries++;
+        // we really need to delay here, somehow.  unfortunately, we can't
+        //  allow event processing to happen, and most of the things we could
+        //  do to delay ourselves result in event processing happening.  (Use
+        //  of a timer, a synchronous dispatch, etc.)
+        // in theory, nsIThreadEventFilter could allow us to stop other events
+        //  that aren't our timer from happening, but it seems slightly
+        //  dangerous and 'notxpcom' suggests it ain't happening anyways...
+        // so, let's just be dumb and hope that the underlying file I/O going
+        //  on makes us more likely to yield to the other thread so it can
+        //  finish what it is doing...
+      }
+    }
+    this._log.error("Synchronous step gave up after " + tries + " tries.");
+  },
+
+  /**
+   * Helper to bind based on the actual type of the javascript value.  Note
+   *  that we always use int64 because under the hood sqlite just promotes the
+   *  normal 'int' call to 'int64' anyways.
+   */
+  _bindVariant: function gloda_ds_bindBlob(aStatement, aIndex, aVariant) {
+    if (aVariant == null) // catch both null and undefined
+      aStatement.bindNullParameter(aIndex);
+    else if (typeof aVariant == "string")
+      aStatement.bindStringParameter(aIndex, aVariant);
+    else if (typeof aVariant == "number") {
+      // we differentiate for storage representation reasons only.
+      if (Math.floor(aVariant) === aVariant)
+        aStatement.bindInt64Parameter(aIndex, aVariant);
+      else
+        aStatement.bindDoubleParameter(aIndex, aVariant);
+    }
+    else
+      throw("Attempt to bind variant with unsupported type: " +
+            (typeof aVariant));
+  },
+
+  _getVariant: function gloda_ds_getBlob(aRow, aIndex) {
+    let typeOfIndex = aRow.getTypeOfIndex(aIndex);
+    if (typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      return null;
+    // XPConnect would just end up going through an intermediary double stage
+    //  for the int64 case anyways...
+    else if (typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER ||
+             typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_DOUBLE)
+      return aRow.getDouble(aIndex);
+    else // typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_TEXT
+      return aRow.getString(aIndex);
+  },
+
+  /** Simple nested transaction support as a performance optimization. */
+  _transactionDepth: 0,
+  _transactionGood: false,
+
+  get _beginTransactionStatement() {
+    let statement = this._createAsyncStatement("BEGIN TRANSACTION");
+    this.__defineGetter__("_beginTransactionStatement", function() statement);
+    return this._beginTransactionStatement;
+  },
+
+  get _commitTransactionStatement() {
+    let statement = this._createAsyncStatement("COMMIT");
+    this.__defineGetter__("_commitTransactionStatement", function() statement);
+    return this._commitTransactionStatement;
+  },
+
+  get _rollbackTransactionStatement() {
+    let statement = this._createAsyncStatement("ROLLBACK");
+    this.__defineGetter__("_rollbackTransactionStatement", function() statement);
+    return this._rollbackTransactionStatement;
+  },
+
+  _pendingPostCommitCallbacks: null,
+  /**
+   * Register a callback to be invoked when the current transaction's commit
+   *  completes.
+   */
+  runPostCommit: function gloda_ds_runPostCommit(aCallback) {
+    this._pendingPostCommitCallbacks.push(aCallback);
+  },
+
+  /**
+   * Begin a potentially nested transaction; only the outermost transaction gets
+   *  to be an actual transaction, and the failure of any nested transaction
+   *  results in a rollback of the entire outer transaction.  If you really
+   *  need an atomic transaction
+   */
+  _beginTransaction: function gloda_ds_beginTransaction() {
+    if (this._transactionDepth == 0) {
+      this._pendingPostCommitCallbacks = [];
+      this._beginTransactionStatement.executeAsync(this.trackAsync());
+      this._transactionGood = true;
+    }
+    this._transactionDepth++;
+  },
+  /**
+   * Commit a potentially nested transaction; if we are the outer-most
+   *  transaction and no sub-transaction issues a rollback
+   *  (via _rollbackTransaction) then we commit, otherwise we rollback.
+   */
+  _commitTransaction: function gloda_ds_commitTransaction() {
+    this._transactionDepth--;
+    if (this._transactionDepth == 0) {
+      try {
+        if (this._transactionGood)
+          this._commitTransactionStatement.executeAsync(
+            new PostCommitHandler(this._pendingPostCommitCallbacks));
+        else
+          this._rollbackTransactionStatement.executeAsync(this.trackAsync());
+      }
+      catch (ex) {
+        this._log.error("Commit problem: " + ex);
+      }
+      this._pendingPostCommitCallbacks = [];
+    }
+  },
+  /**
+   * Abort the commit of the potentially nested transaction.  If we are not the
+   *  outermost transaction, we set a flag that tells the outermost transaction
+   *  that it must roll back.
+   */
+  _rollbackTransaction: function gloda_ds_rollbackTransaction() {
+    this._transactionDepth--;
+    this._transactionGood = false;
+    if (this._transactionDepth == 0) {
+      try {
+        this._rollbackTransactionStatement.executeAsync(this.trackAsync());
+      }
+      catch (ex) {
+        this._log.error("Rollback problem: " + ex);
+      }
+    }
+  },
+
+  _pendingAsyncStatements: 0,
+  /**
+   * The function to call, if any, when we hit 0 pending async statements.
+   */
+  _pendingAsyncCompletedListener: null,
+  _asyncCompleted: function () {
+    if (--this._pendingAsyncStatements == 0) {
+      if (this._pendingAsyncCompletedListener !== null) {
+        this._pendingAsyncCompletedListener();
+        this._pendingAsyncCompletedListener = null;
+      }
+    }
+  },
+  _asyncTrackerListener: {
+    handleResult: function () {},
+    handleError: function() {},
+    handleCompletion: function () {
+      // the helper method exists because the other classes need to call it too
+      GlodaDatastore._asyncCompleted();
+    }
+  },
+  /**
+   * Increments _pendingAsyncStatements and returns a listener that will
+   *  decrement the value when the statement completes.
+   */
+  trackAsync: function() {
+    this._pendingAsyncStatements++;
+    return this._asyncTrackerListener;
+  },
+
+  /* ********** Attribute Definitions ********** */
+  /** Maps (attribute def) compound names to the GlodaAttributeDBDef objects. */
+  _attributeDBDefs: {},
+  /** Map attribute ID to the definition and parameter value that produce it. */
+  _attributeIDToDBDefAndParam: {},
+  /**
+   * We maintain the attributeDefinitions next id counter mainly because we can.
+   *  Since we mediate the access, there's no real risk to doing so, and it
+   *  allows us to keep the writes on the async connection without having to
+   *  wait for a completion notification.
+   */
+  _nextAttributeId: 1,
+
+  _populateAttributeDefManagedId: function () {
+    let stmt = this._createSyncStatement(
+      "SELECT MAX(id) FROM attributeDefinitions", true);
+    if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+      this._nextAttributeId = stmt.getInt64(0) + 1;
+    }
+    stmt.finalize();
+  },
+
+  get _insertAttributeDefStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO attributeDefinitions (id, attributeType, extensionName, \
+                                  name, parameter) \
+              VALUES (?1, ?2, ?3, ?4, ?5)");
+    this.__defineGetter__("_insertAttributeDefStatement", function() statement);
+    return this._insertAttributeDefStatement;
+  },
+
+  /**
+   * Create an attribute definition and return the row ID.  Special/atypical
+   *  in that it doesn't directly return a GlodaAttributeDBDef; we leave that up
+   *  to the caller since they know much more than actually needs to go in the
+   *  database.
+   *
+   * @return The attribute id allocated to this attribute.
+   */
+  _createAttributeDef: function gloda_ds_createAttributeDef(aAttrType,
+                                    aExtensionName, aAttrName, aParameter) {
+    let attributeId = this._nextAttributeId++;
+
+    let iads = this._insertAttributeDefStatement;
+    iads.bindInt64Parameter(0, attributeId);
+    iads.bindInt64Parameter(1, aAttrType);
+    iads.bindStringParameter(2, aExtensionName);
+    iads.bindStringParameter(3, aAttrName);
+    this._bindVariant(iads, 4, aParameter);
+
+    iads.executeAsync(this.trackAsync());
+
+    return attributeId;
+  },
+
+  /**
+   * Sync-ly look-up all the attribute definitions, populating our authoritative
+   *  _attributeDBDefss and _attributeIDToDBDefAndParam maps.  (In other words,
+   *  once this method is called, those maps should always be in sync with the
+   *  underlying database.)
+   */
+  getAllAttributes: function gloda_ds_getAllAttributes() {
+    let stmt = this._createSyncStatement(
+      "SELECT id, attributeType, extensionName, name, parameter \
+         FROM attributeDefinitions", true);
+
+    // map compound name to the attribute
+    let attribs = {};
+    // map the attribute id to [attribute, parameter] where parameter is null
+    //  in cases where parameter is unused.
+    let idToAttribAndParam = {}
+
+    this._log.info("loading all attribute defs");
+
+    while (stmt.executeStep()) {  // no chance of this SQLITE_BUSY on this call
+      let rowId = stmt.getInt64(0);
+      let rowAttributeType = stmt.getInt64(1);
+      let rowExtensionName = stmt.getString(2);
+      let rowName = stmt.getString(3);
+      let rowParameter = this._getVariant(stmt, 4);
+
+      let compoundName = rowExtensionName + ":" + rowName;
+
+      let attrib;
+      if (compoundName in attribs) {
+        attrib = attribs[compoundName];
+      } else {
+        attrib = new GlodaAttributeDBDef(this, /* aID */ null,
+          compoundName, rowAttributeType, rowExtensionName, rowName);
+        attribs[compoundName] = attrib;
+      }
+      // if the parameter is null, the id goes on the attribute def, otherwise
+      //  it is a parameter binding and goes in the binding map.
+      if (rowParameter == null) {
+        this._log.debug(compoundName + " primary: " + rowId);
+        attrib._id = rowId;
+        idToAttribAndParam[rowId] = [attrib, null];
+      } else {
+        this._log.debug(compoundName + " binding: " + rowParameter +
+            " = " + rowId);
+        attrib._parameterBindings[rowParameter] = rowId;
+        idToAttribAndParam[rowId] = [attrib, rowParameter];
+      }
+    }
+    stmt.finalize();
+
+    this._log.info("done loading all attribute defs");
+
+    this._attributeDBDefs = attribs;
+    this._attributeIDToDBDefAndParam = idToAttribAndParam;
+  },
+
+  /**
+   * Helper method for GlodaAttributeDBDef to tell us when their bindParameter
+   *  method is called and they have created a new binding (using
+   *  GlodaDatastore._createAttributeDef).  In theory, that method could take
+   *  an additional argument and obviate the need for this method.
+   */
+  reportBinding: function gloda_ds_reportBinding(aID, aAttrDef, aParamValue) {
+    this._attributeIDToDBDefAndParam[aID] = [aAttrDef, aParamValue];
+  },
+
+  /* ********** Folders ********** */
+  /** next folder (row) id to issue, populated by _getAllFolderMappings. */
+  _nextFolderId: 1,
+
+  get _insertFolderLocationStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO folderLocations (id, folderURI, dirtyStatus, name) VALUES \
+        (?1, ?2, ?3, ?4)");
+    this.__defineGetter__("_insertFolderLocationStatement",
+      function() statement);
+    return this._insertFolderLocationStatement;
+  },
+
+  /**
+   * Authoritative map from folder URI to folder ID.  (Authoritative in the
+   *  sense that this map exactly represents the state of the underlying
+   *  database.  If it does not, it's a bug in updating the database.)
+   */
+  _folderByURI: {},
+  /** Authoritative map from folder ID to folder URI */
+  _folderByID: {},
+
+  /** Intialize our _folderByURI/_folderByID mappings, called by _init(). */
+  _getAllFolderMappings: function gloda_ds_getAllFolderMappings() {
+    let stmt = this._createSyncStatement(
+      "SELECT id, folderURI, dirtyStatus, name FROM folderLocations", true);
+
+    while (stmt.executeStep()) {  // no chance of this SQLITE_BUSY on this call
+      let folderID = stmt.getInt64(0);
+      let folderURI = stmt.getString(1);
+      let dirtyStatus = stmt.getInt32(2);
+      let folderName = stmt.getString(3);
+      
+      let folder = new GlodaFolder(this, folderID, folderURI, dirtyStatus,
+                                   folderName);
+      
+      this._folderByURI[folderURI] = folder;
+      this._folderByID[folderID] = folder;
+
+      if (folderID >= this._nextFolderId)
+        this._nextFolderId = folderID + 1;
+    }
+    stmt.finalize();
+  },
+
+  _folderKnown: function gloda_ds_folderKnown(aFolder) {
+    let folderURI = aFolder.URI;
+    return folderURI in this._folderByURI;
+  },
+
+  /**
+   * Map a folder URI to a folder ID, creating the mapping if it does not yet
+   *  exist.
+   */
+  _mapFolder: function gloda_ds_mapFolderURI(aFolder) {
+    let folderURI = aFolder.URI;
+    if (folderURI in this._folderByURI) {
+      return this._folderByURI[folderURI];
+    }
+
+    let folderID = this._nextFolderId++;
+    
+    let folder = new GlodaFolder(this, folderID, folderURI,
+      GlodaFolder.prototype.kFolderFilthy, aFolder.prettiestName);
+    
+    this._insertFolderLocationStatement.bindInt64Parameter(0, folder.id)
+    this._insertFolderLocationStatement.bindStringParameter(1, folder.uri);
+    this._insertFolderLocationStatement.bindInt64Parameter(2,
+                                                           folder.dirtyStatus);
+    this._insertFolderLocationStatement.bindStringParameter(3, folder.name);
+    this._insertFolderLocationStatement.executeAsync(this.trackAsync());
+
+    this._folderByURI[folderURI] = folder;
+    this._folderByID[folderID] = folder;
+    this._log.debug("!! mapped " + folder.id + " from " + folderURI);
+    return folder;
+  },
+
+  _mapFolderID: function gloda_ds_mapFolderID(aFolderID) {
+    if (aFolderID === null)
+      return null;
+    if (aFolderID in this._folderByID)
+      return this._folderByID[aFolderID];
+    throw "Got impossible folder ID: " + aFolderID;
+  },
+
+  get _updateFolderDirtyStatusStatement() {
+    let statement = this._createAsyncStatement(
+      "UPDATE folderLocations SET dirtyStatus = ?1 \
+              WHERE id = ?2");
+    this.__defineGetter__("_updateFolderDirtyStatusStatement",
+      function() statement);
+    return this._updateFolderDirtyStatusStatement;
+  },
+
+  updateFolderDirtyStatus: function gloda_ds_updateFolderDirtyStatus(aFolder) {
+    let ufds = this._updateFolderDirtyStatusStatement;
+    ufds.bindInt64Parameter(1, aFolder.id);
+    ufds.bindInt64Parameter(0, aFolder.dirtyStatus);
+    ufds.executeAsync(this.trackAsync());
+  },
+
+  get _updateFolderLocationStatement() {
+    let statement = this._createAsyncStatement(
+      "UPDATE folderLocations SET folderURI = ?1 \
+              WHERE id = ?2");
+    this.__defineGetter__("_updateFolderLocationStatement",
+      function() statement);
+    return this._updateFolderLocationStatement;
+  },
+
+  /**
+   * Non-recursive asynchronous folder renaming based on the URI.
+   *
+   * @TODO provide a mechanism for recursive folder renames or have a higher
+   *     layer deal with it and remove this note.
+   */
+  renameFolder: function gloda_ds_renameFolder(aOldFolder, aNewURI) {
+    let folder = this._mapFolder(aOldFolder); // ensure the folder is mapped
+    let oldURI = folder.uri; 
+    this._folderByURI[aNewURI] = folder;
+    folder._uri = aNewURI;
+    this._log.info("renaming folder URI " + oldURI + " to " + aNewURI);
+    this._updateFolderLocationStatement.bindStringParameter(1, folder.id);
+    this._updateFolderLocationStatement.bindStringParameter(0, aNewURI);
+    this._updateFolderLocationStatement.executeAsync(this.trackAsync());
+    
+    delete this._folderByURI[oldURI];
+  },
+
+  get _deleteFolderByIDStatement() {
+    let statement = this._createAsyncStatement(
+      "DELETE FROM folderLocations WHERE id = ?1");
+    this.__defineGetter__("_deleteFolderByIDStatement",
+      function() statement);
+    return this._deleteFolderByIDStatement;
+  },
+
+  deleteFolderByID: function gloda_ds_deleteFolder(aFolderID) {
+    let dfbis = this._deleteFolderByIDStatement;
+    dfbis.bindInt64Parameter(0, aFolderID);
+    dfbis.executeAsync(this.trackAsync());
+  },
+
+  /* ********** Conversation ********** */
+  /** The next conversation id to allocate.  Initialize at startup. */
+  _nextConversationId: 1,
+
+  _populateConversationManagedId: function () {
+    let stmt = this._createSyncStatement(
+      "SELECT MAX(id) FROM conversations", true);
+    if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+      this._nextConversationId = stmt.getInt64(0) + 1;
+    }
+    stmt.finalize();
+  },
+
+  get _insertConversationStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO conversations (id, subject, oldestMessageDate, \
+                                  newestMessageDate) \
+              VALUES (?1, ?2, ?3, ?4)");
+    this.__defineGetter__("_insertConversationStatement", function() statement);
+    return this._insertConversationStatement;
+  },
+
+  get _insertConversationTextStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO conversationsText (docid, subject) \
+              VALUES (?1, ?2)");
+    this.__defineGetter__("_insertConversationTextStatement",
+      function() statement);
+    return this._insertConversationTextStatement;
+  },
+
+  /**
+   * Asynchronously create a conversation.
+   */
+  createConversation: function gloda_ds_createConversation(aSubject,
+        aOldestMessageDate, aNewestMessageDate) {
+
+    // create the data row
+    let conversationID = this._nextConversationId++;
+    let ics = this._insertConversationStatement;
+    ics.bindInt64Parameter(0, conversationID);
+    ics.bindStringParameter(1, aSubject);
+    if (aOldestMessageDate == null)
+      ics.bindNullParameter(2);
+    else
+      ics.bindInt64Parameter(2, aOldestMessageDate);
+    if (aNewestMessageDate == null)
+      ics.bindNullParameter(3);
+    else
+      ics.bindInt64Parameter(3, aNewestMessageDate);
+    ics.executeAsync(this.trackAsync());
+
+    // create the fulltext row, using the same rowid/docid
+    let icts = this._insertConversationTextStatement;
+    icts.bindInt64Parameter(0, conversationID);
+    icts.bindStringParameter(1, aSubject);
+    icts.executeAsync(this.trackAsync());
+
+    // create it
+    let conversation = new GlodaConversation(this, conversationID,
+                                 aSubject, aOldestMessageDate,
+                                 aNewestMessageDate);
+    // it's new! let the collection manager know about it.
+    GlodaCollectionManager.itemsAdded(conversation.NOUN_ID, [conversation]);
+    // return it
+    return conversation;
+  },
+
+  get _deleteConversationByIDStatement() {
+    let statement = this._createAsyncStatement(
+      "DELETE FROM conversations WHERE id = ?1");
+    this.__defineGetter__("_deleteConversationByIDStatement",
+                          function() statement);
+    return this._deleteConversationByIDStatement;
+  },
+
+  /**
+   * Asynchronously delete a conversation given its ID.
+   */
+  deleteConversationByID: function gloda_ds_deleteConversationByID(
+                                      aConversationID) {
+    let dcbids = this._deleteConversationByIDStatement;
+    dcbids.bindInt64Parameter(0, aConversationID);
+    dcbids.executeAsync(this.trackAsync());
+
+    // TODO: collection manager implications
+    //GlodaCollectionManager.removeByID()
+  },
+
+  get _selectConversationByIDStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT id, subject, oldestMessageDate, newestMessageDate \
+         FROM conversations WHERE id = ?1");
+    this.__defineGetter__("_selectConversationByIDStatement",
+      function() statement);
+    return this._selectConversationByIDStatement;
+  },
+
+  _conversationFromRow: function gloda_ds_conversationFromRow(aStmt) {
+      let oldestMessageDate, newestMessageDate;
+      if (aStmt.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+        oldestMessageDate = null;
+      else
+        oldestMessageDate = aStmt.getInt64(2);
+      if (aStmt.getTypeOfIndex(3) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+        newestMessageDate = null;
+      else
+        newestMessageDate = aStmt.getInt64(3);
+      return new GlodaConversation(this, aStmt.getInt64(0),
+        aStmt.getString(1), oldestMessageDate, newestMessageDate);
+  },
+
+  /**
+   * Synchronously look up a conversation given its ID.
+   */
+  getConversationByID: function gloda_ds_getConversationByID(aConversationID) {
+    let conversation = GlodaCollectionManager.cacheLookupOne(
+      GlodaConversation.prototype.NOUN_ID, aConversationID);
+
+    if (conversation === null) {
+      let scbids = this._selectConversationByIDStatement;
+
+      scbids.bindInt64Parameter(0, aConversationID);
+      if (this._syncStep(scbids)) {
+        conversation = this._conversationFromRow(scbids);
+        GlodaCollectionManager.itemLoaded(conversation);
+      }
+      scbids.reset();
+    }
+
+    return conversation;
+  },
+
+  /* ********** Message ********** */
+  /**
+   * Next message id, managed because of our use of asynchronous inserts.
+   * Initialized by _populateMessageManagedId called by _init.
+   */
+  _nextMessageId: 1,
+
+  _populateMessageManagedId: function () {
+    let stmt = this._createSyncStatement(
+      "SELECT MAX(id) FROM messages", true);
+    if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+      this._nextMessageId = stmt.getInt64(0) + 1;
+    }
+    stmt.finalize();
+  },
+
+  get _insertMessageStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO messages (id, folderID, messageKey, conversationID, date, \
+                             headerMessageID, jsonAttributes) \
+              VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)");
+    this.__defineGetter__("_insertMessageStatement", function() statement);
+    return this._insertMessageStatement;
+  },
+
+  get _insertMessageTextStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO messagesText (docid, subject, body, attachmentNames) \
+              VALUES (?1, ?2, ?3, ?4)");
+    this.__defineGetter__("_insertMessageTextStatement", function() statement);
+    return this._insertMessageTextStatement;
+  },
+
+  /**
+   * Create a GlodaMessage with the given properties.  Because this is only half
+   *  of the process of creating a message (the attributes still need to be
+   *  completed), it's on the caller's head to call GlodaCollectionManager's
+   *  itemAdded method once the message is fully created.
+   *
+   * This method uses the async connection, any downstream logic that depends on
+   *  this message actually existing in the database must be done using an
+   *  async query.
+   */
+  createMessage: function gloda_ds_createMessage(aFolder, aMessageKey,
+                              aConversationID, aDatePRTime, aHeaderMessageID) {
+    let folderID;
+    if (aFolder != null) {
+      folderID = this._mapFolder(aFolder).id;
+    }
+    else {
+      folderID = null;
+    }
+
+    let messageID = this._nextMessageId++;
+
+    let message = new GlodaMessage(this, messageID, folderID,
+                            aMessageKey, aConversationID, null,
+                            aDatePRTime ? new Date(aDatePRTime / 1000) : null,
+                            aHeaderMessageID);
+
+    this._log.debug("CreateMessage: " + folderID + ", " + aMessageKey + ", " +
+                    aConversationID + ", " + aDatePRTime + ", " +
+                    aHeaderMessageID);
+
+    // We would love to notify the collection manager about the message at this
+    //  point (at least if it's not a ghost), but we can't yet.  We need to wait
+    //  until the attributes have been indexed, which means it's out of our
+    //  hands.  (Gloda.processMessage does it.)
+
+    return message;
+  },
+  
+  insertMessage: function gloda_ds_insertMessage(aMessage) {
+    let ims = this._insertMessageStatement;
+    ims.bindInt64Parameter(0, aMessage.id);
+    if (aMessage.folderID == null)
+      ims.bindNullParameter(1);
+    else
+      ims.bindInt64Parameter(1, aMessage.folderID);
+    if (aMessage.messageKey == null)
+      ims.bindNullParameter(2);
+    else
+      ims.bindInt64Parameter(2, aMessage.messageKey);
+    ims.bindInt64Parameter(3, aMessage.conversationID);
+    if (aMessage.date == null)
+      ims.bindNullParameter(4);
+    else
+      ims.bindInt64Parameter(4, aMessage.date * 1000);
+    ims.bindStringParameter(5, aMessage.headerMessageID);
+    if (aMessage._jsonText)
+      ims.bindStringParameter(6, aMessage._jsonText);
+    else
+      ims.bindNullParameter(6);
+
+    try {
+       ims.executeAsync(this.trackAsync());
+    }
+    catch(ex) {
+       throw("error executing statement... " +
+             this.asyncConnection.lastError + ": " +
+             this.asyncConnection.lastErrorString + " - " + ex);
+    }
+
+    // we only create the full-text row if the body is non-null.
+    // so, even though body might be null, we still want to create the
+    //  full-text search row
+    if (aMessage._bodyLines) {
+      let bodyText;
+      if (aMessage._content && aMessage._content.hasContent())
+        bodyText = aMessage._content.getContentString(true);
+      else
+        bodyText = aMessage._bodyLines.join("\n");
+      
+      let imts = this._insertMessageTextStatement;
+      imts.bindInt64Parameter(0, aMessage.id);
+      imts.bindStringParameter(1, aMessage._subject);
+      imts.bindStringParameter(2, bodyText);
+      if (aMessage._attachmentNames === null)
+        imts.bindNullParameter(3);
+      else
+        imts.bindStringParameter(3, aMessage._attachmentNames);
+      
+      try {
+         imts.executeAsync(this.trackAsync());
+      }
+      catch(ex) {
+         throw("error executing fulltext statement... " +
+               this.asyncConnection.lastError + ": " +
+               this.asyncConnection.lastErrorString + " - " + ex);
+      }
+    }
+  },
+
+  get _updateMessageStatement() {
+    let statement = this._createAsyncStatement(
+      "UPDATE messages SET folderID = ?1, \
+                           messageKey = ?2, \
+                           conversationID = ?3, \
+                           date = ?4, \
+                           headerMessageID = ?5, \
+                           jsonAttributes = ?6 \
+              WHERE id = ?7");
+    this.__defineGetter__("_updateMessageStatement", function() statement);
+    return this._updateMessageStatement;
+  },
+
+  /**
+   * Update the database row associated with the message.  If aBody is supplied,
+   *  the associated full-text row is created; it is assumed that it did not
+   *  previously exist.
+   */
+  updateMessage: function gloda_ds_updateMessage(aMessage) {
+    let ums = this._updateMessageStatement;
+    ums.bindInt64Parameter(6, aMessage.id);
+    if (aMessage.folderID === null)
+      ums.bindNullParameter(0);
+    else
+      ums.bindInt64Parameter(0, aMessage.folderID);
+    if (aMessage.messageKey === null)
+      ums.bindNullParameter(1);
+    else
+      ums.bindInt64Parameter(1, aMessage.messageKey);
+    ums.bindInt64Parameter(2, aMessage.conversationID);
+    if (aMessage.date === null)
+      ums.bindNullParameter(3);
+    else
+      ums.bindInt64Parameter(3, aMessage.date * 1000);
+    ums.bindStringParameter(4, aMessage.headerMessageID);
+    if (aMessage._jsonText)
+      ums.bindStringParameter(5, aMessage._jsonText);
+    else
+      ums.bindNullParameter(5);
+
+    ums.executeAsync(this.trackAsync());
+
+    if (aMessage._isNew && aMessage._bodyLines) {
+      let bodyText;
+      if (aMessage._content && aMessage._content.hasContent())
+        bodyText = aMessage._content.getContentString(true);
+      else
+        bodyText = aMessage._bodyLines.join("\n");
+      
+      let imts = this._insertMessageTextStatement;
+      imts.bindInt64Parameter(0, aMessage.id);
+      imts.bindStringParameter(1, aMessage._subject);
+      imts.bindStringParameter(2, bodyText);
+      if (aMessage._attachmentNames === null)
+        imts.bindNullParameter(3);
+      else
+        imts.bindStringParameter(3, aMessage._attachmentNames);
+      
+      try {
+         imts.executeAsync(this.trackAsync());
+      }
+      catch(ex) {
+         throw("error executing fulltext statement... " +
+               this.asyncConnection.lastError + ": " +
+               this.asyncConnection.lastErrorString + " - " + ex);
+      }
+    }
+
+    // In completely abstract theory, this is where we would call
+    //  GlodaCollectionManager.itemsModified, except that the attributes may
+    //  also have changed, so it's out of our hands.  (Gloda.grokNoun
+    //  handles it.)
+  },
+
+  get _updateMessageLocationStatement() {
+    let statement = this._createAsyncStatement(
+      "UPDATE messages SET folderID = ?1, messageKey = ?2 WHERE id = ?3");
+    this.__defineGetter__("_updateMessageLocationStatement",
+                          function() statement);
+    return this._updateMessageLocationStatement;
+  },
+
+  /**
+   * Given a list of gloda message ids, and a list of their new message keys in
+   *  the given new folder location, asynchronously update the message's
+   *  database locations.  Also, update the in-memory representations.
+   */
+  updateMessageLocations: function gloda_ds_updateMessageLocations(aMessageIds,
+      aNewMessageKeys, aDestFolder) {
+    let statement = this._updateMessageLocationStatement;
+    let destFolderID = this._mapFolder(aDestFolder).id;
+
+    let modifiedItems = [];
+
+    for (let iMsg = 0; iMsg < aMessageIds.length; iMsg++) {
+      let id = aMessageIds[iMsg]
+      statement.bindInt64Parameter(0, destFolderID);
+      statement.bindInt64Parameter(1, aNewMessageKeys[iMsg]);
+      statement.bindInt64Parameter(2, id);
+      statement.executeAsync(this.trackAsync());
+
+      // so, if the message is currently loaded, we also need to change it up...
+      let message = GlodaCollectionManager.cacheLookupOne(
+        GlodaMessage.prototype.NOUN_ID, id);
+      if (message) {
+        message._folderID = destFolderID;
+        modifiedItems.push(message);
+      }
+    }
+
+    // if we're talking about a lot of messages, it's worth committing after
+    //  this to ensure that we don't spill to disk and cause contention with
+    //  synchronous reads off (this) the main thread.
+    if ((aMessageIds.length > 200) && this._transactionDepth) {
+      this._commitTransaction();
+      this._beginTransaction();
+    }
+
+    // tell the collection manager about the modified messages so it can update
+    //  any existing views...
+    if (modifiedItems.length) {
+      GlodaCollectionManager.itemsModified(GlodaMessage.prototype.NOUN,
+                                           modifiedItems);
+    }
+  },
+
+  /**
+   * Asynchronously mutate message folder id/message keys for the given
+   *  messages, indicating that we are moving them to the target folder, but
+   *  don't yet know their target message keys.
+   */
+  updateMessageFoldersByKeyPurging:
+      function gloda_ds_updateMessageFoldersByKeyPurging(aSrcFolder,
+        aMessageKeys, aDestFolder) {
+    let srcFolderID = this._mapFolder(aSrcFolder).id;
+    let destFolderID = this._mapFolder(aDestFolder).id;
+
+    let sqlStr = "UPDATE messages SET folderID = ?1, \
+                                      messageKey = ?2 \
+                   WHERE folderID = ?3 \
+                     AND messageKey IN (" + aMessageKeys.join(", ") + ")";
+    let statement = this._createAsyncStatement(sqlStr, true);
+    statement.bindInt64Parameter(2, srcFolderID);
+    statement.bindInt64Parameter(0, destFolderID);
+    statement.bindNullParameter(1);
+    statement.executeAsync(this.trackAsync());
+    statement.finalize();
+
+    // if we're talking about a lot of messages, it's worth committing after
+    //  this to ensure that we don't spill to disk and cause contention with
+    //  synchronous reads off (this) the main thread.
+    if ((aMessageKeys.length > 200) && this._transactionDepth) {
+      this._commitTransaction();
+      this._beginTransaction();
+    }
+  },
+
+  _messageFromRow: function gloda_ds_messageFromRow(aRow) {
+    let folderId, messageKey, date, jsonText;
+    if (aRow.getTypeOfIndex(1) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      folderId = null;
+    else
+      folderId = aRow.getInt64(1);
+    if (aRow.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      messageKey = null;
+    else
+      messageKey = aRow.getInt64(2);
+    if (aRow.getTypeOfIndex(4) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      date = null;
+    else
+      date = new Date(aRow.getInt64(4) / 1000);
+    if (aRow.getTypeOfIndex(7) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      jsonText = undefined;
+    else
+      jsonText = aRow.getString(7);
+    return new GlodaMessage(this, aRow.getInt64(0), folderId, messageKey,
+                            aRow.getInt64(3), null, date, aRow.getString(5),
+                            aRow.getInt64(6), jsonText);
+  },
+
+  get _selectMessageByIDStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT id, folderID, messageKey, conversationID, date, headerMessageID, \
+           deleted FROM messages WHERE id = ?1");
+    this.__defineGetter__("_selectMessageByIDStatement",
+      function() statement);
+    return this._selectMessageByIDStatement;
+  },
+
+  /**
+   * Synchronously retrieve the given message given its gloda message id.
+   */
+  getMessageByID: function gloda_ds_getMessageByID(aID) {
+    let message = GlodaCollectionManager.cacheLookupOne(
+      GlodaMessage.prototype.NOUN_ID, aID);
+
+    if (message === null) {
+      let smbis = this._selectMessageByIDStatement;
+
+      smbis.bindInt64Parameter(0, aID);
+      if (this._syncStep(smbis)) {
+        message = this._messageFromRow(smbis);
+        GlodaCollectionManager.itemLoaded(message);
+      }
+      smbis.reset();
+    }
+
+    return message;
+  },
+
+  get _selectMessageByLocationStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT * FROM messages WHERE folderID = ?1 AND messageKey = ?2");
+    this.__defineGetter__("_selectMessageByLocationStatement",
+      function() statement);
+    return this._selectMessageByLocationStatement;
+  },
+
+  /**
+   * Synchronously retrieve the message that we believe to correspond to the
+   *  given message key in the given folder.
+   * @return null on failure to locate the message, the message on success.
+   *
+   * @XXX on failure, attempt to resolve the problem through re-indexing, etc.
+   */
+  getMessageFromLocation: function gloda_ds_getMessageFromLocation(aFolder,
+                                                                 aMessageKey) {
+    this._selectMessageByLocationStatement.bindInt64Parameter(0,
+      this._mapFolder(aFolder).id);
+    this._selectMessageByLocationStatement.bindInt64Parameter(1, aMessageKey);
+
+    let message = null;
+    if (this._syncStep(this._selectMessageByLocationStatement))
+      message = this._messageFromRow(this._selectMessageByLocationStatement);
+    this._selectMessageByLocationStatement.reset();
+
+    if (message === null)
+      this._log.info("Error locating message with key=" + aMessageKey +
+                     " and URI " + aFolder.URI);
+
+    return message && GlodaCollectionManager.cacheLoadUnifyOne(message);
+  },
+
+  get _selectMessageIDsByFolderStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT id FROM messages WHERE folderID = ?1");
+    this.__defineGetter__("_selectMessageIDsByFolderStatement",
+      function() statement);
+    return this._selectMessageIDsByFolderStatement;
+  },
+
+  getMessageIDsByFolderID:
+      function gloda_ds_getMessageIDsFromFolderID(aFolderID) {
+    let messageIDs = [];
+
+    let smidbfs = this._selectMessageIDsByFolderStatement;
+    smidbfs.bindInt64Parameter(0, aFolderID);
+
+    while (this._syncStep(smidbfs)) {
+      messageIDs.push(smidbfs.getInt64(0));
+    }
+    smidbfs.reset();
+
+    return messageIDs;
+  },
+
+  /**
+   * Given a list of Message-ID's, return a matching list of lists of messages
+   *  matching those Message-ID's.  So if you pass an array with three
+   *  Message-ID's ["a", "b", "c"], you would get back an array containing
+   *  3 lists, where the first list contains all the messages with a message-id
+   *  of "a", and so forth.  The reason a list is returned rather than null/a
+   *  message is that we accept the reality that we have multiple copies of
+   *  messages with the same ID.
+   * This call is asynchronous because it depends on previously created messages
+   *  to be reflected in our results, which requires us to execute on the async
+   *  thread where all our writes happen.  This also turns out to be a
+   *  reasonable thing because we could imagine pathological cases where there
+   *  could be a lot of message-id's and/or a lot of messages with those
+   *  message-id's.
+   */
+  getMessagesByMessageID: function gloda_ds_getMessagesByMessageID(aMessageIDs,
+      aCallback, aCallbackThis) {
+    let msgIDToIndex = {};
+    let results = [];
+    for (let iID = 0; iID < aMessageIDs.length; ++iID) {
+      let msgID = aMessageIDs[iID];
+      results.push([]);
+      msgIDToIndex[msgID] = iID;
+    }
+
+    // Unfortunately, IN doesn't work with statement binding mechanisms, and
+    //  a chain of ORed tests really can't be bound unless we create one per
+    //  value of N (seems silly).
+    let quotedIDs = ["'" + msgID.replace("'", "''", "g") + "'" for each
+                     ([i, msgID] in Iterator(aMessageIDs))]
+    let sqlString = "SELECT * FROM messages WHERE headerMessageID IN (" +
+                    quotedIDs + ")";
+    
+    let nounDef = GlodaMessage.prototype.NOUN_DEF;
+    let listener = new MessagesByMessageIdCallback(msgIDToIndex, results,
+        aCallback, aCallbackThis);
+    let query = new nounDef.explicitQueryClass();
+    return this._queryFromSQLString(sqlString, [], nounDef,
+        query, listener);
+  },
+
+  get _updateMessagesMarkDeletedByFolderID() {
+    let statement = this._createAsyncStatement(
+      "UPDATE messages SET folderID = NULL, messageKey = NULL, \
+              deleted = 1 WHERE folderID = ?1");
+    this.__defineGetter__("_updateMessagesMarkDeletedByFolderID",
+      function() statement);
+    return this._updateMessagesMarkDeletedByFolderID;
+  },
+
+  markMessagesDeletedByFolderID:
+      function gloda_ds_markMessagesDeletedByFolderID(aFolderID) {
+    let statement = this._updateMessagesMarkDeletedByFolderID;
+    statement.bindInt64Parameter(0, aFolderID);
+    statement.executeAsync(this.trackAsync());
+    statement.finalize();
+  },
+
+  markMessagesDeletedByIDs: function gloda_ds_markMessagesDeletedByIDs(
+      aMessageIDs) {
+    let sqlString = "UPDATE messages SET deleted = 1 WHERE id IN (" +
+      aMessageIDs.join(",") + ")";
+
+    let statement = this._createAsyncStatement(sqlString, true);
+    statement.executeAsync(this.trackAsync());
+    statement.finalize();
+
+    // some people are inclined to deleting ridiculous numbers of messages at
+    //  a time.  if we are in a transaction, this has the potential to cause us
+    //  to spill the transaction to disk prior to disk, resulting in a lock
+    //  escalation and making any synchronous reads from the main thread need
+    //  to become blocking.  We don't want that, so:
+    // If we are in a transaction and there are a "lot" of messages being
+    //  marked as deleted, issue a commit and then re-open the transaction.
+    if ((aMessageIDs.length > 200) && this._transactionDepth) {
+      this._commitTransaction();
+      this._beginTransaction();
+    }
+  },
+
+  get _deleteMessageByIDStatement() {
+    let statement = this._createAsyncStatement(
+      "DELETE FROM messages WHERE id = ?1");
+    this.__defineGetter__("_deleteMessageByIDStatement",
+                          function() statement);
+    return this._deleteMessageByIDStatement;
+  },
+
+  deleteMessageByID: function gloda_ds_deleteMessageByID(aMessageID) {
+    // TODO: collection manager implications
+    let dmbids = this._deleteMessageByIDStatement;
+    dmbids.bindInt64Parameter(0, aMessageID);
+    dmbids.executeAsync(this.trackAsync());
+  },
+
+  get _deleteMessagesByConversationIDStatement() {
+    let statement = this._createAsyncStatement(
+      "DELETE FROM messages WHERE conversationID = ?1");
+    this.__defineGetter__("_deleteMessagesByConversationIDStatement",
+                          function() statement);
+    return this._deleteMessagesByConversationIDStatement;
+  },
+
+  /**
+   * Delete messages by conversation ID.  For use by the indexer's deletion
+   *  logic, NOT you.
+   */
+  deleteMessagesByConversationID:
+    // TODO: collection manager implications
+      function gloda_ds_deleteMessagesByConversationID(aConversationID) {
+    let dmbcids = this._deleteMessagesByConversationIDStatement;
+    dmbcids.bindInt64Parameter(0, aConversationID);
+    dmbcids.executeAsync(this.trackAsync());
+  },
+
+  get _selectMessagesByConversationIDStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT * FROM messages WHERE conversationID = ?1");
+    this.__defineGetter__("_selectMessagesByConversationIDStatement",
+      function() statement);
+    return this._selectMessagesByConversationIDStatement;
+  },
+
+  get _selectMessagesByConversationIDNoGhostsStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT * FROM messages WHERE conversationID = ?1 AND \
+                                    folderID IS NOT NULL");
+    this.__defineGetter__("_selectMessagesByConversationIDNoGhostsStatement",
+      function() statement);
+    return this._selectMessagesByConversationIDNoGhostsStatement;
+  },
+
+  /**
+   * Retrieve all the messages belonging to the given conversation.  This
+   *  method is used by the indexer and the GlodaConversation class and is not
+   *  intended to be used by any other code.  (Most other code should probably
+   *  use the GlodaConversation.messages attribute or the general purpose query
+   *  mechanism.)
+   *
+   * @param aConversationID The ID of the conversation for which you want all
+   *     the messages.
+   * @param aIncludeGhosts Boolean indicating whether you want 'ghost' messages
+   *     (true) or not (false).  'Ghost' messages are messages that exist in the
+   *     database purely for conversation tracking/threading purposes.  They
+   *     are markers for messages we have not yet seen yet assume must exist
+   *     based on references/in-reply-to headers from non-ghost messages in our
+   *     database.
+   */
+  getMessagesByConversationID: function gloda_ds_getMessagesByConversationID(
+        aConversationID, aIncludeGhosts) {
+    let statement;
+    if (aIncludeGhosts)
+      statement = this._selectMessagesByConversationIDStatement;
+    else
+      statement = this._selectMessagesByConversationIDNoGhostsStatement;
+    statement.bindInt64Parameter(0, aConversationID);
+
+    let messages = [];
+    while (this._syncStep(statement)) {
+      messages.push(this._messageFromRow(statement));
+    }
+    statement.reset();
+
+    if (messages.length)
+      GlodaCollectionManager.cacheLoadUnify(GlodaMessage.prototype.NOUN_ID,
+                                            messages);
+
+    return messages;
+  },
+
+  /* ********** Message Attributes ********** */
+  get _insertMessageAttributeStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO messageAttributes (conversationID, messageID, attributeID, \
+                             value) \
+              VALUES (?1, ?2, ?3, ?4)");
+    this.__defineGetter__("_insertMessageAttributeStatement",
+      function() statement);
+    return this._insertMessageAttributeStatement;
+  },
+
+  get _deleteMessageAttributeStatement() {
+    let statement = this._createAsyncStatement(
+      "DELETE FROM messageAttributes WHERE attributeID = ?1 AND value = ?2 \
+         AND conversationID = ?3 AND messageID = ?4");
+    this.__defineGetter__("_deleteMessageAttributeStatement",
+      function() statement);
+    return this._deleteMessageAttributeStatement;
+  },
+
+  /**
+   * Insert and remove attributes relating to a GlodaMessage.  This is performed
+   *  inside a pseudo-transaction (we create one if we aren't in one, using
+   *  our _beginTransaction wrapper, but if we are in one, no additional
+   *  meaningful semantics are added).
+   * No attempt is made to verify uniqueness of inserted attributes, either
+   *  against the current database or within the provided list of attributes.
+   *  The caller is responsible for ensuring that unwanted duplicates are
+   *  avoided.
+   *
+   * @param aMessage The GlodaMessage the attributes belong to.  This is used
+   *     to provide the message id and conversation id.
+   * @param aAddDBAttributes A list of attribute tuples to add, where each tuple
+   *     contains an attribute ID and a value.  Lest you forget, an attribute ID
+   *     corresponds to a row in the attribute definition table.  The attribute
+   *     definition table stores the 'parameter' for the attribute, if any.
+   *     (Which is to say, our frequent Attribute-Parameter-Value triple has
+   *     the Attribute-Parameter part distilled to a single attribute id.)
+   * @param aRemoveDBAttributes A list of attribute tuples to remove.
+   */
+  adjustMessageAttributes: function gloda_ds_adjustMessageAttributes(aMessage,
+                                        aAddDBAttributes, aRemoveDBAttributes) {
+    let imas = this._insertMessageAttributeStatement;
+    let dmas = this._deleteMessageAttributeStatement;
+    this._beginTransaction();
+    try {
+      for (let iAttrib = 0; iAttrib < aAddDBAttributes.length; iAttrib++) {
+        let attribValueTuple = aAddDBAttributes[iAttrib];
+
+        imas.bindInt64Parameter(0, aMessage.conversationID);
+        imas.bindInt64Parameter(1, aMessage.id);
+        imas.bindInt64Parameter(2, attribValueTuple[0]);
+        // use 0 instead of null, otherwise the db gets upset.  (and we don't
+        //  really care anyways.)
+        if (attribValueTuple[1] == null)
+          imas.bindInt64Parameter(3, 0);
+        else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+          imas.bindInt64Parameter(3, attribValueTuple[1]);
+        else
+          imas.bindDoubleParameter(3, attribValueTuple[1]);
+        imas.executeAsync(this.trackAsync());
+      }
+
+      for (let iAttrib = 0; iAttrib < aRemoveDBAttributes.length; iAttrib++) {
+        let attribValueTuple = aRemoveDBAttributes[iAttrib];
+
+        dmas.bindInt64Parameter(0, attribValueTuple[0]);
+        // use 0 instead of null, otherwise the db gets upset.  (and we don't
+        //  really care anyways.)
+        if (attribValueTuple[1] == null)
+          dmas.bindInt64Parameter(1, 0);
+        else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+          dmas.bindInt64Parameter(1, attribValueTuple[1]);
+        else
+          dmas.bindDoubleParameter(1, attribValueTuple[1]);
+        dmas.bindInt64Parameter(2, aMessage.conversationID);
+        dmas.bindInt64Parameter(3, aMessage.id);
+        dmas.executeAsync(this.trackAsync());
+      }
+
+      this._commitTransaction();
+    }
+    catch (ex) {
+      this._log.error("adjustMessageAttributes:" + ex.lineNumber + ": " + eX);
+      this._rollbackTransaction();
+      throw ex;
+    }
+  },
+
+  get _deleteMessageAttributesByMessageIDStatement() {
+    let statement = this._createAsyncStatement(
+      "DELETE FROM messageAttributes WHERE messageID = ?1");
+    this.__defineGetter__("_deleteMessageAttributesByMessageIDStatement",
+      function() statement);
+    return this._deleteMessageAttributesByMessageIDStatement;
+  },
+
+  /**
+   * Clear all the message attributes for a given GlodaMessage.  No changes
+   *  are made to the in-memory representation of the message; it is up to the
+   *  caller to ensure that it handles things correctly.
+   *
+   * @param aMessage The GlodaMessage whose database attributes should be
+   *     purged.
+   */
+  clearMessageAttributes: function gloda_ds_clearMessageAttributes(aMessage) {
+    if (aMessage.id != null) {
+      this._deleteMessageAttributesByMessageIDStatement.bindInt64Parameter(0,
+        aMessage.id);
+      this._deleteMessageAttributesByMessageIDStatement.executeAsync(
+        this.trackAsync());
+    }
+  },
+
+  get _selectMessageAttributesByMessageIDStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT attributeID, value FROM messageAttributes \
+         WHERE messageID = ?1");
+    this.__defineGetter__("_selectMessageAttributesByMessageIDStatement",
+      function() statement);
+    return this._selectMessageAttributesByMessageIDStatement;
+  },
+
+  /**
+   * Look-up the attributes associated with the given GlodaMessage instance,
+   *  returning them in APV form (a tuple of Attribute definition object,
+   *  attribute Parameter, and attribute Value).
+   *
+   * @param aMessage The GlodaMessage whose attributes you want retrieved.
+   * @return An APV list of the attributes.
+   */
+  getMessageAttributes: function gloda_ds_getMessageAttributes(aMessage) {
+    // A list of [attribute def object, (attr) parameter value, attribute value]
+    let attribParamVals = []
+
+    let smas = this._selectMessageAttributesByMessageIDStatement;
+
+    smas.bindInt64Parameter(0, aMessage.id);
+    while (this._syncStep(smas)) {
+      let attributeID = smas.getInt64(0);
+      if (!(attributeID in this._attributeIDToDBDefAndParam)) {
+        this._log.error("Attribute ID " + attributeID + " not in our map!");
+      }
+      let attribAndParam = this._attributeIDToDBDefAndParam[attributeID];
+      let val = smas.getDouble(1);
+      attribParamVals.push([attribAndParam[0], attribAndParam[1], val]);
+    }
+    smas.reset();
+
+    return attribParamVals;
+  },
+
+  _stringSQLQuoter: function(aString) {
+    return "'" + aString.replace("'", "''", "g") + "'";
+  },
+  _numberQuoter: function(aNum) {
+    return aNum;
+  },
+
+  /* ===== Generic Attribute Support ===== */
+  adjustAttributes: function gloda_ds_adjustAttributes(aItem, aAddDBAttributes,
+      aRemoveDBAttributes) {
+    let nounDef = aItem.NOUN_DEF;
+    let dbMeta = nounDef._dbMeta;
+    if (dbMeta.insertAttrStatement === undefined) {
+      dbMeta.insertAttrStatement = this._createAsyncStatement(
+        "INSERT INTO " + nounDef.attrTableName +
+        " (" + nounDef.attrIDColumnName + ", attributeID, value) " +
+        " VALUES (?1, ?2, ?3)");
+      // we always create this at the same time (right here), no need to check
+      dbMeta.deleteAttrStatement = this._createAsyncStatement(
+        "DELETE FROM " + nounDef.attrTableName + " WHERE " +
+        " attributeID = ?1 AND value = ?2 AND " +
+        nounDef.attrIDColumnName + " = ?3");
+    }
+
+    let ias = dbMeta.insertAttrStatement;
+    let das = dbMeta.deleteAttrStatement;
+    this._beginTransaction();
+    try {
+      for (let iAttr = 0; iAttr < aAddDBAttributes.length; iAttr++) {
+        let attribValueTuple = aAddDBAttributes[iAttr];
+
+        ias.bindInt64Parameter(0, aItem.id);
+        ias.bindInt64Parameter(1, attribValueTuple[0]);
+        // use 0 instead of null, otherwise the db gets upset.  (and we don't
+        //  really care anyways.)
+        if (attribValueTuple[1] == null)
+          ias.bindInt64Parameter(2, 0);
+        else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+          ias.bindInt64Parameter(2, attribValueTuple[1]);
+        else
+          ias.bindDoubleParameter(2, attribValueTuple[1]);
+        ias.executeAsync(this.trackAsync());
+      }
+
+      for (let iAttr = 0; iAttr < aRemoveDBAttributes.length; iAttr++) {
+        let attribValueTuple = aRemoveDBAttributes[iAttr];
+
+        das.bindInt64Parameter(0, attribValueTuple[0]);
+        // use 0 instead of null, otherwise the db gets upset.  (and we don't
+        //  really care anyways.)
+        if (attribValueTuple[1] == null)
+          das.bindInt64Parameter(1, 0);
+        else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+          das.bindInt64Parameter(1, attribValueTuple[1]);
+        else
+          das.bindDoubleParameter(1, attribValueTuple[1]);
+        das.bindInt64Parameter(2, aItem.id);
+        das.executeAsync(this.trackAsync());
+      }
+
+      this._commitTransaction();
+    }
+    catch (ex) {
+      this._log.error("adjustAttributes:" + ex.lineNumber + ": " + eX);
+      this._rollbackTransaction();
+      throw ex;
+    }
+  },
+
+  clearAttributes: function gloda_ds_clearAttributes(aItem) {
+    let nounDef = aItem.NOUN_DEF;
+    let dbMeta = nounMeta._dbMeta;
+    if (dbMeta.clearAttrStatement === undefined) {
+      dbMeta.clearAttrStatement = this._createAsyncStatement(
+        "DELETE FROM " + nounDef.attrTableName + " WHERE " +
+        nounDef.attrIDColumnName + " = ?1");
+    }
+  
+    if (aItem.id != null) {
+      dbMeta.clearAttrStatement.bindInt64Parameter(0, aItem.id);
+      dbMeta.clearAttrStatement.executeAsync(this.trackAsync());
+    }
+  },
+
+  /**
+   * escapeStringForLIKE is only available on statements, and sometimes we want
+   *  to use it before we create our statement, so we create a statement just
+   *  for this reason.
+   */
+  get _escapeLikeStatement() {
+    let statement = this._createAsyncStatement("SELECT 0");
+    this.__defineGetter__("_escapeLikeStatement", function() statement);
+    return this._escapeLikeStatement;
+  },
+
+  _convertToDBValuesAndGroupByAttributeID:
+    function gloda_ds__convertToDBValuesAndGroupByAttributeID(aAttrDef,
+                                                              aValues) {
+    let objectNounDef = aAttrDef.objectNounDef;
+    if (!objectNounDef.usesParameter) {
+      let dbValues = [];
+      for (let iValue = 0; iValue < aValues.length; iValue++) {
+        let dbValue = objectNounDef.toParamAndValue(aValues[iValue])[1];
+        if (dbValue != null)
+          dbValues.push(dbValue);
+      }
+      yield [aAttrDef.special ? undefined : aAttrDef.id, dbValues];
+      return;
+    }
+    
+    let curParam, attrID, dbValues;
+    let attrDBDef = aAttrDef.dbDef;
+    for (let iValue = 0; iValue < aValues.length; iValue++) {
+      let [dbParam, dbValue] = objectNounDef.toParamAndValue(aValues[iValue]);
+      if (curParam === undefined) {
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        if (dbValue != null)
+          dbValues = [dbValue];
+        else
+          dbValues = [];
+      }
+      else if (curParam == dbParam) {
+        if (dbValue != null)
+          dbValues.push(dbValue);
+      }
+      else {
+        yield [attrID, dbValues];
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        if (dbValue != null)
+          dbValues = [dbValue];
+        else
+          dbValues = [];
+      }
+    }
+    if (dbValues !== undefined)
+      yield [attrID, dbValues];
+  },
+
+  _convertRangesToDBStringsAndGroupByAttributeID:
+    function gloda_ds__convertRangesToDBStringsAndGroupByAttributeID(aAttrDef,
+      aValues, aValueColumnName) {
+    let objectNounDef = aAttrDef.objectNounDef;
+    if (!objectNounDef.usesParameter) {
+      let dbStrings = [];
+      for (let iValue = 0; iValue < aValues.length; iValue++) {
+        let [lowerVal, upperVal] = aValues[iValue];
+        // they both can't be null.  that is the law.
+        if (lowerVal == null)
+          dbStrings.push(aValueColumnName + " <= " +
+                         objectNounDef.toParamAndValue(upperVal)[1]);
+        else if (upperVal == null)
+          dbStrings.push(aValueColumnName + " >= " +
+                         objectNounDef.toParamAndValue(lowerVal)[1]);
+        else // no one is null!
+          dbStrings.push(aValueColumnName + " BETWEEN " +
+                         objectNounDef.toParamAndValue(lowerVal)[1] + " AND " +
+                         objectNounDef.toParamAndValue(upperVal)[1]);
+      }
+      yield [aAttrDef.special ? undefined : aAttrDef.id, dbStrings];
+      return;
+    }
+    
+    let curParam, attrID, dbStrings;
+    let attrDBDef = aAttrDef.dbDef;
+    for (let iValue = 0; iValue < aValues.length; iValue++) {
+      let [lowerVal, upperVal] = aValues[iValue];
+
+      let dbString, dbParam, lowerDBVal, upperDBVal;
+      // they both can't be null.  that is the law.
+      if (lowerVal == null) {
+        [dbParam, upperDBVal] = objectNounDef.toParamAndValue(upperVal);
+        dbString = aValueColumnName + " <= " + upperDBVal;
+      }
+      else if (upperVal == null) {
+        [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+        dbString = aValueColumnName + " >= " + lowerDBVal; 
+      }
+      else { // no one is null!
+        [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+        dbString = aValueColumnName + " BETWEEN " + lowerDBVal + " AND " +
+                   objectNounDef.toParamAndValue(upperVal)[1];
+      }
+
+      if (curParam === undefined) {
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        dbStrings = [dbString];
+      }
+      else if (curParam === dbParam) {
+        dbStrings.push(dbString);
+      }
+      else {
+        yield [attrID, dbStrings];
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        dbStrings = [dbString];
+      }
+    }
+    if (dbStrings !== undefined)
+      yield [attrID, dbStrings];
+  },
+
+  /**
+   * Perform a database query given a GlodaQueryClass instance that specifies
+   *  a set of constraints relating to the noun type associated with the query.
+   *  A GlodaCollection is returned containing the results of the look-up.
+   *  By default the collection is "live", and will mutate (generating events to
+   *  its listener) as the state of the database changes.
+   * This functionality is made user/extension visible by the Query's
+   *  getCollection (asynchronous).
+   */
+  queryFromQuery: function gloda_ds_queryFromQuery(aQuery, aListener,
+      aListenerData, aExistingCollection, aMasterCollection) {
+    // when changing this method, be sure that GlodaQuery's testMatch function
+    //  likewise has its changes made.
+    let nounDef = aQuery._nounDef;
+
+    let whereClauses = [];
+    let unionQueries = [aQuery].concat(aQuery._unions);
+    let boundArgs = [];
+
+    for (let iUnion = 0; iUnion < unionQueries.length; iUnion++) {
+      let curQuery = unionQueries[iUnion];
+      let selects = [];
+      
+      let lastConstraintWasSpecial = false;
+      let curConstraintIsSpecial;
+
+      for (let iConstraint = 0; iConstraint < curQuery._constraints.length;
+           iConstraint++) {
+        let constraint = curQuery._constraints[iConstraint];
+        let [constraintType, attrDef] = constraint;
+        let constraintValues = constraint.slice(2);
+        
+        let idColumnName, tableColumnName;
+        if (constraintType == this.kConstraintIdIn) {
+          // we don't need any of the next cases' setup code, and we especially
+          //  would prefer that attrDef isn't accessed since it's null for us.
+        }
+        else if (attrDef.special) {
+          tableName = nounDef.tableName;
+          idColumnName = "id"; // canonical id for a table is "id".
+          valueColumnName = attrDef.specialColumnName;
+          curConstraintIsSpecial = true;
+        }
+        else {
+          tableName = nounDef.attrTableName;
+          idColumnName = nounDef.attrIDColumnName;
+          valueColumnName = "value";
+          curConstraintIsSpecial = false;
+        }
+        
+        let select = null, test = null, bindArgs = null;
+        if (constraintType === this.kConstraintIdIn) {
+          // this is somewhat of a trick.  this does mean that this can be the
+          //  only constraint.  Namely, our idiom is:
+          // SELECT * FROM blah WHERE id IN (a INTERSECT b INTERSECT c)
+          //  but if we only have 'a', then that becomes "...IN (a)", and if
+          //  'a' is not a select but a list of id's... tricky, no?  
+          select = constraintValues.join(",");
+        }
+        else if (constraintType === this.kConstraintIn) {
+          let clauses = [];
+          for each ([attrID, values] in
+              this._convertToDBValuesAndGroupByAttributeID(attrDef,
+                                                           constraintValues)) {
+            let clausePart;
+            if (attrID !== undefined)
+              clausePart = "(attributeID = " + attrID +
+                (values.length ? " AND " : "");
+            else
+              clausePart = "(";
+            if (values.length)
+              clausePart += valueColumnName + " IN (" + values.join(",") + "))";
+            else
+              clausePart += ")";
+            clauses.push(clausePart);
+          }
+          test = clauses.join(" OR ");
+        }
+        else if (constraintType === this.kConstraintRanges) {
+          let clauses = [];
+          for each ([attrID, dbStrings] in
+              this._convertRangesToDBStringsAndGroupByAttributeID(attrDef,
+                              constraintValues, valueColumnName)) {
+            if (attrID !== undefined)
+              clauses.push("(attributeID = " + attrID +
+                           " AND (" + dbStrings.join(" OR ") + "))");
+            else
+              clauses.push("(" + dbStrings.join(" OR ") + ")");
+          }
+          test = clauses.join(" OR ");
+        }
+        else if (constraintType === this.kConstraintEquals) {
+          let clauses = [];
+          for each ([attrID, values] in
+              this._convertToDBValuesAndGroupByAttributeID(attrDef,
+                                                           constraintValues)) {
+            if (attrID !== undefined)
+              clauses.push("(attributeID = " + attrID +
+                  " AND (" + [valueColumnName + " = ?" for each
+                  (value in values)].join(" OR ") + "))");
+            else
+              clauses.push("(" + [valueColumnName + " = ?" for each
+                  (value in values)].join(" OR ") + ")");
+            boundArgs.push.apply(boundArgs, values);
+          }
+          test = clauses.join(" OR ");
+        }
+        else if (constraintType === this.kConstraintStringLike) {
+          likePayload = '';
+          for each (let [iValuePart, valuePart] in Iterator(constraintValues)) {
+            if (typeof valuePart == "string")
+              likePayload += this._escapeLikeStatement.escapeStringForLIKE(
+                valuePart, "/");
+            else
+              likePayload += "%";
+          }
+          test = valueColumnName + " LIKE ? ESCAPE '/'";
+          boundArgs.push(likePayload);
+        }
+        else if (constraintType === this.kConstraintFulltext) {
+          let matchStr = constraintValues[0];
+          select = "SELECT docid FROM " + nounDef.tableName + "Text" +
+            " WHERE " + attrDef.specialColumnName + " MATCH ?";
+          boundArgs.push(matchStr);
+        }
+        
+        if (curConstraintIsSpecial && lastConstraintWasSpecial && test) {
+          selects[selects.length-1] += " AND " + test;
+        }
+        else if (select)
+          selects.push(select)
+        else if (test) {
+          select = "SELECT " + idColumnName + " FROM " + tableName + " WHERE " +
+              test;
+          selects.push(select);
+        }
+        else
+          this._log.warning("Unable to translate constraint of type " + 
+            constraintType + " on attribute bound as " + aAttrDef.boundName);
+
+        lastConstraintWasSpecial = curConstraintIsSpecial;
+      }
+
+      if (selects.length)
+        whereClauses.push("id IN (" + selects.join(" INTERSECT ") + ")");
+    }
+
+    let sqlString = "SELECT * FROM " + nounDef.tableName;
+    if (whereClauses.length)
+      sqlString += " WHERE " + whereClauses.join(" OR ");
+    
+    if (aQuery._order.length) {
+      let orderClauses = [];
+      for (let [, colName] in Iterator(aQuery._order)) {
+         if (colName[0] == "-")
+           orderClauses.push(colName.substring(1) + " DESC");
+         else
+           orderClauses.push(colName + " ASC");
+      }
+      sqlString += " ORDER BY " + orderClauses.join(", ");
+    }
+    
+    if (aQuery._limit) {
+      sqlString += " LIMIT ?";
+      boundArgs.push(aQuery._limit); 
+    }
+
+    this._log.debug("QUERY FROM QUERY: " + sqlString + " ARGS: " + boundArgs);
+    
+    return this._queryFromSQLString(sqlString, boundArgs, nounDef, aQuery,
+        aListener, aListenerData, aExistingCollection, aMasterCollection);
+  },
+  
+  _queryFromSQLString: function gloda_ds__queryFromSQLString(aSqlString,
+      aBoundArgs, aNounDef, aQuery, aListener, aListenerData,
+      aExistingCollection, aMasterCollection) {
+    let statement = this._createAsyncStatement(aSqlString, true);
+    for (let [iBinding, bindingValue] in Iterator(aBoundArgs)) {
+      this._bindVariant(statement, iBinding, bindingValue);
+    }
+
+    let collection;
+    if (aExistingCollection)
+      collection = aExistingCollection;
+    else {
+      collection = new GlodaCollection(aNounDef, [], aQuery, aListener,
+                                       aMasterCollection);
+      GlodaCollectionManager.registerCollection(collection);
+      // we don't want to overwrite the existing listener or its data, but this
+      //  does raise the question about what should happen if we get passed in
+      //  a different listener and/or data.
+      if (aListenerData !== undefined)
+        collection.data = aListenerData;
+    }
+    if (aListenerData) {
+      if (collection.dataStack)
+        collection.dataStack.push(aListenerData)
+      else
+        collection.dataStack = [aListenerData];
+    }
+
+    statement.executeAsync(new QueryFromQueryCallback(statement, aNounDef,
+      collection));
+    statement.finalize();
+    return collection;
+  },
+
+  /**
+   * 
+   * 
+   */
+  loadNounItem: function gloda_ds_loadNounItem(aItem, aReferencesByNounID,
+      aInverseReferencesByNounID) {
+    let attribIDToDBDefAndParam = this._attributeIDToDBDefAndParam;
+    
+    let hadDeps = aItem._deps != null;
+    let deps = aItem._deps || {};
+    let hasDeps = false;
+    
+    //this._log.debug("  hadDeps: " + hadDeps + " deps: " + 
+    //    Log4Moz.enumerateProperties(deps).join(","));
+    
+    for each (let [, attrib] in Iterator(aItem.NOUN_DEF.specialLoadAttribs)) {
+      let objectNounDef = attrib.objectNounDef;
+      
+      if (attrib.special === this.kSpecialColumnChildren) {
+        let invReferences = aInverseReferencesByNounID[objectNounDef.id];
+        if (invReferences === undefined)
+          invReferences = aInverseReferencesByNounID[objectNounDef.id] = {};
+        // only contribute if it's not already pending or there
+        if (!(attrib.id in deps) && aItem[attrib.storageAttributeName] == null){
+          //this._log.debug("   Adding inv ref for: " + aItem.id);
+          if (!(aItem.id in invReferences))
+            invReferences[aItem.id] = null;
+          deps[attrib.id] = null;
+          hasDeps = true;
+        }
+      }
+      else if (attrib.special === this.kSpecialColumnParent) {
+        let references = aReferencesByNounID[objectNounDef.id];
+        if (references === undefined)
+          references = aReferencesByNounID[objectNounDef.id] = {};
+        // nothing to contribute if it's already there
+        if (!(attrib.id in deps) && 
+            aItem[attrib.valueStorageAttributeName] == null) {
+          let parentID = aItem[attrib.idStorageAttributeName];
+          if (!(parentID in references))
+            references[parentID] = null;
+          //this._log.debug("   Adding parent ref for: " +
+          //  aItem[attrib.idStorageAttributeName]);
+          deps[attrib.id] = null;
+          hasDeps = true;
+        }
+        else {
+          this._log.debug("  paranoia value storage: " + aItem[attrib.valueStorageAttributeName]);
+        }
+      }
+    }
+    
+    // bail here if arbitrary values are not allowed, there just is no
+    //  encoded json, or we already had dependencies for this guy, implying
+    //  the json pass has already been performed
+    if (!aItem.NOUN_DEF.allowsArbitraryAttrs || !aItem._jsonText || hadDeps) {
+      if (hasDeps)
+        aItem._deps = deps;
+      return hasDeps;
+    }
+
+    //this._log.debug(" load json: " + aItem._jsonText);
+    let jsonDict = this._json.decode(aItem._jsonText);
+    delete aItem._jsonText;
+    
+    // Iterate over the attributes on the item
+    for each (let [attribId, jsonValue] in Iterator(jsonDict)) {
+      // find the attribute definition that corresponds to this key
+      let dbAttrib = attribIDToDBDefAndParam[attribId][0];
+      // the attribute should only fail to exist if an extension was removed
+      if (dbAttrib === undefined)
+        continue;
+      
+      let attrib = dbAttrib.attrDef;
+      let objectNounDef = attrib.objectNounDef;
+      
+      // if it has a tableName member, then it's a persistent object that needs
+      //  to be loaded, which also means we need to hold it in a collection
+      //  owned by our collection.
+      if (objectNounDef.tableName) {
+        let references = aReferencesByNounID[objectNounDef.id];
+        if (references === undefined)
+          references = aReferencesByNounID[objectNounDef.id] = {};
+          
+        if (attrib.singular) {
+          if (!(jsonValue in references))
+            references[jsonValue] = null;
+        }
+        else {
+          for each (let [, anID] in Iterator(jsonValue)) {
+            if (!(anID in references))
+            references[anID] = null;
+          }
+        }
+        
+        deps[attribId] = jsonValue;
+        hasDeps = true;
+      }
+      /* if it has custom contribution logic, use it */
+      else if (objectNounDef.contributeObjDependencies) {
+        if (objectNounDef.contributeObjDependencies(jsonValue,
+                             aReferencesByNounID, aInverseReferencesByNounID)) {
+          deps[attribId] = jsonValue;
+          hasDeps = true;
+        }
+        else // just propagate the value, it's some form of simple sentinel
+          aItem[attrib.boundName] = jsonValue;
+      }
+      // otherwise, the value just needs to be de-persisted, or not
+      else if (objectNounDef.fromJSON) {
+        if (attrib.singular)
+          aItem[attrib.boundName] = objectNounDef.fromJSON(jsonValue);
+        else
+          aItem[attrib.boundName] = [objectNounDef.fromJSON(val) for each
+            ([, val] in Iterator(jsonValue))];
+      }
+      // it's fine as is
+      else
+        aItem[attrib.boundName] = jsonValue;
+    }
+    
+    if (hasDeps)
+      aItem._deps = deps;
+    return hasDeps;
+  },
+  
+  loadNounDeferredDeps: function gloda_ds_loadNounDeferredDeps(aItem,
+      aReferencesByNounID, aInverseReferencesByNounID) {
+    if (aItem._deps === undefined)
+      return;
+
+    //this._log.debug("  loading deferred, deps: " + 
+    //    Log4Moz.enumerateProperties(aItem._deps).join(","));
+
+    
+    let attribIDToDBDefAndParam = this._attributeIDToDBDefAndParam;
+
+    for (let [attribId, jsonValue] in Iterator(aItem._deps)) {
+      let dbAttrib = attribIDToDBDefAndParam[attribId][0];
+      let attrib = dbAttrib.attrDef;
+      
+      let objectNounDef = attrib.objectNounDef;
+      let references = aReferencesByNounID[objectNounDef.id];
+      if (attrib.special) {
+        if (attrib.special === this.kSpecialColumnChildren) {
+          let inverseReferences = aInverseReferencesByNounID[objectNounDef.id];
+          //this._log.info("inverse assignment: " + objectNounDef.id +
+          //    " of " + aItem.id)
+          aItem[attrib.storageAttributeName] = inverseReferences[aItem.id];
+        }
+        else if (attrib.special === this.kSpecialColumnParent) {
+          //this._log.info("parent column load: " + objectNounDef.id +
+          //    " storage value: " + aItem[attrib.idStorageAttributeName]);
+          aItem[attrib.valueStorageAttributeName] =
+            references[aItem[attrib.idStorageAttributeName]];
+        }
+      }
+      else if (objectNounDef.tableName) {
+        //this._log.info("trying to load: " + objectNounDef.id + " refs: " +
+        //    jsonValue + ": " + Log4Moz.enumerateProperties(jsonValue).join(","));
+        if (attrib.singular)
+          aItem[attrib.boundName] = references[jsonValue];
+        else
+          aItem[attrib.boundName] = [references[val] for each
+                                     ([, val] in Iterator(jsonValue))];
+      }
+      else if (objectNounDef.contributeObjDependencies) {
+        aItem[attrib.boundName] =
+          objectNounDef.resolveObjDependencies(jsonValue, aReferencesByNounID,
+            aInverseReferencesByNounID);
+      }
+      // there is no other case
+    }
+    
+    delete aItem._deps;
+  },
+
+  /* ********** Contact ********** */
+  _nextContactId: 1,
+
+  _populateContactManagedId: function () {
+    let stmt = this._createSyncStatement("SELECT MAX(id) FROM contacts", true);
+    if (stmt.executeStep()) {  // no chance of this SQLITE_BUSY on this call
+      this._nextContactId = stmt.getInt64(0) + 1;
+    }
+    stmt.finalize();
+  },
+
+  get _insertContactStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO contacts (id, directoryUUID, contactUUID, name, popularity,\
+                             frecency, jsonAttributes) \
+              VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)");
+    this.__defineGetter__("_insertContactStatement", function() statement);
+    return this._insertContactStatement;
+  },
+
+  createContact: function gloda_ds_createContact(aDirectoryUUID, aContactUUID,
+      aName, aPopularity, aFrecency) {
+    let contactID = this._nextContactId++;
+    this._log.debug("createContact: " + contactID + ": " + aName);
+    let contact = new GlodaContact(this, contactID,
+                                   aDirectoryUUID, aContactUUID, aName,
+                                   aPopularity, aFrecency);
+    return contact;
+  },
+  
+  insertContact: function gloda_ds_insertContact(aContact) {
+    let ics = this._insertContactStatement;
+    ics.bindInt64Parameter(0, aContact.id);
+    if (aContact.directoryUUID == null)
+      ics.bindNullParameter(1);
+    else
+      ics.bindStringParameter(1, aContact.directoryUUID);
+    if (aContact.contactUUID == null)
+      ics.bindNullParameter(2);
+    else
+      ics.bindStringParameter(2, aContact.contactUUID);
+    ics.bindStringParameter(3, aContact.name);
+    ics.bindInt64Parameter(4, aContact.popularity);
+    ics.bindInt64Parameter(5, aContact.frecency);
+    if (aContact._jsonText)
+      ics.bindStringParameter(6, aContact._jsonText);
+    else
+      ics.bindNullParameter(6);
+
+    ics.executeAsync(this.trackAsync());
+    this._log.debug("insertContact: " + aContact.id + ":" + aContact.name);
+
+    return aContact;
+  },
+
+  get _updateContactStatement() {
+    let statement = this._createAsyncStatement(
+      "UPDATE contacts SET directoryUUID = ?1, \
+                           contactUUID = ?2, \
+                           name = ?3, \
+                           popularity = ?4, \
+                           frecency = ?5, \
+                           jsonAttributes = ?6 \
+                       WHERE id = ?7");
+    this.__defineGetter__("_updateContactStatement", function() statement);
+    return this._updateContactStatement;
+  },
+
+  updateContact: function gloda_ds_updateContact(aContact) {
+    let ucs = this._updateContactStatement;
+    ucs.bindInt64Parameter(6, aContact.id);
+    ucs.bindStringParameter(0, aContact.directoryUUID);
+    ucs.bindStringParameter(1, aContact.contactUUID);
+    ucs.bindStringParameter(2, aContact.name);
+    ucs.bindInt64Parameter(3, aContact.popularity);
+    ucs.bindInt64Parameter(4, aContact.frecency);
+    if (aContact._jsonText)
+      ucs.bindStringParameter(5, aContact._jsonText);
+    else
+      ucs.bindNullParameter(5);
+
+    ucs.executeAsync(this.trackAsync());
+  },
+
+  _contactFromRow: function gloda_ds_contactFromRow(aRow) {
+    let directoryUUID, contactUUID, jsonText;
+    if (aRow.getTypeOfIndex(1) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      directoryUUID = null;
+    else
+      directoryUUID = aRow.getString(1);
+    if (aRow.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      contactUUID = null;
+    else
+      contactUUID = aRow.getString(2);
+    if (aRow.getTypeOfIndex(6) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+      jsonText = undefined;
+    else
+      jsonText = aRow.getString(6);
+
+    return new GlodaContact(this, aRow.getInt64(0), directoryUUID,
+                            contactUUID, aRow.getString(5),
+                            aRow.getInt64(3), aRow.getInt64(4), jsonText);
+  },
+
+  get _selectContactByIDStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT * FROM contacts WHERE id = ?1");
+    this.__defineGetter__("_selectContactByIDStatement",
+      function() statement);
+    return this._selectContactByIDStatement;
+  },
+
+  getContactByID: function gloda_ds_getContactByID(aContactID) {
+    let contact = GlodaCollectionManager.cacheLookupOne(
+      GlodaContact.prototype.NOUN_ID, aContactID);
+
+    if (contact === null) {
+      let scbi = this._selectContactByIDStatement;
+      scbi.bindInt64Parameter(0, aContactID);
+      if (this._syncStep(scbi)) {
+        contact = this._contactFromRow(scbi);
+        GlodaCollectionManager.itemLoaded(contact);
+      }
+      scbi.reset();
+    }
+
+    return contact;
+  },
+
+  /* ********** Identity ********** */
+  /** next identity id, managed for async use reasons. */
+  _nextIdentityId: 1,
+  _populateIdentityManagedId: function () {
+    let stmt = this._createSyncStatement(
+      "SELECT MAX(id) FROM identities", true);
+    if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+      this._nextIdentityId = stmt.getInt64(0) + 1;
+    }
+    stmt.finalize();
+  },
+
+  get _insertIdentityStatement() {
+    let statement = this._createAsyncStatement(
+      "INSERT INTO identities (id, contactID, kind, value, description, relay) \
+              VALUES (?1, ?2, ?3, ?4, ?5, ?6)");
+    this.__defineGetter__("_insertIdentityStatement", function() statement);
+    return this._insertIdentityStatement;
+  },
+
+  createIdentity: function gloda_ds_createIdentity(aContactID, aContact, aKind,
+                                                   aValue, aDescription,
+                                                   aIsRelay) {
+    let identityID = this._nextIdentityId++;
+    let iis = this._insertIdentityStatement;
+    iis.bindInt64Parameter(0, identityID);
+    iis.bindInt64Parameter(1, aContactID);
+    iis.bindStringParameter(2, aKind);
+    iis.bindStringParameter(3, aValue);
+    iis.bindStringParameter(4, aDescription);
+    iis.bindInt64Parameter(5, aIsRelay ? 1 : 0);
+    iis.executeAsync(this.trackAsync());
+
+    let identity = new GlodaIdentity(this, identityID,
+                                     aContactID, aContact, aKind, aValue,
+                                     aDescription, aIsRelay);
+    GlodaCollectionManager.itemsAdded(identity.NOUN_ID, [identity]);
+    return identity;
+  },
+
+  _identityFromRow: function gloda_ds_identityFromRow(aRow) {
+    return new GlodaIdentity(this, aRow.getInt64(0), aRow.getInt64(1), null,
+                             aRow.getString(2), aRow.getString(3),
+                             aRow.getString(4),
+                             aRow.getInt32(5) ? true : false);
+  },
+
+  get _selectIdentityByKindValueStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT * FROM identities WHERE kind = ?1 AND value = ?2");
+    this.__defineGetter__("_selectIdentityByKindValueStatement",
+      function() statement);
+    return this._selectIdentityByKindValueStatement;
+  },
+
+  /** Lookup an identity by kind and value.  Ex: (email, foo@bar.com) */
+  getIdentity: function gloda_ds_getIdentity(aKind, aValue) {
+    let identity = GlodaCollectionManager.cacheLookupOneByUniqueValue(
+      GlodaIdentity.prototype.NOUN_ID, aKind + "@" + aValue);
+
+    let ibkv = this._selectIdentityByKindValueStatement;
+    ibkv.bindStringParameter(0, aKind);
+    ibkv.bindStringParameter(1, aValue);
+    if (this._syncStep(ibkv)) {
+      identity = this._identityFromRow(ibkv);
+      GlodaCollectionManager.itemLoaded(identity);
+    }
+    ibkv.reset();
+
+    return identity;
+  },
+
+  get _selectIdentityByIDStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT * FROM identities WHERE id = ?1");
+    this.__defineGetter__("_selectIdentityByIDStatement",
+      function() statement);
+    return this._selectIdentityByIDStatement;
+  },
+
+  getIdentityByID: function gloda_ds_getIdentityByID(aID) {
+    let identity = GlodaCollectionManager.cacheLookupOne(
+      GlodaIdentity.prototype.NOUN_ID, aID);
+
+    if (identity === null) {
+      let sibis = this._selectIdentityByIDStatement;
+      sibis.bindInt64Parameter(0, aID);
+      if (this._syncStep(sibis)) {
+        identity = this._identityFromRow(sibis);
+        GlodaCollectionManager.itemLoaded(identity);
+      }
+      sibis.reset();
+    }
+
+    return identity;
+  },
+
+  get _selectIdentityByContactIDStatement() {
+    let statement = this._createSyncStatement(
+      "SELECT * FROM identities WHERE contactID = ?1");
+    this.__defineGetter__("_selectIdentityByContactIDStatement",
+      function() statement);
+    return this._selectIdentityByContactIDStatement;
+  },
+
+  getIdentitiesByContactID: function gloda_ds_getIdentitiesByContactID(
+      aContactID) {
+    let sibcs = this._selectIdentityByContactIDStatement;
+
+    sibcs.bindInt64Parameter(0, aContactID);
+
+    let identities = [];
+    while (this._syncStep(sibcs)) {
+      identities.push(this._identityFromRow(sibcs));
+    }
+    sibcs.reset();
+
+    if (identities.length)
+      GlodaCollectionManager.cacheLoadUnify(GlodaIdentity.prototype.NOUN_ID,
+                                            identities);
+    return identities;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/everybody.js
@@ -0,0 +1,90 @@
+/* ***** 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 EXPORTED_SYMBOLS = [];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+const LOG = Log4Moz.Service.getLogger("gloda.everybody");
+
+var importNS = {};
+var strtab = null;
+
+function loadModule(aModuleURI, aNSContrib) {
+  if (strtab === null) {
+    let bundleService = Cc["@mozilla.org/intl/stringbundle;1"].
+                        getService(Ci.nsIStringBundleService);
+    strtab = bundleService.createBundle("chrome://gloda/locale/gloda.properties");
+    LOG.debug("string bundle: " + strtab);
+  }
+
+  try {
+    LOG.info("... loading " + aModuleURI);
+    Cu.import(aModuleURI, importNS);
+  }
+  catch (ex) {
+    LOG.error("!!! error loading " + aModuleURI);
+    LOG.error("(" + ex.fileName + ":" + ex.lineNumber + ") " + ex);
+    return false;
+  }
+  LOG.info("+++ loaded " + aModuleURI);
+
+  if (aNSContrib) {
+    try {  
+      importNS[aNSContrib].init(strtab);
+    }
+    catch (ex) {
+      LOG.error("!!! error initializing " + aModuleURI);
+      LOG.error("(" + ex.fileName + ":" + ex.lineNumber + ") " + ex);
+      return false;
+    }
+    LOG.info("+++ inited " + aModuleURI);
+  }
+  return true;
+}
+
+loadModule("resource://gloda/modules/fundattr.js", "GlodaFundAttr");
+loadModule("resource://gloda/modules/explattr.js", "GlodaExplicitAttr");
+
+loadModule("resource://gloda/modules/noun_tag.js");
+loadModule("resource://gloda/modules/noun_freetag.js");
+loadModule("resource://gloda/modules/noun_mimetype.js");
+loadModule("resource://gloda/modules/index_ab.js", "GlodaABAttrs");
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/explattr.js
@@ -0,0 +1,165 @@
+/* ***** 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 ***** */
+
+/*
+ * This file provides the "explicit attribute" provider for messages.  It is
+ *  concerned with attributes that are the result of user actions.  For example,
+ *  whether a message is starred (flagged), message tags, whether it is
+ *  read/unread, etc.
+ */
+
+EXPORTED_SYMBOLS = ['GlodaExplicitAttr'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+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/gloda.js");
+Cu.import("resource://gloda/modules/noun_tag.js");
+
+
+const EXT_BUILTIN = "built-in";
+const FA_TAG = "TAG";
+const FA_STAR = "STAR";
+const FA_READ = "READ";
+
+/**
+ * @namespace Explicit attribute provider.  Indexes/defines attributes that are
+ *  explicitly a result of user action.  This dubiously includes marking a
+ *  message as read. 
+ */
+var GlodaExplicitAttr = {
+  providerName: "gloda.explattr",
+  _log: null,
+  _strBundle: null,
+  _msgTagService: null,
+
+  init: function gloda_explattr_init(aStrBundle) {
+    this._log =  Log4Moz.Service.getLogger("gloda.explattr");
+    this._strBundle = aStrBundle;
+
+    this._msgTagService = Cc["@mozilla.org/messenger/tagservice;1"].
+                          getService(Ci.nsIMsgTagService);
+  
+    try {
+      this.defineAttributes();
+    }
+    catch (ex) {
+      this._log.error("Error in init: " + ex);
+      throw ex;
+    }
+  },
+
+  _attrTag: null,
+  _attrStar: null,
+  _attrRead: null,
+  
+  defineAttributes: function() {
+    // Tag
+    this._attrTag = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrExplicit,
+                        attributeName: "tag",
+                        bindName: "tags",
+                        singular: false,
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_TAG,
+                        parameterNoun: null,
+                        // Property change notifications that we care about:
+                        propertyChanges: ["keywords"],
+                        }); // not-tested
+
+    // Star
+    this._attrStar = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrExplicit,
+                        attributeName: "star",
+                        bindName: "starred",
+                        singular: true,
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_BOOLEAN,
+                        parameterNoun: null,
+                        }); // tested-by: test_attributes_explicit
+    // Read/Unread
+    this._attrRead = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrExplicit,
+                        attributeName: "read",
+                        singular: true,
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_BOOLEAN,
+                        parameterNoun: null,
+                        }); // tested-by: test_attributes_explicit
+    
+  },
+  
+  process: function Gloda_explattr_process(aGlodaMessage, aRawReps, aIsNew,
+                                           aCallbackHandle) {
+    let aMsgHdr = aRawReps.header;
+    
+    aGlodaMessage.starred = aMsgHdr.isFlagged;
+    aGlodaMessage.read = aMsgHdr.isRead;
+    
+    let tags = aGlodaMessage.tags = [];
+    
+    // -- Tag
+    // build a map of the keywords
+    let keywords = aMsgHdr.getStringProperty("keywords");
+    let keywordList = keywords.split(' ');
+    let keywordMap = {};
+    for (let iKeyword = 0; iKeyword < keywordList.length; iKeyword++) {
+      let keyword = keywordList[iKeyword];
+      keywordMap[keyword] = true;
+    }
+
+    let tagArray = this._msgTagService.getAllTags({});
+    for (let iTag = 0; iTag < tagArray.length; iTag++) {
+      let tag = tagArray[iTag];
+      if (tag.key in keywordMap)
+        tags.push(tag);
+    }
+    
+    yield Gloda.kWorkDone;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/fundattr.js
@@ -0,0 +1,544 @@
+/* ***** 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 = ['GlodaFundAttr'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+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/gloda.js");
+Cu.import("resource://gloda/modules/datastore.js");
+
+Cu.import("resource://gloda/modules/noun_mimetype.js");
+
+
+/**
+ * @namespace The Gloda Fundamental Attribute provider is a special attribute
+ *  provider; it provides attributes that the rest of the providers should be
+ *  able to assume exist.  Also, it may end up accessing things at a lower level
+ *  than most extension providers should do.  In summary, don't mimic this code
+ *  unless you won't complain when your code breaks.
+ */
+var GlodaFundAttr = {
+  providerName: "gloda.fundattr",
+  _log: null,
+  _strBundle: null,
+
+  init: function gloda_explattr_init(aStrBundle) {
+    this._log =  Log4Moz.Service.getLogger("gloda.fundattr");
+    this._strBundle = aStrBundle;
+  
+    try {
+      this.defineAttributes();
+    }
+    catch (ex) {
+      this._log.error("Error in init: " + ex);
+      throw ex;
+    }
+  },
+
+  POPULARITY_FROM_ME_TO: 10,
+  POPULARITY_FROM_ME_CC: 4,
+  POPULARITY_TO_ME: 5,
+  POPULARITY_CC_ME: 1,
+
+  _attrConvSubject: null,
+  _attrFolder: null,
+  _attrBody: null,
+  _attrFrom: null,
+  _attrFromMe: null,
+  _attrTo: null,
+  _attrToMe: null,
+  _attrCc: null,
+  _attrCcMe: null,
+  _attrDate: null,
+  
+  defineAttributes: function() {
+    /* ***** Conversations ***** */
+    // conversation: subjectMatches
+    this._attrConvSubject = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrDerived,
+      attributeName: "subjectMatches",
+      singular: true,
+      special: Gloda.kSpecialFulltext,
+      specialColumnName: "subject",
+      subjectNouns: [Gloda.NOUN_CONVERSATION],
+      objectNoun: Gloda.NOUN_FULLTEXT,
+      });
+  
+    /* ***** Messages ***** */
+    // folder
+    this._attrFolder = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrFundamental,
+      attributeName: "folder",
+      singular: true,
+      special: Gloda.kSpecialColumn,
+      specialColumnName: "folderID",
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_FOLDER,
+      }); // tested-by: test_attributes_fundamental
+    this._attrFolder = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrFundamental,
+      attributeName: "messageKey",
+      singular: true,
+      special: Gloda.kSpecialColumn,
+      specialColumnName: "messageKey",
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_NUMBER,
+      }); // tested-by: test_attributes_fundamental
+    
+    // bodyMatches. super-synthetic full-text matching...
+    this._attrBody = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrDerived,
+      attributeName: "bodyMatches",
+      singular: true,
+      special: Gloda.kSpecialFulltext,
+      specialColumnName: "body",
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_FULLTEXT,
+      }); // not-tested
+    
+    // conversation
+    this._attrConversation = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrFundamental,
+      attributeName: "conversation",
+      singular: true,
+      special: Gloda.kSpecialColumnParent,
+      specialColumnName: "conversationID",
+      idStorageAttributeName: "_conversationID",
+      valueStorageAttributeName: "_conversation", 
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_CONVERSATION,
+      });
+  
+    // --- Fundamental
+    // From
+    this._attrFrom = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrFundamental,
+                        attributeName: "from",
+                        singular: true,
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_IDENTITY,
+                        }); // tested-by: test_attributes_fundamental
+    // To
+    this._attrTo = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrFundamental,
+                        attributeName: "to",
+                        singular: false,
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_IDENTITY,
+                        }); // tested-by: test_attributes_fundamental
+    // Cc
+    this._attrCc = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrFundamental,
+                        attributeName: "cc",
+                        singular: false,
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_IDENTITY,
+                        }); // not-tested
+
+    // Date.  now lives on the row.
+    this._attrDate = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrFundamental,
+                        attributeName: "date",
+                        singular: true,
+                        special: Gloda.kSpecialColumn,
+                        specialColumnName: "date",
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_DATE,
+                        }); // tested-by: test_attributes_fundamental
+    
+    // Attachment MIME Types
+    this._attrAttachmentTypes = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrFundamental,
+      attributeName: "attachmentTypes",
+      singular: false,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_MIME_TYPE,
+      });
+
+    // --- Optimization
+    // Involves.  Means any of from/to/cc.  The queries get ugly enough without
+    //   this that it seems to justify the cost, especially given the frequent
+    //   use case.  (In fact, post-filtering for the specific from/to/cc is
+    //   probably justifiable rather than losing this attribute...)
+    this._attrInvolves = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrOptimization,
+      attributeName: "involves",
+      singular: false,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_IDENTITY,
+      }); // not-tested
+
+    // From Me To
+    this._attrFromMeTo = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrOptimization,
+      attributeName: "fromMeTo",
+      singular: false,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+      }); // not-tested
+    // From Me Cc
+    this._attrFromMeCc = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrOptimization,
+      attributeName: "fromMeCc",
+      singular: false,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+      }); // not-tested
+    // To Me
+    this._attrToMe = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrFundamental,
+      attributeName: "toMe",
+      singular: false,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+      }); // not-tested
+    // Cc Me
+    this._attrCcMe = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrFundamental,
+      attributeName: "ccMe",
+      singular: false,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+      }); // not-tested
+
+
+    // -- Mailing List
+    // Non-singular, but a hard call.  Namely, it is obvious that a message can
+    //  be addressed to multiple mailing lists.  However, I don't see how you
+    //  could receive a message with more than one set of List-* headers,
+    //  since each list-serve would each send you a copy.  Based on our current
+    //  decision to treat each physical message as separate, it almost seems
+    //  right to limit the list attribute to the copy that originated at the
+    //  list.  That may sound entirely wrong, but keep in mind that until we
+    //  have seen a message from the list with the List headers, we can't
+    //  definitely know it's a mailing list (although heuristics could take us
+    //  pretty far).  As such, the quasi-singular thing is appealing.
+    // Of course, the reality is that we really want to know if a message was
+    //  sent to multiple mailing lists and be able to query on that.
+    //  Additionally, our implicit-to logic needs to work on messages that
+    //  weren't relayed by the list-serve, especially messages sent to the list
+    //  by the user.
+    this._attrList = Gloda.defineAttribute({
+                        provider: this,
+                        extensionName: Gloda.BUILT_IN,
+                        attributeType: Gloda.kAttrFundamental,
+                        attributeName: "mailing-list",
+                        bindName: "mailingLists",
+                        singular: false,
+                        subjectNouns: [Gloda.NOUN_MESSAGE],
+                        objectNoun: Gloda.NOUN_IDENTITY,
+                        }); // not-tested, not-implemented
+  },
+  
+  /**
+   *
+   * 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, aRawReps,
+                                           aIsNew, aCallbackHandle) {
+    let aMsgHdr = aRawReps.header;
+    let aMimeMsg = aRawReps.mime;
+    
+    // -- 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;
+    /*
+    try {
+      author = aMsgHdr.getStringProperty("replyTo");
+    }
+    catch (ex) {
+    }
+    */
+    if (author == null || author == "")
+      author = aMsgHdr.author;
+    
+    let [authorIdentities, toIdentities, ccIdentities] =
+      yield aCallbackHandle.pushAndGo(
+        Gloda.getOrCreateMailIdentities(aCallbackHandle,
+                                        author, aMsgHdr.recipients,
+                                        aMsgHdr.ccList));
+
+    if (authorIdentities.length == 0) {
+      this._log.error("Message with subject '" + aMsgHdr.mime2DecodedSubject +
+                      "' somehow lacks a valid author.  Bailing.");
+      return; // being a generator, this generates an exception; we like.
+    }
+    let authorIdentity = authorIdentities[0];
+    aGlodaMessage.from = authorIdentity;
+
+    // -- To, Cc
+    aGlodaMessage.to = toIdentities;
+    aGlodaMessage.cc = ccIdentities;
+    
+    // -- Attachments
+    let attachmentTypes = [];
+    for each (let attachment in aMimeMsg.allAttachments) {
+      if (attachment.isRealAttachment) {
+        attachmentTypes.push(MimeTypeNoun.getMimeType(attachment.contentType));
+      }
+    }
+    if (attachmentTypes.length) {
+      aGlodaMessage.attachmentTypes = attachmentTypes;
+    }
+    
+    // TODO: deal with mailing lists, including implicit-to.  this will require
+    //  convincing the indexer to pass us in the previous message if it is
+    //  available.  (which we'll simply pass to everyone... it can help body
+    //  logic for quoting purposes, etc. too.)
+    
+    yield Gloda.kWorkDone;
+  },
+  
+  optimize: function gloda_fundattr_process(aGlodaMessage, aRawReps,
+      aIsNew, aCallbackHandle) {
+
+    let involvesIdentities = {};
+    let involves = aGlodaMessage.involves || [];
+    
+    // me specialization optimizations
+    let toMe = aGlodaMessage.toMe || [];
+    let fromMeTo = aGlodaMessage.fromMeTo || [];
+    let ccMe = aGlodaMessage.ccMe || [];
+    let fromMeCc = aGlodaMessage.fromMeCc || [];
+
+    let myIdentities = Gloda.myIdentities; // needless optimization?
+    let authorIdentity = aGlodaMessage.from;
+    let isFromMe = authorIdentity.id in myIdentities;
+
+    involves.push(authorIdentity);
+    involvesIdentities[authorIdentity.id] = true;
+    
+    for each (let [,toIdentity] in Iterator(aGlodaMessage.to)) {
+      if (!(toIdentity.id in involvesIdentities)) {
+        involves.push(toIdentity);
+        involvesIdentities[toIdentity.id] = true;
+      }
+      
+      // optimization attribute to-me ('I' am the parameter)
+      if (toIdentity.id in myIdentities) {
+        toMe.push([toIdentity, authorIdentity]);
+        if (aIsNew)
+          authorIdentity.contact.popularity += this.POPULARITY_TO_ME;
+      }
+      // optimization attribute from-me-to ('I' am the parameter)
+      if (isFromMe) {
+        fromMeTo.push([authorIdentity, toIdentity]);
+        // also, popularity
+        if (aIsNew)
+          toIdentity.contact.popularity += this.POPULARITY_FROM_ME_TO;
+      }
+    }
+    for each (let [,ccIdentity] in Iterator(aGlodaMessage.cc)) {
+      if (!(ccIdentity.id in involvesIdentities)) {
+        involves.push(ccIdentity);
+        involvesIdentities[ccIdentity.id] = true;
+      }
+      // optimization attribute cc-me ('I' am the parameter)
+      if (ccIdentity.id in myIdentities) {
+        ccMe.push([ccIdentity, authorIdentity]);
+        if (aIsNew)
+          authorIdentity.contact.popularity += this.POPULARITY_CC_ME;
+      }
+      // optimization attribute from-me-to ('I' am the parameter)
+      if (isFromMe) {
+        fromMeCc.push([authorIdentity, ccIdentity]);
+        // also, popularity
+        if (aIsNew)
+          ccIdentity.contact.popularity += this.POPULARITY_FROM_ME_CC;
+      }
+    }
+    
+    aGlodaMessage.involves = involves;
+    if (toMe.length)
+      aGlodaMessage.toMe = toMe;
+    if (fromMeTo.length)
+      aGlodaMessage.fromMeTo = fromMeTo;
+    if (ccMe.length)
+      aGlodaMessage.ccMe = ccMe;
+    if (fromMeCc.length)
+      aGlodaMessage.fromMeCc = fromMeCc;
+    
+    if (aRawReps.bodyLines &&
+        this.contentWhittle(aGlodaMessage, {}, aRawReps.bodyLines,
+                            aRawReps.content)) {
+      // we were going to do something here?
+    }
+
+    yield Gloda.kWorkDone;
+  },
+  
+  _countQuoteDepthAndNormalize:
+    function gloda_fundattr__countQuoteDepthAndNormalize(aLine) {
+    let count = 0;
+    let lastStartOffset = 0;
+    
+    for (let i = 0; i < aLine.length; i++) {
+      let c = aLine[i];
+      if (c == ">") {
+        count++;
+        lastStartOffset = i+1;
+      }
+      else if (c == " ") {
+      }
+      else {
+        return [count,
+                lastStartOffset ? aLine.substring(lastStartOffset) : aLine];
+      }
+    }
+    
+    return [count, lastStartOffset ? aLine.substring(lastStartOffset) : aLine];
+  },
+  
+  /**
+   * Attempt to understand simple quoting constructs that use ">" with
+   * obvious phrases to enter the quoting block.  No support for other types
+   * of quoting at this time.  Also no support for piercing the wrapper of
+   * forwarded messages to actually be the content of the forwarded message.
+   */
+  contentWhittle: function gloda_fundattr_contentWhittle(aGlodaMessage,
+      aMeta, aBodyLines, aContent) {
+    if (!aContent.volunteerContent(aContent.kPriorityBase))
+      return false;
+    
+    // duplicate the list; we mutate somewhat...
+    let bodyLines = aBodyLines.concat();
+    
+    let rangeStart = 0, lastNonBlankLine = null;
+    let inQuoteDepth = 0;
+    for each (let [iLine, line] in Iterator(bodyLines)) {
+      if (!line)
+        continue;
+      
+      if (line[0] == ">") {
+        if (!inQuoteDepth) {
+          let rangeEnd = iLine - 1;
+          let quoteRangeStart = iLine;
+          // see if the last non-blank-line was a lead-in...
+          if (lastNonBlankLine != null) {
+            if (aBodyLines[lastNonBlankLine].indexOf("wrote") >= 0) {
+              quoteRangeStart = lastNonBlankLine;
+              rangeEnd = lastNonBlankLine - 1;
+            }
+          }
+          if (rangeEnd >= rangeStart)
+            aContent.content(aBodyLines.slice(rangeStart, rangeEnd+1));
+          
+          [inQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
+          bodyLines[iLine] = line;
+          rangeStart = quoteRangeStart;
+        }
+        else {
+          let curQuoteDepth;
+          [curQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
+          bodyLines[iLine] = line;
+          
+          if (curQuoteDepth != inQuoteDepth) {
+            // we could do some "wrote" compensation here, but it's not really
+            //  as important.  let's wait for a more clever algorithm.
+            aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
+            inQuoteDepth = curQuoteDepth;
+            rangeStart = iLine;
+          }
+        }
+      }
+      else {
+        if (inQuoteDepth) {
+          aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
+          inQuoteDepth = 0;
+          rangeStart = iLine;
+        }
+      }
+      
+      lastNonBlankLine = iLine;
+    }
+    
+    if (inQuoteDepth) {
+      aContent.quoted(aBodyLines.slice(rangeStart), inQuoteDepth);
+    }
+    else {
+      aContent.content(aBodyLines.slice(rangeStart));
+    }
+    
+    return true;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/gloda.js
@@ -0,0 +1,1643 @@
+/* ***** 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 = ['Gloda'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+Cu.import("resource://gloda/modules/datastore.js");
+Cu.import("resource://gloda/modules/datamodel.js");
+Cu.import("resource://gloda/modules/collection.js");
+Cu.import("resource://gloda/modules/connotent.js");
+Cu.import("resource://gloda/modules/query.js");
+Cu.import("resource://gloda/modules/utils.js");
+
+/**
+ * Provides the user-visible (and extension visible) global database
+ *  functionality.  There is currently a dependency/ordering
+ *  problem in that the concept of 'gloda' also includes some logic that is
+ *  contributed by built-in extensions, if you will.  Those built-in extensions
+ *  (fundattr.js, explattr.js) also import this file.  To avoid a circular
+ *  dependency, those built-in extensions are loaded by everybody.js.  The
+ *  simplest/best solution is probably to move everybody.js to be gloda.js and
+ *  have it re-export only 'Gloda'.  gloda.js (this file) can then move to be
+ *  gloda_int.js (or whatever our eventual naming scheme is), which built-in
+ *  extensions can explicitly rely upon.
+ *
+ * === Concepts
+ *
+ * == Nouns
+ *
+ * Inspired by reasonable uses of triple-stores, I have tried to leverage
+ *  existing model and terminology rather than rolling out own for everything.
+ *  The idea with triple-stores is that you have a subject, a predicate, and an
+ *  object.  For example, if we are talking about a message, that is the
+ *  subject, the predicate could roughly be sent-by, and the object a person.
+ *  We can generalize this idea to say that the subject and objects are nouns.
+ * Since we want to be more flexible than only dealing with messages, we
+ *  therefore introduce the concept of nouns as an organizing principle.
+ *
+ * == Attributes
+ *
+ * Our attributes definitions are basically our predicates.  When we define
+ *  an attribute, it's a label with a bunch of meta-data.  Our attribute
+ *  instances are basically a 'triple' in a triple-store.  The attributes
+ *  are stored in database rows that imply a specific noun-type (ex: the
+ *  messageAttributes table), with an ID identifying the message which is our
+ *  subject, an attribute ID which identifies the attribute definition in use
+ *  (and therefore the predicate), plus an object ID (given context aka the
+ *  noun type by the attribute's meta-data) which identifies the 'object'.
+ *
+ * == But...
+ *
+ * Things aren't entirely as clear as they could be right now, terminology/
+ *  concept/implementation-wise.  Some work is probably still in order.
+ *
+ * === Implementation
+ *
+ * == Nouns
+ *
+ * So, we go and define the nouns that are roughly the classes in our data
+ *  model.  Every 'class' we define in datamodel.js is a noun that gets defined
+ *  here in the Gloda core.  We provide sufficient meta-data about the noun to
+ *  serialize/deserialize its representation from our database representation.
+ *  Nouns do not have to be defined in this class, but can also be contributed
+ *  by external code.
+ * We have a concept of 'first class' nouns versus non-first class nouns.  The
+ *  distinction is meant to be whether we can store meta-information about those
+ *  nouns using attributes.  Right now, only message are real first-class nouns,
+ *  but we want to expand that to include contacts and eventually events and
+ *  tasks as lightning-integration occurs.  In practice, we are stretching the
+ *  definition of first-class nouns slightly to include things we can't store
+ *  meta-data about, but want to be able to query about.  We do want to resolve
+ *  this.
+ *
+ * == Attributes
+ *
+ * Attributes are defined by "attribute providers" who are responsible for
+ *  taking an instance of a first-class noun (for which they are registered)
+ *  plus perhaps some other meta-data, and returning a list of attributes
+ *  extracted from that noun.  For now, this means messages.  Attribute
+ *  providers may create new data records as a side-effect of the indexing
+ *  process, although we have not yet fully dealt with the problem of deleting
+ *  these records should they become orphaned in the database due to the
+ *  purging of a message and its attributes.
+ * All of the 'core' gloda attributes are provided by the fundattr.js and
+ *  explattr.js providers.
+ *
+ * === (Notable) Future Work
+ *
+ * == Attributes
+ *
+ * Attribute mechanisms currently lack any support for 'overriding' attributes
+ *  provided by other attribute providers.  For example, the fundattr provider
+ *  tells us who a message is 'from' based on the e-mail address present.
+ *  However, other plugins may actually know better.  For example, the bugzilla
+ *  daemon e-mails based on bug activity although the daemon gets the credit
+ *  as the official sender.  A bugzilla plugin can easily extract the actual
+ *  person/e-mail addressed who did something on the bug to cause the
+ *  notification to be sent.  In practice, we would like that person to be
+ *  the 'sender' of the bugmail.  But we can't really do that right, yet.
+ *
+ * @namespace
+ */
+var Gloda = {
+  /**
+   * Initialize logging, the datastore (SQLite database), the core nouns and
+   *  attributes, and the contact and identities that belong to the presumed
+   *  current user (based on accounts).
+   *
+   * Additional nouns and the core attribute providers are initialized by the
+   *  everybody.js module which ensures all of those dependencies are loaded
+   *  (and initialized).
+   */
+  _init: function gloda_ns_init() {
+    this._initLogging();
+    this._json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+    GlodaDatastore._init(this._json, this._nounIDToDef);
+    this._initAttributes();
+    this._initMyIdentities();
+  },
+
+  _log: null,
+  /**
+   * Initialize logging; the error console window gets Warning/Error, and stdout
+   *  (via dump) gets everything.
+   */
+  _initLogging: function gloda_ns_initLogging() {
+    let formatter = new Log4Moz.BasicFormatter();
+    let root = Log4Moz.Service.rootLogger;
+    root.level = Log4Moz.Level.Debug;
+
+    let enableConsoleLogging = false;
+    let enableDumpLogging = false;
+    let considerNetLogging = false;
+
+    try {
+      // figure out if event-driven indexing should be enabled...
+      let prefService = Cc["@mozilla.org/preferences-service;1"].
+                          getService(Ci.nsIPrefService);
+      let branch = prefService.getBranch("mailnews.database.global.logging.");
+      enableConsoleLogging = branch.getBoolPref("console");
+      enableDumpLogging = branch.getBoolPref("dump");
+      enableNetLogging = branch.getBoolPref("net");
+    } catch (ex) {}
+
+    if (enableConsoleLogging) {
+      let capp = new Log4Moz.ConsoleAppender(formatter);
+      capp.level = Log4Moz.Level.Warn;
+      root.addAppender(capp);
+    }
+
+    if (enableDumpLogging) {
+      let dapp = new Log4Moz.DumpAppender(formatter);
+      dapp.level = Log4Moz.Level.All;
+      root.addAppender(dapp);
+    }
+    
+    if (considerNetLogging) {
+      let file = Cc["@mozilla.org/file/directory_service;1"]
+                    .getService(Ci.nsIProperties)
+                    .get("TmpD", Ci.nsIFile);
+      file.append("chainsaw.ptr");
+      if (file.exists()) {
+        let data = GlodaUtils.loadFileToString(file);
+        data = data.trim();
+        let [host, port] = data.split(":");
+        let xf = new Log4Moz.XMLFormatter();
+        let sapp = new Log4Moz.SocketAppender(host, Number(port), xf);
+        sapp.level = Log4Moz.Level.All;
+        root.addAppender(sapp);
+      }
+    }
+
+    this._log = Log4Moz.Service.getLogger("gloda.NS");
+    this._log.info("Logging Initialized");
+  },
+
+  kIndexerIdle: 0,
+  kIndexerIndexing: 1,
+  kIndexerMoving: 2,
+  kIndexerRemoving: 3,
+
+  /** Synchronous activities performed, you can drive us more. */
+  kWorkSync: 0,
+  /**
+   * Asynchronous activity performed, you need to relinquish flow control and
+   *  trust us to call callbackDriver later.
+   */
+  kWorkAsync: 1,
+  /**
+   * We are all done with our task, close us and figure out something else to do.
+   */
+  kWorkDone: 2,
+  /**
+   * We are not done with our task, but we think it's a good idea to take a
+   *  breather.
+   */
+  kWorkPause: 3,
+  /**
+   * We are done with our task, and have a result that we are returning.  This
+   *  should only be used by your callback handler's doneWithResult method.
+   *  Ex: you are passed aCallbackHandle, and you do
+   *  "yield aCallbackHandle.doneWithResult(myResult);".
+   */
+  kWorkDoneWithResult: 4,
+
+  /**
+   * Lookup a gloda message from an nsIMsgDBHdr.
+   *
+   * @param aMsgHdr The header of the message you want the gloda message for.
+   *
+   * @return the gloda messages that corresponds to the provided nsIMsgDBHdr
+   *    if one exists, null if one cannot be found.
+   */
+  getMessageCollectionForHeader: function gloda_ns_getMessageForHeader(aMsgHdr,
+      aListener, aData) {
+    let query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
+    query.folder(aMsgHdr.folder).messageKey(aMsgHdr.messageKey);
+    return query.getCollection(aListener, aData);
+  },
+  
+  getMessageContent: function gloda_ns_getMessageContent(aGlodaMessage,
+      aMimeMsg) {
+    let content = new GlodaContent();
+    
+    let meta = {subject: aMimeMsg.get("subject")};
+    
+    let bodyLines = aMimeMsg.bodyPlain.split(/\r?\n/);
+    
+    // get the provider list in reverse order, mainly because we want more
+    //  specific content processors to get a chance before the default one
+    let attributeProviders =
+      this._attrProviderOrderByNoun[this.NOUN_MESSAGE].concat().reverse();
+    for each (let [, attrProvider] in Iterator(attributeProviders)) {
+      if (attrProvider.contentWhittle) {
+        try {
+          attrProvider.contentWhittle(aGlodaMessage, meta, bodyLines, content);
+        }
+        catch (ex) {
+          this._log.warn("Content whittler exception " + ex.fileName + ":" +
+            ex.lineNumber + ": " + ex);
+        }
+      }
+    }
+    
+    return content;
+  },
+  
+  getFolderForFolder: function gloda_ns_getFolderForFolder(aMsgFolder) {
+    return GlodaDatastore._mapFolder(aMsgFolder);
+  },
+  
+  /**
+   * Given one or more full mail addresses (ex: "Bob Smith" <bob@smith.com>),
+   *  return a list of the identities that corresponds to each mail address,
+   *  creating them as required.
+   */
+  getOrCreateMailIdentities:
+      function gloda_ns_getOrCreateMailIdentities(aCallbackHandle) {
+    let addresses = {};
+    let resultLists = [];
+    
+    for (let iArg = 1; iArg < arguments.length; iArg++) {
+      let aMailAddresses = arguments[iArg];
+      let parsed = GlodaUtils.parseMailAddresses(aMailAddresses);
+      
+      let resultList = [];
+      resultLists.push(resultList);
+      
+      let identities = [];
+      for (let iAddress = 0; iAddress < parsed.count; iAddress++) {
+        let address = parsed.addresses[iAddress];
+        if (address in addresses)
+          addresses[address].push(resultList);
+        else
+          addresses[address] = [parsed.names[iAddress], resultList];
+      }
+    }
+
+    let query = this.newQuery(this.NOUN_IDENTITY);
+    query.kind("email");
+    query.value.apply(query, [address for (address in addresses)]);
+    let collection = query.getCollection(aCallbackHandle);
+    yield this.kWorkAsync;
+
+    // put the identities in the appropriate result lists
+    for each (let [, identity] in Iterator(collection.items)) {
+      let nameAndResultLists = addresses[identity.value];
+      this._log.debug(" found identity for '" + nameAndResultLists[0] + "' (" +
+                      identity.value + ")");
+      // index 0 is the name, skip it
+      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
+        nameAndResultLists[iResList].push(identity);
+      }
+      delete addresses[identity.value];
+    }
+    
+    // create the identities that did not exist yet
+    for each (let [address, nameAndResultLists] in Iterator(addresses)) {
+      let name = nameAndResultLists[0];
+      
+      this._log.debug(" creating contact for '" + name + "' (" + address + ")");
+
+      // try and find an existing address book contact.
+      let card = GlodaUtils.getCardForEmail(address);
+      // XXX when we have the address book GUID stuff, we need to use that to
+      //  find existing contacts... (this will introduce a new query phase
+      //  where we batch all the GUIDs for an async query)
+      // XXX when the address book supports multiple e-mail addresses, we
+      //  should also just create identities for any that don't yet exist
+      
+      // if there is no name, just use the e-mail (the ab indexer actually
+      //  processes the card's displayName for synchronization, so we don't
+      //  need to do that.)
+      if (!name)
+        name = address;
+
+      let contact = GlodaDatastore.createContact(null, null, name, 0, 0);
+
+      // we must create the identity.  use a blank description because there's
+      //  nothing to differentiate it from other identities, as this contact
+      //  only has one initially (us).
+      // XXX when we have multiple e-mails and there is a meaning associated
+      //  with each e-mail, try and use that to populate the description.
+      // XXX we are creating the identity here before we insert the contact.
+      //  conceptually it is good for us to be creating the identity before
+      //  exposing it to the address-book indexer, but we could get our id's
+      //  in a bad way from not deferring the identity insertion until after
+      //  the contact insertion.
+      let identity = GlodaDatastore.createIdentity(contact.id, contact,
+        "email", address, /* description */ "", /* relay? */ false);
+      contact._identities = [identity];
+      
+      // give the address book indexer a chance if we have a card.
+      // (it will fix-up the name based on the card as appropriate)
+      if (card)
+        yield aCallbackHandle.pushAndGo(
+          Gloda.grokNounItem(contact, card, true, aCallbackHandle));
+      else // grokNounItem will issue the insert for us...
+        GlodaDatastore.insertContact(contact);
+
+      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
+        nameAndResultLists[iResList].push(identity);
+      }
+    }
+
+    yield aCallbackHandle.doneWithResult(resultLists);
+  },
+
+  /**
+   * Dictionary of the user's known identities; key is the identity id, value
+   *  is the actual identity.  This is populated by _initMyIdentities based on
+   *  the accounts defined.
+   */
+  myIdentities: {},
+  /**
+   * The contact corresponding to the current user.  We are assuming that only
+   *  a single user/human being uses the current profile.  This is known to be
+   *  a flawed assumption, but is the best first approximation available.
+   *
+   * @TODO attempt to deal with multile people using the same profile
+   */
+  myContact: null,
+  /**
+   * Populate myIdentities with all of our identities.  Currently we do this
+   *  by assuming that there is one human/user per profile, and that all of the
+   *  accounts defined in the profile belong to them.  The single contact is
+   *  stored on myContact.
+   *
+   * @TODO deal with account addition/modification/removal
+   * @TODO attempt to deal with multiple people using the same profile
+   */
+  _initMyIdentities: function gloda_ns_initMyIdentities() {
+    let myContact = null;
+    let myIdentities = {};
+    let myEmailAddresses = {}; // process each email at most once; stored here
+
+    let fullName = null;
+    let existingIdentities = [];
+    let identitiesToCreate = [];
+
+    let msgAccountManager = Cc["@mozilla.org/messenger/account-manager;1"].
+                            getService(Ci.nsIMsgAccountManager);
+    let numIdentities = msgAccountManager.allIdentities.Count();
+
+    // nothing to do if there are no accounts/identities.
+    if (!numIdentities)
+      return;
+
+    for (let iIdentity = 0; iIdentity < numIdentities; iIdentity++) {
+      let msgIdentity = msgAccountManager.allIdentities.GetElementAt(iIdentity)
+                                         .QueryInterface(Ci.nsIMsgIdentity);
+
+      if (fullName === null)
+        fullName = msgIdentity.fullName;
+
+      let emailAddress = msgIdentity.email;
+      let replyTo = msgIdentity.replyTo;
+
+      // find the identities if they exist, flag to create them if they don't
+      if (emailAddress) {
+        parsed = GlodaUtils.parseMailAddresses(emailAddress);
+        if (!(parsed.addresses[0] in myEmailAddresses)) {
+          let identity = GlodaDatastore.getIdentity("email",
+                                                    parsed.addresses[0]);
+          if (identity)
+            existingIdentities.push(identity);
+          else
+            identitiesToCreate.push(parsed.addresses[0]);
+          myEmailAddresses[parsed.addresses[0]] = true;
+        }
+      }
+      if (replyTo) {
+        parsed = GlodaUtils.parseMailAddresses(replyTo);
+        if (!(parsed.addresses[0] in myEmailAddresses)) {
+          let identity = GlodaDatastore.getIdentity("email",
+                                                    parsed.addresses[0]);
+          if (identity)
+            existingIdentities.push(identity);
+          else
+            identitiesToCreate.push(parsed.addresses[0]);
+          myEmailAddresses[parsed.addresses[0]] = true;
+        }
+      }
+    }
+
+    // we need to establish the identity.contact portions of the relationship
+    for each (let [,identity] in Iterator(existingIdentities)) {
+      identity._contact = GlodaDatastore.getContactByID(identity.contactID);
+    }
+    
+    if (existingIdentities.length) {
+      // just use the first guy's contact
+      myContact = existingIdentities[0].contact;
+    }
+    else {
+      // create a new contact
+      myContact = GlodaDatastore.createContact(null, null, fullName || "Me",
+                                               0, 0);
+      GlodaDatastore.insertContact(myContact);
+    }
+
+    if (identitiesToCreate.length) {
+      for (let iIdentity = 0; iIdentity < identitiesToCreate.length;
+          iIdentity++) {
+        let emailAddress = identitiesToCreate[iIdentity];
+        // XXX this won't always be of type "email" as we add new account types
+        // XXX the blank string could be trying to differentiate; we do have
+        //  enough info to do it.
+        let identity = GlodaDatastore.createIdentity(myContact.id, myContact,
+                                                     "email",
+                                                     emailAddress,
+                                                     "", false);
+        existingIdentities.push(identity);
+      }
+    }
+
+    for (let iIdentity = 0; iIdentity < existingIdentities.length;
+        iIdentity++) {
+      let identity = existingIdentities[iIdentity];
+      myIdentities[identity.id] = identity;
+    }
+
+    this.myContact = myContact;
+    this.myIdentities = myContact._identities = myIdentities;
+  },
+
+  /**
+   * An attribute that is a defining characteristic of the subject.
+   */
+  kAttrFundamental: 0,
+  /**
+   * An attribute that is an optimization derived from two or more fundamental
+   *  attributes and exists solely to improve database query performance.
+   */
+  kAttrOptimization: 1,
+  /**
+   * An attribute that is derived from the content of the subject.  For example,
+   *  a message that references a bugzilla bug could have a "derived" attribute
+   *  that captures the bugzilla reference.  This is not
+   */
+  kAttrDerived: 2,
+  /**
+   * An attribute that is the result of an explicit and intentional user action
+   *  upon the subject.  For example, a tag placed on a message by a user (or
+   *  at the user's request by a filter) is explicit.
+   */
+  kAttrExplicit: 3,
+  /**
+   * An attribute that is indirectly the result of a user's behaviour.  For
+   *  example, if a user consults a message multiple times, we may conclude that
+   *  the user finds the message interesting.  It is "implied", if you will,
+   *  that the message is interesting.
+   */
+  kAttrImplicit: 4,
+
+  /**
+   * This attribute is not 'special'; it is stored as a (thing id, attribute id,
+   *  attribute id) tuple in the database rather than on thing's row or on
+   *  thing's fulltext row.  (Where "thing" could be a message or any other
+   *  first class noun.)
+   */
+  kSpecialNotAtAll: GlodaDatastore.kSpecialNotAtAll,
+  /**
+   * This attribute is stored as a numeric column on the row for the noun.  The
+   *  attribute definition should include this value as 'special' and the
+   *  column name that stores the attribute as 'specialColumnName'.
+   */
+  kSpecialColumn: GlodaDatastore.kSpecialColumn,
+  kSpecialColumnChildren: GlodaDatastore.kSpecialColumnChildren,
+  kSpecialColumnParent: GlodaDatastore.kSpecialColumnParent,
+  /**
+   * This attribute is stored as a string column on the row for the noun.  It
+   *  differs from kSpecialColumn in that it is a string and thus uses different
+   *  query mechanisms.
+   */
+  kSpecialString: GlodaDatastore.kSpecialString,
+  /**
+   * This attribute is stored as a fulltext column on the fulltext table for
+   *  the noun.  The attribute defintion should include this value as 'special'
+   *  and the column name that stores the table as 'specialColumnName'.
+   */
+  kSpecialFulltext: GlodaDatastore.kSpecialFulltext,
+
+  /**
+   * The extensionName used for the attributes defined by core gloda plugins
+   *  such as fundattr.js and explattr.js.
+   */
+  BUILT_IN: "built-in",
+
+
+  /*
+   * The following are explicit noun IDs.  While most extension-provided nouns
+   *  will have dynamically allocated id's that are looked up by name, these
+   *  id's can be relied upon to exist and be accessible via these
+   *  pseudo-constants.  It's not really clear that we need these, although it
+   *  does potentially simplify code to not have to look up all of their nouns
+   *  at initialization time.
+   */
+  /**
+   * Boolean values, expressed as 0/1 in the database and non-continuous for
+   *  constraint purposes.  Like numbers, such nouns require their attributes
+   *  to provide them with context, lacking any of their own.
+   * Having this as a noun type may be a bad idea; a change of nomenclature
+   *  (so that we are not claiming a boolean value is a noun, but still using
+   *  it in the same way) or implementation to require each boolean noun
+   *  actually be its own noun may be in order.
+   */
+  NOUN_BOOLEAN: 1,
+  /**
+   * A number, which could mean an integer or floating point values.  We treat
+   *  these as continuous, meaning that queries on them can have ranged
+   *  constraints expressed on them.  Lacking any inherent context, numbers
+   *  depend on their attributes to parameterize them as required.
+   * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
+   */
+  NOUN_NUMBER: 2,
+  /**
+   * A (non-fulltext) string.
+   * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
+   */
+  NOUN_STRING: 3,
+  /** A date, encoded as a PRTime, represented as a js Date object. */
+  NOUN_DATE: 10,
+  /**
+   * Fulltext search support, somewhat magical.  This is only intended to be
+   *  used for kSpecialFulltext attributes, and exclusively as a constraint
+   *  mechanism.  The values are always represented as strings.  It is presumed
+   *  that the user of this functionality knows how to generate SQLite FTS3
+   *  style MATCH queries, or is okay with us just gluing them together with
+   *  " OR " when used in an or-constraint case.  Gloda's query mechanism
+   *  currently lacks the ability to to compile Gloda-style and-constraints
+   *  into a single MATCH query, but it will turn out okay, just less
+   *  efficiently than it could.
+   */
+  NOUN_FULLTEXT: 20,
+  /**
+   * Represents a MIME Type.  We currently lack any human-intelligible
+   *  descriptions of mime types.
+   */
+  NOUN_MIME_TYPE: 40, 
+  /**
+   * Captures a message tag as well as when the tag's presence was observed,
+   *  hoping to approximate when the tag was applied.  It's a somewhat dubious
+   *  attempt to not waste our opporunity to store a value along with the tag.
+   *  (The tag is actually stored as an attribute parameter on the attribute
+   *  definition, rather than a value in the attribute 'instance' for the
+   *  message.)
+   */
+  NOUN_TAG: 50,
+  /**
+   * Doesn't actually work owing to a lack of an object to represent a folder.
+   *  We do expose the folderURI and folderID of a message, but need to map that
+   *  to a good abstraction.  Probably something thin around a SteelFolder or
+   *  the like; we would contribute the functionality to easily move from a
+   *  folder to the list of gloda messages in that folder, as well as the
+   *  indexing preferences for that folder.
+   * @TODO folder noun and related abstraction
+   */
+  NOUN_FOLDER: GlodaFolder.prototype.NOUN_ID, // 100
+  /**
+   * All messages belong to a conversation.  See datamodel.js for the
+   *  definition of the GlodaConversation class.
+   */
+  NOUN_CONVERSATION: GlodaConversation.prototype.NOUN_ID, // 101
+  /**
+   * A one-to-one correspondence with underlying (indexed) nsIMsgDBHdr
+   *  instances.  See datamodel.js for the definition of the GlodaMessage class.
+   */
+  NOUN_MESSAGE: GlodaMessage.prototype.NOUN_ID, // 102
+  /**
+   * Corresponds to a human being, who may have multiple electronic identities
+   *  (a la NOUN_IDENTITY).  There is no requirement for association with an
+   *  address book contact, although when the address book contact exists,
+   *  we want to be associated with it.  See datamodel.js for the definition
+   *  of the GlodaContact class.
+   */
+  NOUN_CONTACT: GlodaContact.prototype.NOUN_ID, // 103
+  /**
+   * A single identity of a contact, who may have one or more.  E-mail accounts,
+   *  instant messaging accounts, social network site accounts, etc. are each
+   *  identities.  See datamodel.js for the definition of the GlodaIdentity
+   *  class.
+   */
+  NOUN_IDENTITY: GlodaIdentity.prototype.NOUN_ID, // 104
+
+  /**
+   * Parameterized identities, for use in the from-me, to-me, cc-me optimization
+   *  cases.  Not for reuse without some thought.  These nouns use the parameter
+   *  to store the 'me' identity that we are talking about, and the value to
+   *  store the identity of the other party.  So in both the from-me and to-me
+   *  cases involving 'me' and 'foo@bar', the 'me' identity is always stored via
+   *  the attribute parameter, and the 'foo@bar' identity is always stored as
+   *  the attribute value.  See fundattr.js for more information on this, but
+   *  you probably shouldn't be touching this unless you are fundattr.
+   */
+  NOUN_PARAM_IDENTITY: 200,
+
+  /** Next Noun ID to hand out, these don't need to be persisted (for now). */
+  _nextNounID: 1000,
+
+  /**
+   * Maps noun names to noun IDs.
+   */
+  _nounNameToNounID: {},
+  /**
+   * Maps noun IDs to noun definition dictionaries.  (Noun definition
+   *  dictionaries provided to us at the time a noun was defined, plus some
+   *  additional stuff we put in there.)
+   */
+  _nounIDToDef: {},
+  
+  _managedToJSON: function gloda_ns_managedToJSON(aItem) {
+    return aItem.id;
+  },
+
+  /**
+   * Define a noun.  Takes a dictionary with the following keys/values:
+   *
+   * @param name The name of the noun.  This is not a display name (anything
+   *     being displayed needs to be localized, after all), but simply the
+   *     canonical name for debugging purposes and for people to pass to
+   *     lookupNoun.  The suggested convention is lower-case-dash-delimited,
+   *     with names being singular (since it's a single noun we are referring
+   *     to.)
+   * @param class The 'class' to which an instance of the noun will belong (aka
+   *     will pass an instanceof test).
+   * @param allowsArbitraryAttrs Is this a 'first class noun'/can it be a subject, AKA can
+   *     this noun have attributes stored on it that relate it to other things?
+   *     For example, a message is first-class; we store attributes of
+   *     messages.  A date is not first-class now, nor is it likely to be; we
+   *     will not store attributes about a date, although dates will be the
+   *     objects of other subjects.  (For example: we might associate a date
+   *     with a calendar event, but the date is an attribute of the calendar
+   *     event and not vice versa.)
+   * @param usesParameter A boolean indicating whether this noun requires use
+   *     of the 'parameter' BLOB storage field on the attribute bindings in the
+   *     database to persist itself.  Use of parameters should be limited
+   *     to a reasonable number of values (16-32 is okay, more than that is
+   *     pushing it and 256 should be considered an absolute upper bound)
+   *     because of the database organization.  When false, your toParamAndValue
+   *     function is expected to return null for the parameter and likewise your
+   *     fromParamAndValue should expect ignore and generally ignore the
+   *     argument.
+   * @param toParamAndValue A function that takes an instantiated noun
+   *     instance and returns a 2-element list of [parameter, value] where
+   *     parameter may only be non-null if you passed a usesParameter of true.
+   *     Parameter may be of any type (BLOB), and value must be numeric (pass
+   *     0 if you don't need the value).
+   */
+  defineNoun: function gloda_ns_defineNoun(aNounDef, aNounID) {
+    this._log.info("Defining noun: " + aNounDef.name);
+    if (aNounID === undefined)
+      aNounID = this._nextNounID++;
+    aNounDef.id = aNounID;
+    // if it has a table, you can query on it.  seems straight-forward.
+    if (aNounDef.tableName) {
+      [aNounDef.queryClass, aNounDef.explicitQueryClass,
+       aNounDef.wildcardQueryClass] =
+          GlodaQueryClassFactory(aNounDef);
+      aNounDef._dbMeta = {};
+      aNounDef.class.prototype.NOUN_DEF = aNounDef;
+      aNounDef.toJSON = this._managedToJSON;
+      
+      aNounDef.specialLoadAttribs = [];
+
+      // - define the 'id' constrainer
+      let idConstrainer = function() {
+        let constraint = [GlodaDatastore.kConstraintIdIn, null];
+        for (let iArg = 0; iArg < arguments.length; iArg++) {
+          constraint.push(arguments[iArg]);
+        }
+        this._constraints.push(constraint);
+        return this;
+      };
+      aNounDef.queryClass.prototype.id = idConstrainer;
+    }
+    if (aNounDef.cache) {
+      let cacheCost = aNounDef.cacheCost || 1024;
+      let cacheBudget = aNounDef.cacheBudget || 128 * 1024;
+      let cacheSize = Math.floor(cacheBudget / cacheCost);
+      if (cacheSize)
+        GlodaCollectionManager.defineCache(aNounDef, cacheSize);
+    }
+    aNounDef.attribsByBoundName = {};
+    aNounDef.domExposeAttribsByBoundName = {};
+    
+    aNounDef.objectNounOfAttributes = [];
+
+    this._nounNameToNounID[aNounDef.name] = aNounID;
+    this._nounIDToDef[aNounID] = aNounDef;
+    aNounDef.actions = [];
+    
+    this._attrProviderOrderByNoun[aNounDef.id] = [];
+    this._attrOptimizerOrderByNoun[aNounDef.id] = [];
+    this._attrProvidersByNoun[aNounDef.id] = {};
+  },
+
+  /**
+   * Lookup a noun (ID) suitable for passing to defineAttribute's various
+   *  noun arguments.  Throws an exception if the noun with the given name
+   *  cannot be found; the assumption is that you can't live without the noun.
+   */
+  lookupNoun: function gloda_ns_lookupNoun(aNounName) {
+    if (aNounName in this._nounNameToNounID)
+      return this._nounNameToNounID[aNounName];
+
+    throw Error("Unable to locate noun with name '" + aNounName + "', but I " +
+                "do know about: " +
+                [propName for
+                 (propName in this._nounNameToNounID)].join(", "));
+  },
+
+  /**
+   * Lookup a noun def given a name.
+   */
+  lookupNounDef: function gloda_ns_lookupNoun(aNounName) {
+    return this._nounIDToDef[this.lookupNoun(aNounName)];
+  },
+
+  
+  /**
+   * Define an action on a noun.  During the prototype stage, this was conceived
+   *  of as a way to expose all the constraints possible given a noun.  For
+   *  example, if you have an identity or a contact, you could use this to
+   *  see all the messages sent from/to a given contact.  It was likewise
+   *  thought potentially usable for future expansion.  For example, you could
+   *  also decide to send an e-mail to a contact when you have the contact
+   *  instance available.
+   * Outside of the 'expmess' checkbox-happy prototype, this functionality is
+   *  not used.  As such, this functionality should be considered in flux and
+   *  subject to changes.  Also, very open to specific suggestsions motivated
+   *  by use cases.
+   * One conceptual issue raised by this mechanism is the interaction of actions
+   *  with facts like "this message is read".  We currently implement the 'fact'
+   *  by defining an attribute with a 'boolean' noun type.  To deal with this,
+   *  in various places we pass-in the attribute as well as the noun value.
+   *  Since the relationships for booleans and integers in these cases is
+   *  standard and well-defined, this works out pretty well, but suggests we
+   *  need to think things through.
+   *
+   * @param aNounID The ID of the noun you want to define an action on.
+   * @param aActionMeta The dictionary describing the noun.  The dictionary
+   *     should have the following fields:
+   * - actionType: a string indicating the type of action.  Currently, only
+   *   "filter" is a legal value.
+   * - actionTarget: the noun ID of the noun type on which this action is
+   *   applicable.  For example,
+   *
+   * The following should be present for actionType=="filter";
+   * - shortName: The name that should be used to display this constraint.  For
+   *   example, a checkbox-heavy UI might display a checkbox for each constraint
+   *   using shortName as the label.
+   * - makeConstraint: A function that takes the attribute that is the source
+   *   of the noun and the noun instance as arguments, and returns APV-style
+   *   constraints.  Since the APV-style query mechanism is now deprecated,
+   *   this signature is deprecated.  Probably the way to update this would be
+   *   to pass in the query instance that constraints should be contributed to.
+   */
+  defineNounAction: function gloda_ns_defineNounAction(aNounID, aActionMeta) {
+    let nounDef = this._nounIDToDef[aNounID];
+    nounDef.actions.push(aActionMeta);
+  },
+
+  /**
+   * Retrieve all of the actions (as defined using defineNounAction) for the
+   *  given noun type (via noun ID) with the given action type (ex: filter).
+   */
+  getNounActions: function gloda_ns_getNounActions(aNounID, aActionType) {
+    let nounDef = this._nounIDToDef[aNounID];
+    if (!nounDef)
+      return [];
+    return [action for each ([i, action] in Iterator(nounDef.actions))
+            if (!aActionType || (action.actionType == aActionType))];
+  },
+
+  /** Attribute providers in the sequence to process them. */
+  _attrProviderOrderByNoun: {},
+  /** Attribute providers that provide optimizers, in the sequence to proc. */
+  _attrOptimizerOrderByNoun: {},
+  /** Maps attribute providers to the list of attributes they provide */
+  _attrProviders: {},
+  /**
+   * Maps nouns to their attribute providers to a list of the attributes they
+   *  provide for the noun.
+   */
+  _attrProvidersByNoun: {},
+
+  /**
+   * Define the core nouns (that are not defined elsewhere) and a few noun
+   *  actions.  Core nouns could be defined in other files, assuming dependency
+   *  issues are resolved via the everybody.js mechanism or something else.
+   *  Right now, noun_tag defines the tag noun.  If we broke more of these out,
+   *  we would probably want to move the 'class' code from datamodel.js, the
+   *  SQL table def and helper code from datastore.js (and this code) to their
+   *  own noun_*.js files.  There are some trade-offs to be made, and I think
+   *  we can deal with those once we start to integrate lightning/calendar and
+   *  our noun space gets large and more heterogeneous.
+   */
+  _initAttributes: function gloda_ns_initAttributes() {
+    this.defineNoun({
+      name: "bool",
+      class: Boolean, allowsArbitraryAttrs: false,
+      toParamAndValue: function(aBool) {
+        return [null, aBool ? 1 : 0];
+      }}, this.NOUN_BOOLEAN);
+    this.defineNoun({
+      name: "number",
+      class: Number, allowsArbitraryAttrs: false, continuous: true,
+      toParamAndValue: function(aNum) {
+        return [null, aNum];
+      }}, this.NOUN_NUMBER);
+    this.defineNoun({
+      name: "string",
+      class: String, allowsArbitraryAttrs: false,
+      toParamAndValue: function(aString) {
+        return [null, aString];
+      }}, this.NOUN_STRING);
+    this.defineNoun({
+      name: "date",
+      class: Date, allowsArbitraryAttrs: false, continuous: true,
+      toParamAndValue: function(aDate) {
+        return [null, aDate.valueOf() * 1000];
+      }}, this.NOUN_DATE);
+    this.defineNoun({
+      name: "fulltext",
+      class: String, allowsArbitraryAttrs: false, continuous: false,
+      // as noted on NOUN_FULLTEXT, we just pass the string around.  it never
+      //  hits the database, so it's okay.
+      toParamAndValue: function(aString) {
+        return [null, aString];
+      }}, this.NOUN_FULLTEXT);
+
+    this.defineNoun({
+      name: "folder",
+      class: GlodaFolder,
+      allowsArbitraryAttrs: false,
+      toParamAndValue: function(aFolderOrGlodaFolder) {
+        if (aFolderOrGlodaFolder instanceof GlodaFolder)
+          return [null, aFolderOrGlodaFolder.id];
+        else
+          return [null, GlodaDatastore._mapFolder(aFolderOrGlodaFolder).id];
+      }}, this.NOUN_FOLDER);
+    this.defineNoun({
+      name: "conversation",
+      class: GlodaConversation,
+      allowsArbitraryAttrs: false,
+      cache: true, cacheCost: 512,
+      tableName: "conversations",
+      attrTableName: "messageAttributes", attrIDColumnName: "conversationID",
+      datastore: GlodaDatastore,
+      objFromRow: GlodaDatastore._conversationFromRow,
+      toParamAndValue: function(aConversation) {
+        if (aConversation instanceof GlodaConversation)
+          return [null, aConversation.id];
+        else // assume they're just passing the id directly
+          return [null, aConversation];
+      }}, this.NOUN_CONVERSATION);
+    this.defineNoun({
+      name: "message",
+      class: GlodaMessage,
+      allowsArbitraryAttrs: true,
+      cache: true, cacheCost: 2048,
+      tableName: "messages",
+      attrTableName: "messageAttributes", attrIDColumnName: "messageID",
+      datastore: GlodaDatastore, objFromRow: GlodaDatastore._messageFromRow,
+      dbAttribAdjuster: GlodaDatastore.adjustMessageAttributes,
+      objInsert: GlodaDatastore.insertMessage,
+      objUpdate: GlodaDatastore.updateMessage,
+      toParamAndValue: function(aMessage) {
+        if (aMessage instanceof GlodaMessage)
+          return [null, aMessage.id];
+        else // assume they're just passing the id directly
+          return [null, aMessage];
+      }}, this.NOUN_MESSAGE);
+    this.defineNoun({
+      name: "contact",
+      class: GlodaContact,
+      allowsArbitraryAttrs: true,
+      cache: true, cacheCost: 128,
+      tableName: "contacts",
+      attrTableName: "contactAttributes", attrIDColumnName: "contactID",
+      datastore: GlodaDatastore, objFromRow: GlodaDatastore._contactFromRow,
+      dbAttribAdjuster: GlodaDatastore.adjustAttributes,
+      objInsert: GlodaDatastore.insertContact,
+      objUpdate: GlodaDatastore.updateContact,
+      toParamAndValue: function(aContact) {
+        if (aContact instanceof GlodaContact)
+          return [null, aContact.id];
+        else // assume they're just passing the id directly
+          return [null, aContact];
+      }}, this.NOUN_CONTACT);
+    this.defineNoun({
+      name: "identity",
+      class: GlodaIdentity,
+      allowsArbitraryAttrs: false,
+      cache: true, cacheCost: 128,
+      usesUniqueValue: true,
+      tableName: "identities",
+      datastore: GlodaDatastore, objFromRow: GlodaDatastore._identityFromRow,
+      toParamAndValue: function(aIdentity) {
+        if (aIdentity instanceof GlodaIdentity)
+          return [null, aIdentity.id];
+        else // assume they're just passing the id directly
+          return [null, aIdentity];
+      }}, this.NOUN_IDENTITY);
+
+    // parameterized identity is just two identities; we store the first one
+    //  (whose value set must be very constrainted, like the 'me' identities)
+    //  as the parameter, the second (which does not need to be constrained)
+    //  as the value.
+    this.defineNoun({
+      name: "parameterized-identity",
+      class: null,
+      allowsArbitraryAttrs: false,
+      computeDelta: function(aCurValues, aOldValues) {
+        let oldMap = {};
+        for each (let [, tupe] in Iterator(aOldValues)) {
+          let [originIdentity, targetIdentity] = tupe;
+          let targets = oldMap[originIdentity];
+          if (targets === undefined)
+            targets = oldMap[originIdentity] = {};
+          targets[targetIdentity] = true;
+        }
+        
+        let added = [], removed = [];
+        for each (let [, tupe] in Iterator(aCurValues)) {
+          let [originIdentity, targetIdentity] = tupe;
+          let targets = oldMap[originIdentity];
+          if ((targets === undefined) || !(targetIdentity in targets))
+            added.push(tupe);
+          else
+            delete targets[targetIdentity];
+        }
+        
+        for each (let [originIdentity, targets] in Iterator(oldMap)) {
+          for (let targetIdentity in targets) {
+            removed.push([originIdentity, targetIdentity]);
+          }
+        }
+        
+        return [added, removed];
+      },
+      contributeObjDependencies: function(aJsonValues, aReferencesByNounID,
+          aInverseReferencesByNounID) {
+        // nothing to do with a zero-length list
+        if (aJsonValues.length == 0)
+          return false;
+      
+        let nounIdentityDef = Gloda._nounIDToDef[Gloda.NOUN_IDENTITY]
+        let references = aReferencesByNounID[nounIdentityDef.id];
+        if (references === undefined)
+          references = aReferencesByNounID[nounIdentityDef.id] = {};
+        
+        for each (let [, tupe] in Iterator(aJsonValues)) {
+          let [originIdentityID, targetIdentityID] = tupe;
+          if (!(originIdentityID in references))
+            references[originIdentityID] = null;
+          if (!(targetIdentityID in references))
+            references[targetIdentityID] = null;
+        }
+        
+        return true;
+      },
+      resolveObjDependencies: function(aJsonValues, aReferencesByNounID,
+          aInverseReferencesByNounID) {
+        let references =
+          aReferencesByNounID[Gloda.NOUN_IDENTITY];
+        
+        let results = [];
+        for each (let [, tupe] in Iterator(aJsonValues)) {
+          let [originIdentityID, targetIdentityID] = tupe;
+          results.push([references[originIdentityID],
+                        references[targetIdentityID]]);
+        }
+        
+        return results;
+      },
+      toJSON: function (aIdentityTuple) {
+        return [aIdentityTuple[0].id, aIdentityTuple[1].id];
+      },
+      toParamAndValue: function(aIdentityTuple) {
+        return [aIdentityTuple[0].id, aIdentityTuple[1].id];
+      }}, this.NOUN_PARAM_IDENTITY);
+
+    GlodaDatastore.getAllAttributes();
+  },
+
+  /**
+   * Create accessor functions to 'bind' an attribute to underlying normalized
+   *  attribute storage, as well as creating the appropriate query object
+   *  constraint helper functions.  This name is somewhat of a misnomer because
+   *  special attributes are not 'bound' (because specific/non-generic per-class
+   *  code provides the properties) but still depend on this method to
+   *  establish their constraint helper methods.
+   *
+   * @XXX potentially rename to not suggest binding is required.
+   */
+  _bindAttribute: function gloda_ns_bindAttr(aAttrDef, aSubjectNounDef) {
+    let objectNounDef = aAttrDef.objectNounDef;
+
+    // -- the query constraint helpers
+    if (aSubjectNounDef.queryClass !== undefined) {
+      let constrainer;
+      // non-strings can use IN
+      if (aAttrDef.special == this.kSpecialFulltext) {
+        constrainer = function() {
+          let constraint = [GlodaDatastore.kConstraintFulltext, aAttrDef];
+          for (let iArg = 0; iArg < arguments.length; iArg++) {
+            constraint.push(arguments[iArg]);
+          }
+          this._constraints.push(constraint);
+          return this;
+        };
+      }
+      else if (aAttrDef.special != this.kSpecialString) {
+        constrainer = function() {
+          let constraint = [GlodaDatastore.kConstraintIn, aAttrDef];
+          for (let iArg = 0; iArg < arguments.length; iArg++) {
+            constraint.push(arguments[iArg]);
+          }
+          this._constraints.push(constraint);
+          return this;
+        };
+      }
+      else { // strings need to use equals for escaping reasons
+        // (we could introduce an 'escaped' in that we manually escape though)
+        constrainer = function() {
+          let constraint = [GlodaDatastore.kConstraintEquals, aAttrDef];
+          for (let iArg = 0; iArg < arguments.length; iArg++) {
+            constraint.push(arguments[iArg]);
+          }
+          this._constraints.push(constraint);
+          return this;
+        };
+      }
+
+      aSubjectNounDef.queryClass.prototype[aAttrDef.boundName] = constrainer;
+
+      // - ranged value helper: fooRange
+      if (objectNounDef.continuous) {
+        // takes one or more tuples of [lower bound, upper bound]
+        let rangedConstrainer = function() {
+          let constraint = [GlodaDatastore.kConstraintRanges, aAttrDef];
+          for (let iArg = 0; iArg < arguments.length; iArg++ ) {
+            constraint.push(arguments[iArg]);
+          }
+          this._constraints.push(constraints);
+          return this;
+        }
+
+        aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Range"] =
+          rangedConstrainer;
+      }
+
+      // - string LIKE helper for special on-row attributes: fooLike
+      // (it is impossible to store a string as an indexed attribute, which is
+      //  why we do this for on-row only.)
+      if (aAttrDef.special == this.kSpecialString) {
+        let likeConstrainer = function() {
+          let constraint = [GlodaDatastore.kConstraintStringLike, aAttrDef];
+          for (let iArg = 0; iArg < arguments.length; iArg++) {
+            constraint.push(arguments[iArg]);
+          }
+          this._constraints.push(constraint);
+          return this;
+        }
+
+        aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Like"] =
+          likeConstrainer;
+      }
+    }
+  },
+
+  /**
+   * Define an attribute and all its meta-data.  Takes a single dictionary as
+   *  its argument, with the following required properties:
+   *
+   * @param provider The object instance providing a 'process' method.
+   * @param extensionName The name of the extension providing these attributes.
+   * @param attributeType The type of attribute, one of the values from the
+   *     kAttr* enumeration.
+   * @param attributeName The name of the attribute, which also doubles as the
+   *     bound property name if you pass 'bind' a value of true.  You are
+   *     responsible for avoiding collisions, which presumably will mean
+   *     checking/updating a wiki page in the future, or just prefixing your
+   *     attribute name with your extension name or something like that.
+   * @param bind Should this attribute be 'bound' as a convenience attribute
+   *     on the subject's object (true/false)?  For example, with an
+   *     attributeName of "foo" and passing true for 'bind' with a subject noun
+   *     of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter
+   *     that returns the value of the attribute.  If 'singular' is true, this
+   *     means an instance of the object class corresponding to the noun type or
+   *     null if the attribute does not exist.  If 'singular' is false, this
+   *     means a list of instances of the object class corresponding to the noun
+   *     type, where the list may be empty if no instances of the attribute are
+   *     present.
+   * @param bindName Optional override of attributeName for purposes of the
+   *     binding property's name.
+   * @param singular Is the attribute going to happen at most once (true),
+   *     or potentially multiple times (false).  This affects whether
+   *     the binding  returns a list or just a single item (which is null when
+   *     the attribute is not present).
+   * @param subjectNouns A list of object types (NOUNs) that this attribute can
+   *     be set on.  Each element in the list should be one of the NOUN_*
+   *     constants or a dynamically registered noun type.
+   * @param objectNoun The object type (one of the NOUN_* constants or a
+   *     dynamically registered noun types) that is the 'object' in the
+   *     traditional RDF triple.  More pragmatically, in the database row used
+   *     to represent an attribute, we store the subject (ex: message ID),
+   *     attribute ID, and an integer which is the integer representation of the
+   *     'object' whose type you are defining right here.
+   */
+  defineAttribute: function gloda_ns_defineAttribute(aAttrDef) {
+    // ensure required properties exist on aAttrDef
+    if (!("provider" in aAttrDef) ||
+        !("extensionName" in aAttrDef) ||
+        !("attributeType" in aAttrDef) ||
+        !("attributeName" in aAttrDef) ||
+        !("singular" in aAttrDef) ||
+        !("subjectNouns" in aAttrDef) ||
+        !("objectNoun" in aAttrDef))
+      // perhaps we should have a list of required attributes, perchance with
+      //  and explanation of what it holds, and use that to be friendlier?
+      throw Error("You omitted a required attribute defining property, please" +
+                  " consult the documentation as penance.")
+
+    // return if the attribute has already been defined
+    if (aAttrDef.dbDef) {
+      return aAttrDef;
+    }
+
+    // provider tracking
+    if (!(aAttrDef.provider.providerName in this._attrProviders)) {
+      this._attrProviders[aAttrDef.provider.providerName] = [];
+    }
+
+    let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
+    let attrDBDef;
+    if (compoundName in GlodaDatastore._attributeDBDefs) {
+      // the existence of the GlodaAttributeDBDef means that either it has
+      //  already been fully defined, or has been loaded from the database but
+      //  not yet 'bound' to a provider (and had important meta-info that
+      //  doesn't go in the db copied over)
+      attrDBDef = GlodaDatastore._attributeDBDefs[compoundName];
+    }
+    // we need to create the attribute definition in the database
+    else {
+      let attrID = null;
+      attrID = GlodaDatastore._createAttributeDef(aAttrDef.attributeType,
+                                                  aAttrDef.extensionName,
+                                                  aAttrDef.attributeName,
+                                                  null);
+    
+      attrDBDef = new GlodaAttributeDBDef(GlodaDatastore, attrID, compoundName,
+        aAttrDef.attributeType, aAttrDef.extensionName, aAttrDef.attributeName);
+      GlodaDatastore._attributeDBDefs[compoundName] = attrDBDef;
+      GlodaDatastore._attributeIDToDBDefAndParam[attrID] = [attrDBDef, null];
+    }
+    
+    aAttrDef.dbDef = attrDBDef;
+    attrDBDef.attrDef = aAttrDef;
+    
+    aAttrDef.id = aAttrDef.dbDef.id;
+
+    if ("bindName" in aAttrDef)
+      aAttrDef.boundName = aAttrDef.bindName;
+    else
+      aAttrDef.boundName = aAttrDef.attributeName;
+    
+    aAttrDef.objectNounDef = this._nounIDToDef[aAttrDef.objectNoun];
+    aAttrDef.objectNounDef.objectNounOfAttributes.push(aAttrDef);
+
+    for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
+           iSubject++) {
+      let subjectType = aAttrDef.subjectNouns[iSubject];
+      let subjectNounDef = this._nounIDToDef[subjectType];
+      this._bindAttribute(aAttrDef, subjectNounDef);
+
+      // update the provider maps...
+      if (this._attrProviderOrderByNoun[subjectType]
+              .indexOf(aAttrDef.provider) == -1) {
+        this._attrProviderOrderByNoun[subjectType].push(aAttrDef.provider);
+        if (aAttrDef.provider.optimize)
+          this._attrOptimizerOrderByNoun[subjectType].push(aAttrDef.provider);
+        this._attrProvidersByNoun[subjectType][aAttrDef.provider] = [];
+      }
+      this._attrProvidersByNoun[subjectType][aAttrDef.provider].push(aAttrDef);
+      
+      subjectNounDef.attribsByBoundName[aAttrDef.boundName] = aAttrDef;
+      if (aAttrDef.domExpose)
+        subjectNounDef.domExposeAttribsByBoundName[aAttrDef.boundName] =
+          aAttrDef;
+      
+      if (aAttrDef.special & this.kSpecialColumn)
+        subjectNounDef.specialLoadAttribs.push(aAttrDef);
+      
+      // if this is a parent column attribute, make note of it so that if we
+      //  need to do an inverse references lookup, we know what column we are
+      //  issuing against.
+      if (aAttrDef.special === this.kSpecialColumnParent) {
+        subjectNounDef.parentColumnAttr = aAttrDef;
+      }
+      
+      if (aAttrDef.objectNounDef.tableName ||
+          aAttrDef.objectNounDef.contributeObjDependencies) {
+        subjectNounDef.hasObjDependencies = true;
+      }
+    }
+    
+    this._attrProviders[aAttrDef.provider.providerName].push(aAttrDef);
+    return aAttrDef;
+  },
+
+  /**
+   * Retrieve the attribute provided by the given extension with the given
+   *  attribute name.  The original idea was that plugins would effectively
+   *  name-space attributes, helping avoid collisions.  Since we are leaning
+   *  towards using binding heavily, this doesn't really help, as the collisions
+   *  will just occur on the attribute name instead.  Also, this can turn
+   *  extensions into liars as name changes/moves to core/etc. happen.
+   * @TODO consider removing the extension name argument parameter requirement
+   */
+  getAttrDef: function gloda_ns_getAttrDef(aPluginName, aAttrName) {
+    let compoundName = aPluginName + ":" + aAttrName;
+    return GlodaDatastore._attributeDBDefs[compoundName];
+  },
+
+  /**
+   * Define a SQL table for plug-ins.  This is intended to be used by
+   *  extensions/plug-ins whose storage needs exceed those provided by the
+   *  attribute parameter (on the attribute definition)/attribute value (on the
+   *  attribute instance) idiom.  (This includes extensions whose parameter
+   *  usage would exceed acceptable cardinality.)  They can create a table
+   *  to store information on their nouns, using their row id (commonly "id")
+   *  as the attribute value.
+   * The current implementation was for a prototype and this should not be
+   *  interpreted as our final approach.  Our goal is just to make it easy to
+   *  add your own data-type and have it interact with the rest of the gloda
+   *  schema.  We don't really want to be a be-all, end-all JS ORM (object
+   *  relational mapper), though we started down that road.
+   *
+   * The argument should be a dictionary with the following keys:
+   * @param name The table name; don't conflict with other things!
+   * @param columns A list of [column name, sqlite type] tuples.  You should
+   *     always include a definition like ["id", "INTEGER PRIMARY KEY"] for
+   *     now.
+   * @param indices A dictionary of lists of column names, where the key name
+   *     becomes the index name.  Ex: {foo: ["bar"]} results in an index on
+   *     the column "bar" where the index is named "foo".
+   */
+  defineTable: function gloda_ns_defineTable(aTableDef) {
+    return GlodaDatastore.createTableIfNotExists(aTableDef);
+  },
+
+  /**
+   * Create a new query instance for the given noun-type.  This provides
+   *  a generic way to provide constraint-based queries of any first-class
+   *  nouns supported by the system.
+   *
+   * The idea is that every attribute on an object can be used to express
+   *  a constraint on the query object.  Constraints implicitly 'AND' together,
+   *  but providing multiple arguments to a constraint function results in an
+   *  'OR'ing of those values.  Additionally, you can call or() on the returned
+   *  query to create an alternate query that is effectively a giant OR against
+   *  all the constraints you create on the main query object (or any other
+   *  alternate queries returned by or()).  (Note: there is no nesting of these
+   *  alternate queries. query.or().or() is equivalent to query.or())
+   * For each attribute, there is a constraint with the same name that takes
+   *  one or more arguments.  The arguments represent a set of OR values that
+   *  objects matching the query can have.  (If you want the constraint
+   *  effectively ANDed together, just invoke the constraint function
+   *  multiple times.)  For example, newQuery(NOUN_PERSON).age(25) would
+   *  constraint to all the people aged 25, while age(25, 26) would constrain
+   *  to all the people age 25 or 26.
+   * For each attribute with a 'continuous' noun, there is a constraint with the
+   *  attribute name with "Range" appended.  It takes two arguments which are an
+   *  inclusive lower bound and an inclusive lower bound for values in the
+   *  range.  If you would like an open-ended range on either side, pass null
+   *  for that argument.  If you would like to specify multiple ranges that
+   *  should be ORed together, simply pass additional (pairs of) arguments.
+   *  For example, newQuery(NOUN_PERSON).age(25,100) would constraint to all
+   *  the people who are >= 25 and <= 100.  Likewise age(25, null) would just
+   *  return all the people who are 25 or older.  And age(25,30,35,40) would
+   *  return people who are either 25-30 or 35-30.
+   * There are also full-text constraint columns.  In a nutshell, their
+   *  arguments are the strings that should be passed to the SQLite FTS3
+   *  MATCH clause.
+   */
+  newQuery: function gloda_ns_newQuery(aNounID) {
+    let nounDef = this._nounIDToDef[aNounID];
+    return new nounDef.queryClass();
+  },
+
+  /**
+   * Create a collection/query for the given noun-type that only matches the
+   *  provided items.  This is to be used when you have an explicit set of items
+   *  that you would still like to receive updates for.
+   */
+  explicitCollection: function gloda_ns_explicitCollection(aNounID, aItems) {
+    let nounDef = this._nounIDToDef[aNounID];
+    let collection = new GlodaCollection(nounDef, aItems, null, null)
+    let query = new nounDef.explicitQueryClass(collection);
+    collection.query = query;
+    GlodaCollectionManager.registerCollection(collection);
+    return collection;
+  },
+
+  /**
+   * Debugging 'wildcard' collection creation support.  A wildcard collection
+   *  will 'accept' any new item instances presented to the collection manager
+   *  as new.  The result is that it allows you to be notified as new items
+   *  as they are indexed, existing items as they are loaded from the database,
+   *  etc.
+   * Because the items are added to the collection without limit, this will
+   *  result in a leak if you don't do something to clean up after the
+   *  collection.  (Forgetting about the collection will suffice, as it is still
+   *  weakly held.)
+   */
+  _wildcardCollection: function gloda_ns_wildcardCollection(aNounID, aItems) {
+    let nounDef = this._nounIDToDef[aNounID];
+    let collection = new GlodaCollection(nounDef, aItems, null, null)
+    let query = new nounDef.wildcardQueryClass(collection);
+    collection.query = query;
+    GlodaCollectionManager.registerCollection(collection);
+    return collection;
+  },
+
+  /**
+   * Populate a gloda representation of an item given the thus-far built
+   *  representation, the previous representation, and one or more raw
+   *  representations.
+   *
+   * The result of the processing ends up with attributes in 3 different forms:
+   * - Database attribute rows (to be added and removed).
+   * - In-memory representation.
+   * - JSON-able representation.
+   */
+  grokNounItem: function gloda_ns_grokNounItem(aItem, aRawReps, aIsNew,
+      aCallbackHandle, aDoCache) {
+    let itemNounDef = this._nounIDToDef[aItem.NOUN_ID];
+    let attribsByBoundName = itemNounDef.attribsByBoundName;
+    
+    this._log.info(" ** grokNounItem: " + itemNounDef.name);
+    
+    let addDBAttribs = [];
+    let removeDBAttribs = [];
+    
+    let jsonDict = {};
+    
+    let aOldItem;
+    if (aIsNew) // there is no old item if we are new.
+      aOldItem = {};
+    else {
+      aOldItem = aItem;
+      // we want to create a clone of the existing item so that we can know the
+      //  deltas that happened for indexing purposes
+      aItem = aItem._clone();
+    }
+  
+    // Have the attribute providers directly set properties on the aItem
+    let attrProviders = this._attrProviderOrderByNoun[aItem.NOUN_ID];
+    for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
+      this._log.info("  * provider: " + attrProviders[iProvider].providerName);
+      yield aCallbackHandle.pushAndGo(
+        attrProviders[iProvider].process(aItem, aRawReps, aIsNew,
+                                         aCallbackHandle));
+    }
+    
+    let attrOptimizers = this._attrOptimizerOrderByNoun[aItem.NOUN_ID];
+    for (let iProvider = 0; iProvider < attrOptimizers.length; iProvider++) {
+      this._log.info("  * optimizer: " + attrOptimizers[iProvider].providerName);
+      yield aCallbackHandle.pushAndGo(
+        attrOptimizers[iProvider].optimize(aItem, aRawReps, aIsNew,
+                                           aCallbackHandle));
+    }
+    
+    this._log.info(" ** done with providers.");
+  
+    // Iterate over the attributes on the item
+    for each (let [key, value] in Iterator(aItem)) {
+      // ignore keys that start with underscores, they are private and not
+      //  persisted by our attribute mechanism.  (they are directly handled by
+      //  the object implementation.)
+      if (key[0] == "_")
+        continue;
+      // find the attribute definition that corresponds to this key
+      let attrib = attribsByBoundName[key];
+      // if there's no attribute, that's not good, but not horrible.
+      if (attrib === undefined) {
+        this._log.warn("new proc ignoring attrib: " + key);
+        continue;
+      }
+
+      let attribDB = attrib.dbDef;
+      let objectNounDef = attrib.objectNounDef;
+      
+      // - translate for our JSON rep
+      if (attrib.singular) {
+        if (objectNounDef.toJSON)
+          jsonDict[attrib.id] = objectNounDef.toJSON(value);
+        else
+          jsonDict[attrib.id] = value; 
+      }
+      else {
+        if (objectNounDef.toJSON) {
+          toJSON = objectNounDef.toJSON;
+          jsonDict[attrib.id] = [toJSON(subValue) for each
+                           ([, subValue] in Iterator(value))] ;
+        }
+        else
+          jsonDict[attrib.id] = value;
+      }
+      
+      // perform a delta analysis against the old value, if we have one
+      let oldValue = aOldItem[key];
+      if (oldValue !== undefined) {
+        // in the singular case if they don't match, it's one add and one remove
+        if (attrib.singular) {
+          // test for identicality, failing that, see if they have explicit
+          //  equals support.
+          if ((value !== oldValue) &&
+              (!value.equals || !value.equals(oldValue))) {
+            this._log.debug("%% want to add1 " + value + " which map to " + attribDB.convertValuesToDBAttributes([value]));
+            this._log.debug("%% want to rem1 " + oldValue + " which map to " + attribDB.convertValuesToDBAttributes([oldValue]));
+            addDBAttribs.push(attribDB.convertValuesToDBAttributes([value])[0]);
+            removeDBAttribs.push(
+              attribDB.convertValuesToDBAttributes([oldValue])[0]);
+          }
+        }
+        // in the plural case, we have to figure the deltas accounting for
+        //  possible changes in ordering (which is insignificant from an
+        //  indexing perspective)
+        // some nouns may not meet === equivalence needs, so must provide a
+        //  custom computeDelta method to help us out
+        else if (objectNounDef.computeDelta) {
+          let [valuesAdded, valuesRemoved] = 
+            objectNounDef.computeDelta(value, oldValue);
+          // convert the values to database-style attribute rows
+          this._log.debug("%% cdelta want to add " + valuesAdded + " which map to " + attribDB.convertValuesToDBAttributes(valuesAdded));
+          this._log.debug("%% cdelta want to rem " + valuesRemoved + " which map to " + attribDB.convertValuesToDBAttributes(valuesRemoved));
+          addDBAttribs.push.apply(addDBAttribs,
+            attribDB.convertValuesToDBAttributes(valuesAdded));
+          removeDBAttribs.push.apply(removeDBAttribs,
+            attribDB.convertValuesToDBAttributes(valuesRemoved));
+        }
+        else {
+          // build a map of the previous values; we will delete the values as
+          //  we see them so that we will know what old values are no longer
+          //  present in the current set of values.
+          let oldValueMap = {};
+          for each (let [, anOldValue] in Iterator(oldValue)) {
+this._log.debug("  old traverse: " + anOldValue);
+            // remember, the key is just the toString'ed value, so we need to
+            //  store and use the actual value as the value!
+            oldValueMap[anOldValue] = anOldValue;
+          }
+          // traverse the current values...
+          let valuesAdded = [];
+          for each (let [, curValue] in Iterator(value)) {
+this._log.debug("  new traverse: " + curValue);
+            if (curValue in oldValueMap)
+              delete oldValueMap[curValue];
+            else
+              valuesAdded.push(curValue);
+          }
+          // anything still on oldValueMap was removed.
+          let valuesRemoved = [val for each (val in oldValueMap)];
+          // convert the values to database-style attribute rows
+          this._log.debug("%% want to add " + valuesAdded + " which map to " + attribDB.convertValuesToDBAttributes(valuesAdded));
+          this._log.debug("%% want to rem " + valuesRemoved + " which map to " + attribDB.convertValuesToDBAttributes(valuesRemoved));
+          addDBAttribs.push.apply(addDBAttribs,
+            attribDB.convertValuesToDBAttributes(valuesAdded));
+          removeDBAttribs.push.apply(removeDBAttribs,
+            attribDB.convertValuesToDBAttributes(valuesRemoved));
+        }
+      
+        // replace the old value with the new values... (the 'old' item is
+        //  canonical)
+        aOldItem[key] = value; 
+      }
+      // no old value, all values are new
+      else {
+        // the 'old' item is still the canonical one; update it
+        if (!aIsNew)
+          aOldItem[key] = value;
+        // add the db reps on the new values
+        if (attrib.singular)
+          value = [value];
+        this._log.debug("%% no old, want to add " + value + " which map to " + attribDB.convertValuesToDBAttributes(value));
+        addDBAttribs.push.apply(addDBAttribs,
+                                attribDB.convertValuesToDBAttributes(value));
+      }
+    }
+    
+    // Iterate over any remaining values in old items for purge purposes.
+    for each (let [key, value] in Iterator(aOldItem)) {
+      // ignore keys that start with underscores, they are private and not
+      //  persisted by our attribute mechanism.  (they are directly handled by
+      //  the object implementation.)
+      if (key[0] == "_")
+        continue;
+      // ignore things we saw in the new guy
+      if (key in aItem)
+        continue;
+      
+      // find the attribute definition that corresponds to this key
+      let attrib = attribsByBoundName[key];
+      // if there's no attribute, that's not good, but not horrible.
+      if (attrib === undefined) {
+        this._log.warn("old proc ignoring attrib: " + key);
+        continue;
+      }
+      let attribDB = attrib.dbDef;
+      this._log.debug("%% want to remove " + value + " which map to " + attribDB.convertValuesToDBAttributes(value));
+      removeDBAttribs.push.apply(removeDBAttribs,
+                                 attribDB.convertValuesToDBAttributes(value));
+      // delete these from the old item, as the old item is canonical, and
+      //  should no longer have these values
+      delete aOldItem[key];
+    }
+    
+    aItem._jsonText = this._json.encode(jsonDict);
+    this._log.debug("  json text: " + aItem._jsonText);
+    
+    if (aIsNew) {
+      this._log.debug(" inserting item");
+      itemNounDef.objInsert.call(itemNounDef.datastore, aItem);
+    }
+    else {
+      this._log.debug(" updating item");
+      itemNounDef.objUpdate.call(itemNounDef.datastore, aItem);
+    }
+    
+    this._log.debug(" adjusting attributes, add: " + addDBAttribs + " rem: " +
+        removeDBAttribs);
+    itemNounDef.dbAttribAdjuster.call(itemNounDef.datastore, aItem,
+      addDBAttribs, removeDBAttribs);
+    
+    // Cache ramifications...
+    if (aDoCache === undefined || aDoCache) {
+      if (aIsNew)
+        GlodaCollectionManager.itemsAdded(aItem.NOUN_ID, [aItem]);
+      else
+        GlodaCollectionManager.itemsModified(aOldItem.NOUN_ID, [aOldItem]);
+    }
+    
+    this._log.debug(" done grokking.");
+    
+    yield this.kWorkDone;
+  },
+};