pre-run status commit
authorAndrew Sutherland <asutherland@asutherland.org>
Fri, 04 Jul 2008 15:55:48 -0700
changeset 825 13338374ead4eee7fd78fbea58411e6c8ee191dc
parent 824 e67ed6c3f26fc4ba4f5a56f2d2331f41d477fd8a
child 826 59834a5d11d3aa7bc075158d7f8f8114cd43541b
push idunknown
push userunknown
push dateunknown
pre-run status commit
content/overlay.js
modules/datamodel.js
modules/datastore.js
modules/everybody.js
modules/explattr.js
modules/fundattr.js
modules/gloda.js
modules/indexer.js
modules/utils.js
--- a/content/overlay.js
+++ b/content/overlay.js
@@ -30,17 +30,20 @@
  * 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/gloda.js");
+// make all the built-in plugins join the party
+Components.utils.import("resource://gloda/modules/everybody.js");
 
 Components.utils.import("resource://gloda/modules/datastore.js");
 Components.utils.import("resource://gloda/modules/indexer.js");
 
 var gloda = {
   onLoad: function() {
     // initialization code
     this.initialized = true;
--- a/modules/datamodel.js
+++ b/modules/datamodel.js
@@ -1,16 +1,16 @@
 /* ***** 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
@@ -27,112 +27,193 @@
  * 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 = ["GlodaConversation", "GlodaMessage"];
 
 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");
 
-function GlodaConversation (aDatastore, aID, aSubject, aOldestMessageDate,
-                            aNewestMessageDate) {
+function GlodaAttributeDef(aDatastore, aID, aCompoundName, aProvider, aAttrType,
+                           aPluginName, aAttrName, aSubjectType, aObjectType,
+                           aParameterType, aExplanationFormat) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._compoundName = aCompoundName;
+  this._provider = aProvider;
+  this._attrType = aAttrType;
+  this._pluginName = aPluginName;
+  this._attrName = aAttrName;
+  this._subjectType = aSubjectType;
+  this._objectType = aObjectType;
+  this._parameterType = aParameterType;
+  this._explanationFormat = aExplanationFormat;
+  
+  /** Map parameter values to the underlying database id. */
+  this._parameterBindings = {};
+}
+
+GlodaAttributeDef.prototype = {
+  get id() { return this._id; },
+  get provider() { return this._provider; },
+
+  /**
+   * Bind a parameter value to the attribute definition, allowing use of the
+   *  attribute-parameter as an attribute.
+   *
+   * @return 
+   */
+  bindParameter: function gloda_attr_bindParameter(aValue) {
+    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;
+    return id;
+  },  
+};
+
+function GlodaConversation(aDatastore, aID, aSubject, aOldestMessageDate,
+                           aNewestMessageDate) {
   this._datastore = aDatastore;
   this._id = aID;
   this._subject = aSubject;
   this._oldestMessageDate = aOldestMessageDate;
   this._newestMessageDate = aNewestMessageDate;
-  
+
   this._messages = null;
 }
 
 GlodaConversation.prototype = {
   get id() { return this._id; },
   get subject() { return this._subject; },
   get oldestMessageDate() { return this._oldestMessageDate; },
   get newestMessageDate() { return this._newestMessageDate; },
-  
+
   get messages() {
     if (this._messages == null) {
       this._messages = this._datastore.getMessagesByConversationID(this._id,
                                                                    false);
     }
     return this._messages;
   }
 };
 
 
-function GlodaMessage (aDatastore, aID, aFolderID, aFolderURI, aMessageKey,
-                       aConversationID, aConversation, aParentID,
-                       aHeaderMessageID, aBodySnippet) {
+function GlodaMessage(aDatastore, aID, aFolderID, aFolderURI, aMessageKey,
+                      aConversationID, aConversation, aParentID,
+                      aHeaderMessageID, aBodySnippet) {
   this._datastore = aDatastore;
   this._id = aID;
   this._folderID = aFolderID;
   this._folderURI = aFolderURI;
   this._messageKey = aMessageKey;
   this._conversationID = aConversationID;
   this._conversation = aConversation;
   this._parentID = aParentID;
   this._headerMessageID = aHeaderMessageID;
   this._bodySnippet = aBodySnippet;
-  
+
   // for now, let's always cache this; they should really be forgetting about us
   //  if they want to forget about the underlying storage anyways...
   this._folderMessage = null;
 }
 
 GlodaMessage.prototype = {
   get id() { return this._id; },
   get folderID() { return this._folderID; },
   get messageKey() { return this._messageKey; },
   get conversationID() { return this._conversationID; },
   // conversation is special
   get parentID() { return this._parentID; },
   get headerMessageID() { return this._headerMessageID; },
   get bodySnippet() { return this._bodySnippet; },
-  
+
   get conversation() {
     if (this._conversation == null) {
       this._conversation = this._datastore.getConversationByID(
         this._conversationID);
     }
     return this._conversation;
   },
-  
+
   set messageKey(aMessageKey) { this._messageKey = aMessageKey; },
   set folderURI(aFolderURI) {
     this._folderID = this._datastore._mapFolderURI(aFolderURI);
   },
-  
+
   /**
    * Return the underlying nsIMsgDBHdr from the folder storage for this, or
    *  null if the message does not exist for one reason or another.
    */
   get folderMessage() {
     if (this._folderMessage != null)
       return this._folderMessage;
     if (this._folderURI == null || this._messageKey == null)
       return null;
-  
+
     let rdfService = Cc['@mozilla.org/rdf/rdf-service;1'].
                      getService(Ci.nsIRDFService);
     let folder = rdfService.GetResource(this._folderURI);
     if (folder instanceof Ci.nsIMsgFolder) {
       this._folderMessage = folder.GetMessageHeader(this._messageKey);
       return this._folderMessage;
     }
-    
+
     throw "Unable to locate folder message for: " + this._folderURI + ":" +
           this._messageKey;
   },
-};
\ No newline at end of file
+  
+  clearAttributes: function gloda_attr_clearAttributes() {
+    this._datastore.clearMessageAttributes(this);
+  },
+};
+
+function GlodaContact(aDatastore, aID, aDirectoryUUID, aContactUUID, aName) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._directoryUUID = aDirectoryUUID;
+  this._contactUUID = aContactUUID;
+  this._name = aName;
+}
+
+GlodaContact.prototype = {
+  get id() { return this._id; },
+  get directoryUUID() { return this._directoryUUID; },
+  get contactUUID() { return this._contactUUID; },
+  get name() { return this._name },
+};
+
+function GlodaIdentity(aDatastore, aID, aContactID, aContact, aKind, aValue) {
+  this._datastore = aDatastore;
+  this._id = aID;
+  this._contactID = aContactID;
+  this._contact = aContact;
+  this._kind = aKind;
+  this._value = aValue;
+}
+
+GlodaIdentity.prototype = {
+  get kind() { return this._kind; },
+  get value() { return this._value; },
+  
+  get contact() {
+    if (this._contact == null) {
+      this._contact = this._datastore.getContactByID(this._contactID);
+    }
+    return this._contact;
+  },
+};
--- a/modules/datastore.js
+++ b/modules/datastore.js
@@ -49,17 +49,19 @@ const Cu = Components.utils;
 
 Cu.import("resource://gloda/modules/log4moz.js");
 
 Cu.import("resource://gloda/modules/datamodel.js");
 
 let GlodaDatastore = {
   _log: null,
 
-  _schemaVersion: 1,
+  /* ******************* SCHEMA ******************* */
+
+  _schemaVersion: 2,
   _schema: {
     tables: {
       
       // ----- Messages
       folderLocations: {
         columns: [
           "id INTEGER PRIMARY KEY",
           "folderURI TEXT",
@@ -106,17 +108,17 @@ let GlodaDatastore = {
           conversationID: ['conversationID'],
         },
       },
       
       // ----- Attributes
       attributeDefinitions: {
         columns: [
           "id INTEGER PRIMARY KEY",
-          "attributeType TEXT",
+          "attributeType INTEGER",
           "extensionName TEXT",
           "name TEXT",
           "parameter BLOB",
         ],
       },
       
       messageAttributes: {
         columns: [
@@ -133,20 +135,61 @@ let GlodaDatastore = {
           messageAttribFetch: [
             "messageID",
             /* covering: */ "conversationID", "messageID", "value"],
           conversationAttribFetch: [
             "conversationID",
             /* covering: */ "messageID", "attributeID", "value"],
         },
       },
+    
+      // ----- 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",
+          "name TEXT"
+        ]
+      },
+      
+      /**
+       * 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",
+          "value TEXT",
+          "description TEXT"
+        ],
+        
+        indices: {
+          contactQuery: ["contactID"],
+          valueQuery: ["kind", "value"]
+        }
+      },
+      
+      //identityAttributes: {
+      //},
+    
     },
   },
+
+  /* ******************* LOGIC ******************* */
   
-  _init: function glodaDBInit() {
+  _init: function gloda_ds_init() {
     this._log = Log4Moz.Service.getLogger("gloda.datastore");
   
     // 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");
     
@@ -158,51 +201,48 @@ let GlodaDatastore = {
     
     // Create the file if it does not exist
     if (!dbFile.exists()) {
       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 {
+      { // try {
         dbConnection = dbService.openDatabase(dbFile);
       
         if (dbConnection.schemaVersion != this._schemaVersion) {
           this._migrate(dbConnection,
                         dbConnection.schemaVersion, this._schemaVersion);
         }
       }
       // Handle corrupt databases, other oddities
-      catch (ex) {
-        // TODO: handle them in the future.  let's die for now.
-        throw ex;
-      }
+      // ... in the future. for now, let us die
     }
     
     this.dbConnection = dbConnection;
   },
   
-  _createDB: function glodaDBCreateDB(aDBService, aDBFile) {
+  _createDB: function gloda_ds_createDB(aDBService, aDBFile) {
     var dbConnection = aDBService.openDatabase(aDBFile);
     
     dbConnection.beginTransaction();
     try {
       this._createSchema(dbConnection);
       dbConnection.commitTransaction();
     }
     catch(ex) {
       dbConnection.rollbackTransaction();
       throw ex;
     }
     
     return dbConnection;
   },
   
-  _createSchema: function glodaDBCreateSchema(aDBConnection) {
+  _createSchema: function gloda_ds_createSchema(aDBConnection) {
     // -- For each table...
     for (let tableName in this._schema.tables) {
       let table = this._schema.tables[tableName];
       
       // - Create the table
       aDBConnection.createTable(tableName, table.columns.join(", "));
       
       // - Create its indices
@@ -213,37 +253,116 @@ let GlodaDatastore = {
           "CREATE INDEX " + indexName + " ON " + tableName +
           "(" + indexColumns.join(", ") + ")"); 
       }
     }
     
     aDBConnection.schemaVersion = this._schemaVersion;  
   },
   
-  _migrate: function glodaDBMigrate(aDBConnection, aCurVersion, aNewVersion) {
+  _migrate: function gloda_ds_migrate(aDBConnection, aCurVersion, aNewVersion) {
+    throw new Error("We currently aren't clever enough to migrate. " +
+                    "Delete your DB");
   },
   
   // cribbed from snowl
-  _createStatement: function glodaDBCreateStatement(aSQLString) {
+  _createStatement: function gloda_ds_createStatement(aSQLString) {
     let statement = null;
     try {
       statement = this.dbConnection.createStatement(aSQLString);
     }
     catch(ex) {
        throw("error creating statement " + aSQLString + " - " +
              this.dbConnection.lastError + ": " +
              this.dbConnection.lastErrorString + " - " + ex);
     }
     
     let wrappedStatement = Cc["@mozilla.org/storage/statement-wrapper;1"].
                            createInstance(Ci.mozIStorageStatementWrapper);
     wrappedStatement.initialize(statement);
     return wrappedStatement;
   },
   
+  /* ********** Attribute Definitions ********** */
+  get _insertAttributeDefStatement() {
+    let statement = this._createStatement(
+      "INSERT INTO attributeDefinitions (attributeType, extensionName, name, \
+                                  parameter) \
+              VALUES (:attributeType, :extensionName, :name, :parameter)");
+    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 GlodaAttributeDef; we leave that up
+   *  to the caller since they know much more than actually needs to go in the
+   *  database.
+   */
+  _createAttributeDef: function gloda_ds_createAttributeDef(aAttrType,
+                                    aExtensionName, aAttrName, aParameter) {
+    let iads = this._insertAttributeDefStatement;
+    iads.params.attributeType = aAttrType;
+    iads.params.extensionName = aExtensionName;
+    iads.params.name = aAttrName;
+    iads.params.parameter = aParamater;
+    
+    iads.execute();
+    
+    return this.dbConnection.lastInsertRowID;
+  },
+  
+  get _selectAttributeDefinitionsStatement() {
+    let statement = this._createStatement(
+      "SELECT * FROM attributeDefinitions");
+    this.__defineGetter__("_selectAttributeDefinitionsStatement",
+      function() statement);
+    return this._selectAttributeDefinitionsStatement;
+  },
+  
+  /**
+   * Look-up all the attribute definitions 
+   */
+  getAllAttributes: function gloda_ds_getAllAttributes() {
+    let attribs = {};
+
+    this._log.info("loading all attribute defs");
+    
+    while (this._selectAttributeDefinitionsStatement.step()) {
+      let row = this._selectAttributeDefinitionsStatement.row;
+      
+      let compoundName = row["extensionName"] + ":" + row["name"];
+      
+      let attrib;
+      if (compoundName in attribs) {
+        attrib = attribs[compoundName];
+      } else {
+        attrib = new GlodaAttributeDef(this, null,
+                                       compoundName, null, row["attributeType"],
+                                       row["extensionName"], row["name"],
+                                       null, null, null, null);
+        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 (row["parameter"] == null) {
+        attrib._id = row["id"]; 
+      } else {
+        attrib._parameterBindings[row["parameter"]] = row["id"];
+      }
+    }
+    this._selectAttributeDefinitionsStatement.reset();
+
+    this._log.info("done loading all attribute defs");
+    
+    return attribs;
+  },
+  
+  /* ********** Folders ********** */
+  
   get _insertFolderLocationStatement() {
     let statement = this._createStatement(
       "INSERT INTO folderLocations (folderURI) VALUES (:folderURI)");
     this.__defineGetter__("_insertFolderLocationStatement",
       function() statement);
     return this._insertFolderLocationStatement;
   },
   
@@ -262,17 +381,17 @@ let GlodaDatastore = {
     this.__defineGetter__("_selectAllFolderLocations",
       function() statement);
     return this._selectAllFolderLocations;
   },
   
   _folderURIs: {},
   _folderIDs: {},
   
-  _mapFolderURI: function glodaDBMapFolderURI(aFolderURI) {
+  _mapFolderURI: function gloda_ds_mapFolderURI(aFolderURI) {
     if (aFolderURI in this._folderURIs) {
       return this._folderURIs[aFolderURI];
     }
     
     var folderID;
     this._selectFolderLocationByURIStatement.params.folderURI = aFolderURI;
     if (this._selectFolderLocationByURIStatement.step()) {
       folderID = this._selectFolderLocationByURIStatement.row["id"];
@@ -289,17 +408,17 @@ let GlodaDatastore = {
     this._log.info("mapping URI " + aFolderURI + " to " + folderID);
     return folderID;
   },
   
   // perhaps a better approach is to just have the _folderIDs load everything
   //  from the database the first time it is accessed, and then rely on
   //  invariant maintenance to ensure that its state keeps up-to-date with the
   //  actual database.
-  _mapFolderID: function glodaDBMapFolderID(aFolderID) {
+  _mapFolderID: function gloda_ds_mapFolderID(aFolderID) {
     if (aFolderID == null)
       return null;
     if (aFolderID in this._folderIDs)
       return this._folderIDs[aFolderID];
     
     while (this._selectAllFolderLocations.step()) {
       let folderID = this._selectAllFolderLocations.row["id"];
       let folderURI = this._selectAllFolderLocations.row["folderURI"];
@@ -309,27 +428,28 @@ let GlodaDatastore = {
     }
     this._selectAllFolderLocations.reset();
     
     if (aFolderID in this._folderIDs)
       return this._folderIDs[aFolderID];
     throw "Got impossible folder ID: " + aFolderID;
   },
   
-  // memoizing message statement creation
+  /* ********** Conversation ********** */
   get _insertConversationStatement() {
     let statement = this._createStatement(
       "INSERT INTO conversations (subject, oldestMessageDate, \
                                   newestMessageDate) \
               VALUES (:subject, :oldestMessageDate, :newestMessageDate)");
     this.__defineGetter__("_insertConversationStatement", function() statement);
     return this._insertConversationStatement; 
   }, 
   
-  createConversation: function glodaDBCreateConversation(aSubject,
+  /** Create a conversation. */
+  createConversation: function gloda_ds_createConversation(aSubject,
         aOldestMessageDate, aNewestMessageDate) {
     
     let ics = this._insertConversationStatement;
     ics.params.subject = aSubject;
     ics.params.oldestMessageDate = aOldestMessageDate;
     ics.params.newestMessageDate = aNewestMessageDate;
         
     ics.execute();
@@ -341,43 +461,45 @@ let GlodaDatastore = {
 
   get _selectConversationByIDStatement() {
     let statement = this._createStatement(
       "SELECT * FROM conversations WHERE id = :conversationID");
     this.__defineGetter__("_selectConversationByIDStatement", function() statement);
     return this._selectConversationByIDStatement; 
   }, 
 
-  getConversationByID: function glodaDBGetConversationByID(aConversationID) {
+  getConversationByID: function gloda_ds_getConversationByID(aConversationID) {
     this._selectConversationByIDStatement.params.conversationID =
       aConversationID;
     
     let conversation = null;
     if (this._selectConversationByIDStatement.step()) {
       let row = this._selectConversationByIDStatement.row;
       conversation = new GlodaConversation(this, aConversationID,
         row["subject"], row["oldestMessageDate"], row["newestMessageDate"]);
     }
     this._selectConversationByIDStatement.reset();
     
     return conversation;
   },
   
+  /* ********** Message ********** */
+  
   // memoizing message statement creation
   get _insertMessageStatement() {
     let statement = this._createStatement(
       "INSERT INTO messages (folderID, messageKey, conversationID, parentID, \
                              headerMessageID, bodySnippet) \
               VALUES (:folderID, :messageKey, :conversationID, :parentID, \
                       :headerMessageID, :bodySnippet)");
     this.__defineGetter__("_insertMessageStatement", function() statement);
     return this._insertMessageStatement; 
   }, 
   
-  createMessage: function glodaDBCreateMessage(aFolderURI, aMessageKey,
+  createMessage: function gloda_ds_createMessage(aFolderURI, aMessageKey,
                               aConversationID, aParentID, aHeaderMessageID,
                               aBodySnippet) {
     let folderID;
     if (aFolderURI != null) {
       folderID = this._mapFolderURI(aFolderURI);
     }
     else {
       folderID = null;
@@ -415,30 +537,30 @@ let GlodaDatastore = {
                            parentID = :parentID, \
                            headerMessageID = :headerMessageID, \
                            bodySnippet = :bodySnippet \
               WHERE id = :id");
     this.__defineGetter__("_updateMessageStatement", function() statement);
     return this._updateMessageStatement; 
   }, 
   
-  updateMessage: function glodaDBUpdateMessage(aMessage) {
+  updateMessage: function gloda_ds_updateMessage(aMessage) {
     let ums = this._updateMessageStatement;
     ums.params.id = aMessage.id;
     ums.params.folderID = aMessage.folderID;
     ums.params.messageKey = aMessage.messageKey;
     ums.params.conversationID = aMessage.conversationID;
     ums.params.parentID = aMessage.parentID;
     ums.params.headerMessageID = aMessage.headerMessageID;
     ums.params.bodySnippet = aMessage.bodySnippet;
     
     ums.execute();
   },
   
-  _messageFromRow: function glodaDBMessageFromRow(aRow) {
+  _messageFromRow: function gloda_ds_messageFromRow(aRow) {
     return new GlodaMessage(this, aRow["id"], aRow["folderID"],
                             this._mapFolderID(aRow["folderID"]),
                             aRow["messageKey"],
                             aRow["conversationID"], null,
                             aRow["parentID"],
                             aRow["headerMessageID"], aRow["bodySnippet"]);
   },
 
@@ -446,17 +568,17 @@ let GlodaDatastore = {
     let statement = this._createStatement(
       "SELECT * FROM messages WHERE folderID = :folderID AND \
                                     messageKey = :messageKey");
     this.__defineGetter__("_selectMessageByLocationStatement",
       function() statement);
     return this._selectMessageByLocationStatement;
   },
 
-  getMessageFromLocation: function glodaDBGetMessageFromLocation(aFolderURI,
+  getMessageFromLocation: function gloda_ds_getMessageFromLocation(aFolderURI,
                                                                  aMessageKey) {
     this._selectMessageByLocationStatement.params.folderID =
       this._mapFolderURI(aFolderURI);
     this._selectMessageByLocationStatement.params.messageKey = aMessageKey;
     
     let message = null;
     if (this._selectMessageByLocationStatement.step())
       message = this._messageFromRow(this._selectMessageByLocationStatement.row);
@@ -464,17 +586,17 @@ let GlodaDatastore = {
     
     if (message == null)
       this._log.error("Error locating message with key=" + aMessageKey +
                       " and URI " + aFolderURI);
     
     return message;
   },
   
-  getMessagesByMessageID: function glodaDBGetMessagesByMessageID(aMessageIDs) {
+  getMessagesByMessageID: function gloda_ds_getMessagesByMessageID(aMessageIDs) {
     let msgIDToIndex = {};
     let results = [];
     for (let iID=0; iID < aMessageIDs.length; ++iID) {
       let msgID = aMessageIDs[iID];
       results.push(null);
       msgIDToIndex[msgID] = iID;
     } 
 
@@ -492,17 +614,17 @@ let GlodaDatastore = {
         this._messageFromRow(statement.row);
     }
     statement.reset();
     
     return results;
   },
   
   // could probably do with an optimized version of this...
-  getMessageByMessageID: function glodaDBGetMessageByMessageID(aMessageID) {
+  getMessageByMessageID: function gloda_ds_getMessageByMessageID(aMessageID) {
     var ids = [aMessageID];
     var messages = this.getMessagesByMessageID(ids);
     return messages.pop();
   },
 
   get _selectMessagesByConversationIDStatement() {
     let statement = this._createStatement(
       "SELECT * FROM messages WHERE conversationID = :conversationID");
@@ -515,26 +637,172 @@ let GlodaDatastore = {
     let statement = this._createStatement(
       "SELECT * FROM messages WHERE conversationID = :conversationID AND \
                                     folderID IS NOT NULL");
     this.__defineGetter__("_selectMessagesByConversationIDNoGhostsStatement",
       function() statement);
     return this._selectMessagesByConversationIDNoGhostsStatement;
   },
 
-  getMessagesByConversationID: function glodaDBGetMessagesByConversationID(
+  getMessagesByConversationID: function gloda_ds_getMessagesByConversationID(
         aConversationID, aIncludeGhosts) {
     let statement;
     if (aIncludeGhosts)
       statement = this._selectMessagesByConversationIDStatement;
     else
       statement = this._selectMessagesByConversationIDNoGhostsStatement;
     statement.params.conversationID = aConversationID; 
     
     let messages = [];
     while (statement.step()) {
       messages.push(this._messageFromRow(statement.row));
     }
     statement.reset();
     
     return messages;
   },
+  
+  /* ********** Message Attributes ********** */
+  get _insertMessageAttributeStatement() {
+    let statement = this._createStatement(
+      "INSERT INTO messageAttributes (conversationID, messageID, attributeID, \
+                             value) \
+              VALUES (:conversationID, :messageID, :attributeID, :value)");
+    this.__defineGetter__("_insertMessageAttributeStatement",
+      function() statement);
+    return this._insertMessageAttributeStatement;
+  },
+  
+  insertMessageAttributes: function gloda_ds_insertMessageAttributes(aMessage,
+                                        aAttributes) {
+    let imas = this._insertMessageAttributeStatement;
+    this.dbConnection.beginTransaction();
+    try {
+      for (let iAttribute=0; iAttribute < aAttributes.length; iAttribute++) {
+        let attribValueTuple = aAttributes[iAttribute];
+        imas.params.conversationID = aMessage.conversationID;
+        imas.params.messageID = aMessage.id;
+        imas.params.attributeID = attribValueTuple[0];
+        imas.params.value = attribValueTuple[1];
+        imas.execute();
+        imas.reset();
+      }
+      
+      this.dbConnection.commitTransaction();
+    }
+    catch (ex) {
+      this.dbConnection.rollbackTransaction();
+      throw ex;
+    }
+  },
+  
+  get _deleteMessageAttributesByMessageIDStatement() {
+    let statement = this._createStatement(
+      "DELETE FROM messageAttributes WHERE messageID = :messageID");
+    this.__defineGetter__("_deleteMessageAttributesByMessageIDStatement",
+      function() statement);
+    return this._deleteMessageAttributesByMessageIDStatement;
+  },
+
+  clearMessageAttributes: function gloda_ds_clearMessageAttributes(aMessage) {
+    if (aMessage.id != null) {
+      this._deleteMessageAttributesByMessageIDStatement.params.messageID =
+        aMessage.id;
+      this._deleteMessageAttributesByMessageIDStatement.execute();
+    }
+  },
+  
+  /* ********** Contact ********** */
+  get _insertContactStatement() {
+    let statement = this._createStatement(
+      "INSERT INTO contacts (directoryUUID, contactUUID, name) \
+              VALUES (:directoryUUID, :contactUUID, :name)");
+    this.__defineGetter__("_insertContactStatement", function() statement);
+    return this._insertContactStatement; 
+  },
+  
+  createContact: function gloda_ds_createContact(aDirectoryUUID, aContactUUID,
+                                                 aName) {
+    let ics = this._insertContactStatement;
+    ics.params.directoryUUID = aDirectoryUUID;
+    ics.params.contactUUID = aContactUUID;
+    ics.params.name = aName;
+    
+    ics.execute();
+    
+    return new GlodaContact(this.this.dbConnection.lastInsertRowID,
+                            aDirectoryUUID, aContactUUID, aName);
+  },
+  
+  _contactFromRow: function gloda_ds_contactFromRow(aRow) {
+    return new GlodaContact(this, aRow["id"], );
+  },
+  
+  get _selectContactByIDStatement() {
+    let statement = this._createStatement(
+      "SELECT * FROM contacts WHERE id = :id");
+    this.__defineGetter__("_selectContactByIDStatement",
+      function() statement);
+    return this._selectContactByIDStatement;
+  },
+
+  getContactByID: function gloda_ds_getContactByID(aContactID) {
+    let contact = null;
+  
+    let scbi = this._selectContactByIDStatement;
+    scbi.params.id = aContactID;
+    if (scbi.step()) {
+      contact = this._contactFromRow(scbi.row);
+    }
+    scbi.reset();
+    
+    return scbi;
+  },
+  
+  /* ********** Identity ********** */
+  get _insertIdentityStatement() {
+    let statement = this._createStatement(
+      "INSERT INTO identities (contactID, kind, value, description) \
+              VALUES (:contactID, :kind, :value, :description)");
+    this.__defineGetter__("_insertIdentityStatement", function() statement);
+    return this._insertIdentityStatement; 
+  },
+  
+  createIdentity: function gloda_ds_createIdentity(aContactID, aContact, aKind,
+                                                   aValue, aDescription) {
+    let iis = this._insertIdentityStatement;
+    iis.params.contactID = aContactID;
+    iis.params.kind = aKind;
+    iis.params.value = aValue;
+    iis.params.description = aDescription;
+  
+    return new GlodaIdentity(this, this.dbConnection.lastInsertRowID,
+                             aContactID, aContact, aKind, aValue, aDescription);
+  },
+  
+  _identityFromRow: function gloda_ds_identityFromRow(aRow) {
+    return new GlodaIdentity(this, aRow["id"], aRow["contactID"], null,
+                             aRow["kind"], aRow["value"]);
+  },
+  
+  get _selectIdentityByKindValueStatement() {
+    let statement = this._createStatement(
+      "SELECT * FROM identities WHERE kind = :kind AND value = :value");
+    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 = null;
+    
+    let ibkv = this._selectIdentityByKindValueStatement;
+    ibkv.params.kind = aKind;
+    ibkv.params.value = aValue;
+    if (ibkv.step()) {
+      identity = this._identityFromRow(ibkv.row);
+    }
+    ibkv.reset();
+    
+    return identity;
+  },
 };
new file mode 100644
--- /dev/null
+++ b/modules/everybody.js
@@ -0,0 +1,40 @@
+/* ***** 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 ***** */
+ 
+ Cu.import("resource://gloda/modules/fundattr.js");
+ Cu.import("resource://gloda/modules/explattr.js");
+ 
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/modules/explattr.js
@@ -0,0 +1,100 @@
+/* ***** 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 = ['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");
+
+const EXT_NAME = "built-in";
+const FA_FROM = "FROM";
+const FA_TO = "TO";
+const FA_CC = "CC";
+const FA_DATE = "DATE";
+
+/**
+ * The Gloda Fundamental Attribute provider is a special-case 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.
+ */
+let GlodaExplicitAttr = {
+  _init: function gloda_explattr_init() {
+    this.defineAttributes();
+  },
+
+  _attrTag: null,
+  _attrStar: null,
+  _attrRead: null,
+  
+  defineAttributes: function() {
+    // Tag
+    this._attrTag = Gloda.defineAttr(this, Gloda.kAttrExplicit, EXT_BUILTIN,
+                        FA_FROM,
+                        Gloda.NOUN_MESSAGE, Gloda.NOUN_DATE, Gloda.NOUN_TAG,
+                        "%{subject} was tagged %{parameter} on %{object}");
+    // Star
+    this._attrStar = Gloda.defineAttr(this, Gloda.kAttrExplicit, EXT_BUILTIN,
+                        FA_TO,
+                        Gloda.NOUN_MESSAGE, Gloda.NOUN_BOOLEAN, null,
+                        "%{subject} has a star state of %{object}");
+    // Read/Unread
+    this._attrRead = Gloda.defineAttr(this, Gloda.ATTR_FUNDAMENTAL, EXT_BUILTIN,
+                        FA_CC,
+                        Gloda.NOUN_MESSAGE, Gloda.NOUN_BOOLEAN, null,
+                        "%{subject} has a read state of %{object}");
+    
+  },
+  
+  process: function Gloda_explattr_process(aGlodaMessage, aMsgHdr) {
+    let attribs = [];
+    
+    // -- Tag
+    let keywords = aMsgHdr.getStringProperty("keywords");
+    
+    return attribs;
+  },
+};
+GlodaExplicitAttr._init();
new file mode 100644
--- /dev/null
+++ b/modules/fundattr.js
@@ -0,0 +1,133 @@
+/* ***** 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");
+
+const EXT_BUILTIN = "built-in";
+const FA_FROM = "FROM";
+const FA_TO = "TO";
+const FA_CC = "CC";
+const FA_DATE = "DATE";
+
+/**
+ * The Gloda Fundamental Attribute provider is a special-case 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.
+ */
+let GlodaFundAttr = {
+  _init: function gloda_explattr_init() {
+    this.defineAttributes();
+  },
+
+  _attrFrom: null,
+  _attrTo: null,
+  _attrCc: null,
+  _attrDate: null,
+  
+  defineAttributes: function() {
+    // From
+    this._attrFrom = gloda.defineAttr(this, gloda.kAttrFundamental, EXT_BUILTIN,
+                        FA_FROM,
+                        gloda.NOUN_MESSAGE, gloda.NOUN_IDENTITY, null,
+                        "%{subject} was sent by %{object}");
+    // To
+    this._attrTo = gloda.defineAttr(this, gloda.kAttrFundamental, EXT_BUILTIN,
+                        FA_TO,
+                        gloda.NOUN_MESSAGE, gloda.NOUN_IDENTITY, null,
+                        "%{subject} was sent to %{object}");
+    // Cc
+    this._attrCc = gloda.defineAttr(this, gloda.kAttrFundamental, EXT_BUILTIN,
+                        FA_CC,
+                        gloda.NOUN_MESSAGE, gloda.NOUN_IDENTITY, null,
+                        "%{subject} was carbon-copied to %{object}");
+    // Date
+    this._attrDate = gloda.defineAttr(this, gloda.kAttrFundamental, EXT_BUILTIN,
+                        FA_DATE,
+                        gloda.NOUN_MESSAGE, gloda.NOUN_DATE, null,
+                        "%{subject} was sent on %{object}");
+    
+  },
+  
+  process: function gloda_fundattr_process(aGlodaMessage, aMsgHdr) {
+    let attribs = [];
+    
+    // -- From
+    // Let's use replyTo if available.
+    // TODO: deal with default charset issues
+    let author = null;
+    try {
+      author = aMsgHdr.getStringProperty("replyTo");
+    }
+    catch (ex) {
+      author = aMsgHdr.author;
+    }
+    let authorIdentity = Gloda.getIdentityForFullMailAddress(author);
+    attribs.push([this._attrFrom.id, authorIdentity.id]); 
+    
+    // -- To, Cc
+    // TODO: handle mailing list semantics (use my visterity logic as a first
+    //  pass.)
+    let toIdentities = Gloda.getIdentitiesForFullMailAddresses(
+                           aMsgHdr.recipients);
+    for (let iTo=0; iTo < toIdentities.length; iTo++) {
+      attribs.push([this._attrTo.id, toIdentities[iTo].id]);
+    }
+    let ccIdentities = Gloda.getIdentitiesForFullMailAddresses(aMsgHdr.ccList);
+    for (let iCc=0; iTo < ccIdentities.length; iCc++) {
+      attribs.push([this._attrCc.id, ccIdentities[iCc].id]);
+    }
+    
+    // -- Date
+    attribs.push([this._attrDate.id, aMsgHdr.date]);
+    
+    return attribs;
+  },
+};
+GlodaFundAttr._init();
--- a/modules/gloda.js
+++ b/modules/gloda.js
@@ -40,49 +40,217 @@ 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");
 
 let Gloda = {
-  _init: function glodaNSInit() {
+  _init: function gloda_ns_init() {
     this._initLogging();
+    GlodaDatastore._init();
+    this._initAttributes();
   },
   
   _log: null,
-  _initLogging: function glodaNSInitLogging() {
+  _initLogging: function gloda_ns_initLogging() {
     let formatter = new Log4Moz.BasicFormatter();
     let root = Log4Moz.Service.rootLogger;
     root.level = Log4Moz.Level.Debug;
 
     let capp = new Log4Moz.ConsoleAppender(formatter);
     capp.level = Log4Moz.Level.Warn;
     root.addAppender(capp);
 
     let dapp = new Log4Moz.DumpAppender(formatter);
     dapp.level = Log4Moz.Level.All;
     root.addAppender(dapp);
     
     this._log = Log4Moz.Service.getLogger("Gloda.NS");
     this._log.info("Logging Initialized");
   },
   
-  getMessageForHeader: function glodaNSGetMessageForHeader(aMsgHdr) {
+  getMessageForHeader: function gloda_ns_getMessageForHeader(aMsgHdr) {
     let message = GlodaDatastore.getMessageFromLocation(aMsgHdr.folder.URI,
                                                         aMsgHdr.messageKey);
     if (message == null) {
       message = GlodaDatastore.getMessageByMessageID(aMsgHdr.messageId);
       this._log.warn("Fell back to locating message by id; actual message " +
                      "key is: " + aMsgHdr.messageKey + " database key: " +
                      message.messageKey);
     }
     
     return message;
   },
   
+  /**
+   * Given a full mail address (ex: "Bob Smith" <bob@smith.com>), return the
+   *  identity that corresponds to that mail address, creating it if required.
+   */
+  getIdentitiesForFullMailAddresseses:
+      function gloda_ns_getIdentitiesForMailAddresses(aMailAddresses) {
+    let parsed = GlodaUtils.parseMailAddresses(aMailAddress);
+    
+    let identities = [];
+    for (let iAddress=0; iAddress < parsed.count; iAddress++) {
+      let identity = GlodaDatastore.getIdentity("email",
+                                                parsed.addresses[iAddress]);
+      
+      if (identity == null) {
+        // we must create a contact
+        let contact = GlodaDatastore.createContact(null, null,
+                                                   parsed.names[iAddress]);
+        
+        // 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).
+        identity = GlodaDatastore.createIdentity(contact.id, contact, "email",
+                                                 parsed.addresses[iAddress],
+                                                 "");
+      }
+      identities.push(identity);
+    }
+    
+    return identities;
+  },
+  
+  getIdentityForFullMailAddress:
+      function gloda_ns_getIdentityForFullMailAddress(aMailAddress) {
+    let identities = this.getIdentitiesForFullMailAddresseses(aMailAddress);
+    if (identities.length != 1)
+      throw Error("Expected exactly 1 address, got " + identities.length + ".");    
+    
+    return identities[0];
+  },
+  
+  kAttrFundamental: 0,
+  kAttrOptimization: 1,
+  kAttrDerived: 2,
+  kAttrExplicit: 3,
+  kAttrImplicit: 4,
+  
+  /** A date, encoded as a PRTime */
+  NOUN_DATE: 10,
+  NOUN_TAG: 50,
+  NOUN_CONVERSATION: 101,
+  NOUN_MESSAGE: 102,
+  NOUN_CONTACT: 103,
+  NOUN_IDENTITY: 104,
+  
+  /** Attribute providers in the sequence to process them. */
+  _attrProviderOrder: [],
+  /** Maps attribute providers to the list of attributes they provide */
+  _attrProviders: {},
+  /** Maps (attribute def) compound names to the GlodaAttributeDef objects. */
+  _attributes: {},
+  
+  _initAttributes: function gloda_ns_initAttributes() {
+    this._attributes = GlodaDatastore.getAllAttributes();
+  },
+  
+  /**
+   * @param aProvider
+   * @param aAttrType
+   * @param aPluginName
+   * @param aAttrName
+   * @param aSubjectType
+   * @param aObjectType
+   * @param aParameterType
+   */
+  defineAttr: function gloda_ns_defineAttr(aProvider, aAttrType,
+                                           aPluginName, aAttrName,
+                                           aSubjectType, aObjectType,
+                                           aParameterType,
+                                           aExplanationFormat) {
+    // provider tracking
+    if (!(aProvider in this._attrProviders)) {
+      this._attrProviderOrder.push(aProvider);
+      this._attrProviders[aProvider] = [];
+    } 
+    
+    let compoundName = aPluginName + ":" + aAttrName;
+    let attr = null;
+    if (compoundName in this._attributes) {
+      // the existence of the GlodaAttributeDef 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)
+      attr = this._attributes[compoundName];
+      if (attr.provider != null) {
+        return attr;
+      }
+      
+      // we are behind the abstraction veil and can set these things
+      attr._provider = aProvider;
+      attr._subjectType = aSubjectType;
+      attr._objectType = aObjectType;
+      attr._parameterType = aParameterType;
+      attr._explanationFormat = aExplanationFormat;
+      
+      this._attrProviders[aProvider].push(attr);
+      return attr; 
+    }
+    
+    // Being here means the attribute def does not exist in the database.
+    // Of course, we only want to create something in the database if the
+    //  parameter is forever un-bound (type is null).
+    let attrID = null;
+    if (aParameterType == null) {
+      attrID = GlodaDatastore.createAttributeDef(aAttrType, aPluginName,
+                                                 aAttrName, null);
+    }
+    
+    attr = new GlodaAttributeDef(GlodaDatastore, attrID, compoundName,
+                                 aProvider, aAttrType, aPluginName, aAttrName,
+                                 aSubjectType, aObjectType, aParameterType,
+                                 aExplanationFormat);
+    this._attributes[compoundName] = attr;
+    this._attrProviders[aProvider].push(attr);
+    return attr;
+  },
+  
+  processMessage: function gloda_ns_processMessage(aMessage, aMsgHdr) {
+    // For now, we are ridiculously lazy and simply nuke all existing attributes
+    //  before applying the new attributes.
+    aMessage.clearAttributes();
+    
+    let allAttribs = [];
+  
+    for(let i = 0; i < this._attributeProviderOrder.length; i++) {
+      let attribs = this._attributeProviderOrder[i].process(aMessage, aMsgHdr);
+      allAttribs = allAttribs.concat(attribs);
+    }
+    
+    let outAttribs = [];
+    
+    for(let iAttrib=0; iAttrib < attribs.length; iAttrib++) {
+      let attribDesc = attribs[iAttrib];
+      
+      // is it an (attributedef / attribute def id, value) tuple?
+      if (attribDesc.length == 2) {
+        // if it's already an attrib id, we can use the tuple outright
+        if (typeof attribDesc[0] == number)
+          outAttribs.push(attribDesc);
+        else
+          outAttribs.push([attribDesc[0].id, attribDesc[1]]);
+      }
+      // it must be an (attrib, parameter value, attrib value) tuple
+      else {
+        let attrib = attribDesc[0];
+        let parameterValue = attribDesc[1];
+        let attribID;
+        if (parameterValue != null)
+          attribID = attrib.bindParameter(parameterValue);
+        else
+          attribID = attrib.id;
+        outAttribs.push([attribID, attribDesc[2]);
+      }
+    }
+    
+    GlodaDatastore.insertMessageAttributes(aMessage, outAttribs);
+  },
 };
 
 Gloda._init();
-GlodaDatastore._init();
\ No newline at end of file
--- a/modules/indexer.js
+++ b/modules/indexer.js
@@ -39,17 +39,19 @@ EXPORTED_SYMBOLS = ['GlodaIndexer'];
 
 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/datastore.js");
+Cu.import("resource://gloda/modules/gloda.js");
 
 function range(begin, end) {
   for (let i = begin; i < end; ++i) {
     yield i;
   }
 }
 
 // FROM STEEL
@@ -152,42 +154,32 @@ let GlodaIndexer = {
       this._log.info("      >>> Indexing Message: " + msgHdr.messageId +
                      " (" + msgHdr.subject + ")");
       this.indexMessage(msgHdr);
       this._log.info("      <<< Indexing Message: " + msgHdr.messageId);
     }
     this._log.info("   <<< Indexing Folder: " + aFolder.prettiestName);
   },
   
-  _mimeConverter: null,
-  _deMime: function glodaIndexDeMime(aString) {
-    if (this._mimeConverter == null) {
-      this._mimeConverter = Cc["@mozilla.org/messenger/mimeconverter;1"].
-                            getService(Ci.nsIMimeConverter);
-    }
-    
-    return this._mimeConverter.decodeMimeHeader(aString, null, false, true);
-  },
-  
   /**
    * Attempt to extract the original subject from a message.  For replies, this
    *  means either taking off the 're[#]:' (or variant, including other language
    *  variants), or in a Microsoft specific-ism, from the Thread-Topic header.
    *
    * Ideally, we would just be able to call NS_MsgStripRE to do the bulk of the
    *  work for us, especially since the subject may be encoded.
    */
   _extractOriginalSubject: function glodaIndexExtractOriginalSubject(aMsgHdr) {
     // mailnews.localizedRe contains a comma-delimited list of alternate
     //  prefixes.
     // NS_MsgStripRE does this, and bug 139317 proposes adding this to
     //  nsIMimeConverter
     
     // HACK FIXME: for now, we just return the subject without any processing 
-    return this._deMime(aMsgHdr.subject);
+    return aMsgHdr.mime2DecodedSubject;
   },
   
   indexMessage: function glodaIndexMessage(aMsgHdr) {
   
     // -- Find/create the conversation the message belongs to.
     // Our invariant is that all messages that exist in the database belong to
     //  a conversation.
     
@@ -280,10 +272,12 @@ let GlodaIndexer = {
                                              aMsgHdr.messageId,
                                              null); // no snippet
      }
      else {
         curMsg.folderURI = aMsgHdr.folder.URI;
         curMsg.messageKey = aMsgHdr.messageKey;
         this._datastore.updateMessage(curMsg);
      }
+     
+     Gloda.processMessage(curMsg, aMsgHdr);
   },
 };
new file mode 100644
--- /dev/null
+++ b/modules/utils.js
@@ -0,0 +1,81 @@
+/* ***** 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 = ['GlodaUtils'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+
+let GlodaUtils = {
+  _mimeConverter: null,
+  deMime: function gloda_utils_deMime(aString) {
+    if (this._mimeConverter == null) {
+      this._mimeConverter = Cc["@mozilla.org/messenger/mimeconverter;1"].
+                            getService(Ci.nsIMimeConverter);
+    }
+    
+    return this._mimeConverter.decodeMimeHeader(aString, null, false, true);
+  },
+  
+  _headerParser: null,
+  
+  /**
+   * Parses an RFC 2822 list of e-mail addresses and returns an object with
+   *  4 attributes: count, the number of addresses parsed; addresses, a list of
+   *  e-mail addresses (ex: bob@company.com); names, list (ex: Bob Smith); and
+   *  fullAddresses, aka the list of name and e-mail together (ex: "Bob Smith"
+   *  <bob@company.com>). 
+   */
+  parseMailAddresses: function gloda_utils_parseMailAddresses(aMailAddresses) {
+    if (this._headerParser == null) {
+      this._headerParser = Cc["@mozilla.org/messenger/headerparser;1"].
+                           getService(Ci.nsIMsgHeaderParser);
+    }
+    let addresses = {}, names = {}, fullAddresses = {};
+    this._headerParser.parseHeadersWithArray(aMailAddresses, addresses,
+                                             names, fullAddresses);
+    return {names: names.value, addresses: addresses.value,
+            fullAddresses: fullAddresses.value,
+            count: names.value.length}; 
+  },
+  
+  extractBodySnippet: function gloda_utils_extractBodySnippet(aBodyString) {
+  },
+};