summaryrefslogtreecommitdiffstats
path: root/mailnews/db/gloda/modules/datamodel.js
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/db/gloda/modules/datamodel.js')
-rw-r--r--mailnews/db/gloda/modules/datamodel.js907
1 files changed, 907 insertions, 0 deletions
diff --git a/mailnews/db/gloda/modules/datamodel.js b/mailnews/db/gloda/modules/datamodel.js
new file mode 100644
index 000000000..1bdd6d01d
--- /dev/null
+++ b/mailnews/db/gloda/modules/datamodel.js
@@ -0,0 +1,907 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = ["GlodaAttributeDBDef", "GlodaAccount",
+ "GlodaConversation", "GlodaFolder", "GlodaMessage",
+ "GlodaContact", "GlodaIdentity", "GlodaAttachment"];
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource:///modules/mailServices.js");
+
+Cu.import("resource:///modules/gloda/log4moz.js");
+var LOG = Log4Moz.repository.getLogger("gloda.datamodel");
+
+Cu.import("resource:///modules/gloda/utils.js");
+
+// Make it lazy.
+var gMessenger;
+function getMessenger () {
+ if (!gMessenger)
+ gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+ return gMessenger;
+}
+
+/**
+ * @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) {
+ // _datastore is now set on the prototype by GlodaDatastore
+ 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 = {
+ // set by GlodaDatastore
+ _datastore: null,
+ 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, 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.
+ *
+ * @param {Array} aInstanceValues An array of instance values regardless of
+ * whether or not the attribute is singular.
+ */
+ convertValuesToDBAttributes:
+ function gloda_attr_convertValuesToDBAttributes(aInstanceValues) {
+ let nounDef = this.attrDef.objectNounDef;
+ let dbAttributes = [];
+ if (nounDef.usesParameter) {
+ for (let instanceValue of aInstanceValues) {
+ let [param, dbValue] = nounDef.toParamAndValue(instanceValue);
+ dbAttributes.push([this.bindParameter(param), dbValue]);
+ }
+ }
+ else {
+ // Not generating any attributes is ok. This basically means the noun is
+ // just an informative property on the Gloda Message and has no real
+ // indexing purposes.
+ if ("toParamAndValue" in nounDef) {
+ for (let instanceValue of aInstanceValues) {
+ dbAttributes.push([this._id,
+ nounDef.toParamAndValue(instanceValue)[1]]);
+ }
+ }
+ }
+ return dbAttributes;
+ },
+
+ toString: function() {
+ return this._compoundName;
+ }
+};
+
+var GlodaHasAttributesMixIn = {
+ enumerateAttributes: function* gloda_attrix_enumerateAttributes() {
+ let nounDef = this.NOUN_DEF;
+ for (let key in this) {
+ let value = this[key];
+ 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 (let attrName in nounDef.domExposeAttribsByBoundName) {
+ let attr = nounDef.domExposeAttribsByBoundName[attrName];
+ 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.startsWith("get_"))
+ proto.__defineGetter__(name.substring(4), func);
+ else
+ proto[name] = func;
+ }
+}
+
+/**
+ * @class A gloda wrapper around nsIMsgIncomingServer.
+ */
+function GlodaAccount(aIncomingServer) {
+ this._incomingServer = aIncomingServer;
+}
+
+GlodaAccount.prototype = {
+ NOUN_ID: 106,
+ get id() { return this._incomingServer.key; },
+ get name() { return this._incomingServer.prettyName; },
+ get incomingServer() { return this._incomingServer; },
+ toString: function gloda_account_toString() {
+ return "Account: " + this.id;
+ },
+
+ toLocaleString: function gloda_account_toLocaleString() {
+ return this.name;
+ }
+};
+
+/**
+ * @class A gloda conversation (thread) exists so that messages can belong.
+ */
+function GlodaConversation(aDatastore, aID, aSubject, aOldestMessageDate,
+ aNewestMessageDate) {
+ // _datastore is now set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._subject = aSubject;
+ this._oldestMessageDate = aOldestMessageDate;
+ this._newestMessageDate = aNewestMessageDate;
+}
+
+GlodaConversation.prototype = {
+ NOUN_ID: 101,
+ // set by GlodaDatastore
+ _datastore: null,
+ 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;
+ },
+
+ toLocaleString: function gloda_conversation_toLocaleString() {
+ return this._subject;
+ }
+};
+
+function GlodaFolder(aDatastore, aID, aURI, aDirtyStatus, aPrettyName,
+ aIndexingPriority) {
+ // _datastore is now set by GlodaDatastore
+ this._id = aID;
+ this._uri = aURI;
+ this._dirtyStatus = aDirtyStatus;
+ this._prettyName = aPrettyName;
+ this._xpcomFolder = null;
+ this._account = null;
+ this._activeIndexing = false;
+ this._activeHeaderRetrievalLastStamp = 0;
+ this._indexingPriority = aIndexingPriority;
+ this._deleted = false;
+ this._compacting = false;
+}
+
+GlodaFolder.prototype = {
+ NOUN_ID: 100,
+ // set by GlodaDatastore
+ _datastore: null,
+
+ /** 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,
+
+ _kFolderDirtyStatusMask: 0x7,
+ /**
+ * The (local) folder has been compacted and all of its message keys are
+ * potentially incorrect. This is not a possible state for IMAP folders
+ * because their message keys are based on UIDs rather than offsets into
+ * the mbox file.
+ */
+ _kFolderCompactedFlag: 0x8,
+
+ /** The folder should never be indexed. */
+ kIndexingNeverPriority: -1,
+ /** The lowest priority assigned to a folder. */
+ kIndexingLowestPriority: 0,
+ /** The highest priority assigned to a folder. */
+ kIndexingHighestPriority: 100,
+
+ /** The indexing priority for a folder if no other priority is assigned. */
+ kIndexingDefaultPriority: 20,
+ /** Folders marked check new are slightly more important I guess. */
+ kIndexingCheckNewPriority: 30,
+ /** Favorite folders are more interesting to the user, presumably. */
+ kIndexingFavoritePriority: 40,
+ /** The indexing priority for inboxes. */
+ kIndexingInboxPriority: 50,
+ /** The indexing priority for sent mail folders. */
+ kIndexingSentMailPriority: 60,
+
+ get id() { return this._id; },
+ get uri() { return this._uri; },
+ get dirtyStatus() {
+ return this._dirtyStatus & this._kFolderDirtyStatusMask;
+ },
+ /**
+ * Mark a folder as dirty if it was clean. Do nothing if it was already dirty
+ * or filthy. For use by GlodaMsgIndexer only. And maybe rkent and his
+ * marvelous extensions.
+ */
+ _ensureFolderDirty: function gloda_folder__markFolderDirty() {
+ if (this.dirtyStatus == this.kFolderClean) {
+ this._dirtyStatus = (this.kFolderDirty & this._kFolderDirtyStatusMask) |
+ (this._dirtyStatus & ~this._kFolderDirtyStatusMask);
+ this._datastore.updateFolderDirtyStatus(this);
+ }
+ },
+ /**
+ * Definitely for use only by GlodaMsgIndexer to downgrade the dirty status of
+ * a folder.
+ */
+ _downgradeDirtyStatus: function gloda_folder__downgradeDirtyStatus(
+ aNewStatus) {
+ if (this.dirtyStatus != aNewStatus) {
+ this._dirtyStatus = (aNewStatus & this._kFolderDirtyStatusMask) |
+ (this._dirtyStatus & ~this._kFolderDirtyStatusMask);
+ this._datastore.updateFolderDirtyStatus(this);
+ }
+ },
+ /**
+ * Indicate whether this folder is currently being compacted. The
+ * |GlodaMsgIndexer| keeps this in-memory-only value up-to-date.
+ */
+ get compacting() {
+ return this._compacting;
+ },
+ /**
+ * Set whether this folder is currently being compacted. This is really only
+ * for the |GlodaMsgIndexer| to set.
+ */
+ set compacting(aCompacting) {
+ this._compacting = aCompacting;
+ },
+ /**
+ * Indicate whether this folder was compacted and has not yet been
+ * compaction processed.
+ */
+ get compacted() {
+ return Boolean(this._dirtyStatus & this._kFolderCompactedFlag);
+ },
+ /**
+ * For use only by GlodaMsgIndexer to set/clear the compaction state of this
+ * folder.
+ */
+ _setCompactedState: function gloda_folder__clearCompactedState(aCompacted) {
+ if (this.compacted != aCompacted) {
+ if (aCompacted)
+ this._dirtyStatus |= this._kFolderCompactedFlag;
+ else
+ this._dirtyStatus &= ~this._kFolderCompactedFlag;
+ this._datastore.updateFolderDirtyStatus(this);
+ }
+ },
+
+ get name() { return this._prettyName; },
+ toString: function gloda_folder_toString() {
+ return "Folder:" + this._id;
+ },
+
+ toLocaleString: function gloda_folder_toLocaleString() {
+ let xpcomFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+ if (!xpcomFolder)
+ return this._prettyName;
+ return xpcomFolder.prettiestName +
+ " (" + xpcomFolder.rootFolder.prettiestName + ")";
+ },
+
+ get indexingPriority() {
+ return this._indexingPriority;
+ },
+
+ /** We are going to index this folder. */
+ kActivityIndexing: 0,
+ /** Asking for the folder to perform header retrievals. */
+ kActivityHeaderRetrieval: 1,
+ /** We only want the folder for its metadata but are not going to open it. */
+ kActivityFolderOnlyNoData: 2,
+
+
+ /** Is this folder known to be actively used for indexing? */
+ _activeIndexing: false,
+ /** Get our indexing status. */
+ get indexing() {
+ return this._activeIndexing;
+ },
+ /**
+ * Set our indexing status. Normally, this will be enabled through passing
+ * an activity type of kActivityIndexing (which will set us), but we will
+ * still need to be explicitly disabled by the indexing code.
+ * When disabling indexing, we will call forgetFolderIfUnused to take care of
+ * shutting things down.
+ * We are not responsible for committing changes to the message database!
+ * That is on you!
+ */
+ set indexing(aIndexing) {
+ this._activeIndexing = aIndexing;
+ if (!aIndexing)
+ this.forgetFolderIfUnused();
+ },
+ /** When was this folder last used for header retrieval purposes? */
+ _activeHeaderRetrievalLastStamp: 0,
+
+ /**
+ * Retrieve the nsIMsgFolder instance corresponding to this folder, providing
+ * an explanation of why you are requesting it for tracking/cleanup purposes.
+ *
+ * @param aActivity One of the kActivity* constants. If you pass
+ * kActivityIndexing, we will set indexing for you, but you will need to
+ * clear it when you are done.
+ * @return The nsIMsgFolder if available, null on failure.
+ */
+ getXPCOMFolder: function gloda_folder_getXPCOMFolder(aActivity) {
+ if (!this._xpcomFolder) {
+ let rdfService = Cc['@mozilla.org/rdf/rdf-service;1']
+ .getService(Ci.nsIRDFService);
+ this._xpcomFolder = rdfService.GetResource(this.uri)
+ .QueryInterface(Ci.nsIMsgFolder);
+ }
+ switch (aActivity) {
+ case this.kActivityIndexing:
+ // mark us as indexing, but don't bother with live tracking. we do
+ // that independently and only for header retrieval.
+ this.indexing = true;
+ break;
+ case this.kActivityHeaderRetrieval:
+ if (this._activeHeaderRetrievalLastStamp === 0)
+ this._datastore.markFolderLive(this);
+ this._activeHeaderRetrievalLastStamp = Date.now();
+ break;
+ case this.kActivityFolderOnlyNoData:
+ // we don't have to do anything here.
+ break;
+ }
+
+ return this._xpcomFolder;
+ },
+
+ /**
+ * Retrieve a GlodaAccount instance corresponding to this folder.
+ *
+ * @return The GlodaAccount instance.
+ */
+ getAccount: function gloda_folder_getAccount() {
+ if (!this._account) {
+ let msgFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+ this._account = new GlodaAccount(msgFolder.server);
+ }
+ return this._account;
+ },
+
+ /**
+ * How many milliseconds must a folder have not had any header retrieval
+ * activity before it's okay to lose the database reference?
+ */
+ ACCEPTABLY_OLD_THRESHOLD: 10000,
+
+ /**
+ * Cleans up our nsIMsgFolder reference if we have one and it's not "in use".
+ * In use, from our perspective, means that it is not being used for indexing
+ * and some arbitrary interval of time has elapsed since it was last
+ * retrieved for header retrieval reasons. The time interval is because if
+ * we have one GlodaMessage requesting a header, there's a high probability
+ * that another message will request a header in the near future.
+ * Because setting indexing to false disables us, we are written in an
+ * idempotent fashion. (It is possible for disabling indexing's call to us
+ * to cause us to return true but for the datastore's timer call to have not
+ * yet triggered.)
+ *
+ * @returns true if we are cleaned up and can be considered 'dead', false if
+ * we should still be considered alive and this method should be called
+ * again in the future.
+ */
+ forgetFolderIfUnused: function gloda_folder_forgetFolderIfUnused() {
+ // we are not cleaning/cleaned up if we are indexing
+ if (this._activeIndexing)
+ return false;
+
+ // set a point in the past as the threshold. the timestamp must be older
+ // than this to be eligible for cleanup.
+ let acceptablyOld = Date.now() - this.ACCEPTABLY_OLD_THRESHOLD;
+ // we are not cleaning/cleaned up if we have retrieved a header more
+ // recently than the acceptably old threshold.
+ if (this._activeHeaderRetrievalLastStamp > acceptablyOld)
+ return false;
+
+ if (this._xpcomFolder) {
+ // This is the key action we take; the nsIMsgFolder will continue to
+ // exist, but we want it to forget about its database so that it can
+ // be closed and its memory can be reclaimed.
+ this._xpcomFolder.msgDatabase = null;
+ this._xpcomFolder = null;
+ // since the last retrieval time tracks whether we have marked live or
+ // not, this needs to be reset to 0 too.
+ this._activeHeaderRetrievalLastStamp = 0;
+ }
+
+ return true;
+ }
+};
+
+/**
+ * @class A message representation.
+ */
+function GlodaMessage(aDatastore, aID, aFolderID, aMessageKey,
+ aConversationID, aConversation, aDate,
+ aHeaderMessageID, aDeleted, aJsonText,
+ aNotability,
+ aSubject, aIndexedBodyText, aAttachmentNames) {
+ // _datastore is now set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._folderID = aFolderID;
+ this._messageKey = aMessageKey;
+ this._conversationID = aConversationID;
+ this._conversation = aConversation;
+ this._date = aDate;
+ this._headerMessageID = aHeaderMessageID;
+ this._jsonText = aJsonText;
+ this._notability = aNotability;
+ this._subject = aSubject;
+ this._indexedBodyText = aIndexedBodyText;
+ this._attachmentNames = aAttachmentNames;
+
+ // 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,
+ // set by GlodaDatastore
+ _datastore: null,
+ 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 notability() { return this._notability; },
+ set notability(aNotability) { this._notability = aNotability; },
+
+ get subject() { return this._subject; },
+ get indexedBodyText() { return this._indexedBodyText; },
+ get attachmentNames() { return this._attachmentNames; },
+
+ get date() { return this._date; },
+ set date(aNewDate) { this._date = aNewDate; },
+
+ get folder() {
+ // XXX due to a deletion bug it is currently possible to get in a state
+ // where we have an illegal folderID value. This will result in an
+ // exception. As a workaround, let's just return null in that case.
+ try {
+ if (this._folderID != null)
+ return this._datastore._mapFolderID(this._folderID);
+ }
+ catch (ex) {
+ }
+ return null;
+ },
+ get folderURI() {
+ // XXX just like for folder, handle mapping failures and return null
+ try {
+ if (this._folderID != null)
+ return this._datastore._mapFolderID(this._folderID).uri;
+ }
+ catch (ex) {
+ }
+ return null;
+ },
+ get account() {
+ // XXX due to a deletion bug it is currently possible to get in a state
+ // where we have an illegal folderID value. This will result in an
+ // exception. As a workaround, let's just return null in that case.
+ try {
+ if (this._folderID == null)
+ return null;
+ let folder = this._datastore._mapFolderID(this._folderID);
+ return folder.getAccount();
+ }
+ catch (ex) { }
+ 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(/* datastore */ null, this._id, this._folderID,
+ this._messageKey, this._conversationID, this._conversation, this._date,
+ this._headerMessageID, "_deleted" in this ? this._deleted : undefined,
+ "_jsonText" in this ? this._jsonText : undefined, this._notability,
+ this._subject, this._indexedBodyText, this._attachmentNames);
+ },
+
+ /**
+ * Provide a means of propagating changed values on our clone back to
+ * ourselves. This is required because of an object identity trick gloda
+ * does; when indexing an already existing object, all mutations happen on
+ * a clone of the existing object so that
+ */
+ _declone: function gloda_message_declone(aOther) {
+ if ("_content" in aOther)
+ this._content = aOther._content;
+
+ // The _indexedAuthor/_indexedRecipients fields don't get updated on
+ // fulltext update so we don't need to propagate.
+ this._indexedBodyText = aOther._indexedBodyText;
+ this._attachmentNames = aOther._attachmentNames;
+ },
+
+ /**
+ * Mark this message as a ghost. Ghosts are characterized by having no folder
+ * id and no message key. They also are not deleted or they would be of
+ * absolutely no use to us.
+ *
+ * These changes are suitable for persistence.
+ */
+ _ghost: function gloda_message_ghost() {
+ this._folderID = null;
+ this._messageKey = null;
+ if ("_deleted" in this)
+ delete this._deleted;
+ },
+
+ /**
+ * Are we a ghost (which implies not deleted)? We are not a ghost if we have
+ * a definite folder location (we may not know our message key in the case
+ * of IMAP moves not fully completed) and are not deleted.
+ */
+ get _isGhost() {
+ return this._folderID == null && !this._isDeleted;
+ },
+
+ /**
+ * If we were dead, un-dead us.
+ */
+ _ensureNotDeleted: function gloda_message__ensureNotDeleted() {
+ if ("_deleted" in this)
+ delete this._deleted;
+ },
+
+ /**
+ * Are we deleted? This is private because deleted gloda messages are not
+ * visible to non-core-gloda code.
+ */
+ get _isDeleted() {
+ return ("_deleted" in this) && this._deleted;
+ },
+
+ /**
+ * Trash this message's in-memory representation because it should no longer
+ * be reachable by any code. The database record is gone, it's not coming
+ * back.
+ */
+ _objectPurgedMakeYourselfUnpleasant: 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;
+ },
+
+ /**
+ * Return the underlying nsIMsgDBHdr from the folder storage for this, or
+ * null if the message does not exist for one reason or another. We may log
+ * to our logger in the failure cases.
+ *
+ * This method no longer caches the result, so if you need to hold onto it,
+ * hold onto it.
+ *
+ * In the process of retrieving the underlying message header, we may have to
+ * open the message header database associated with the folder. This may
+ * result in blocking while the load happens, so you may want to try and find
+ * an alternate way to initiate the load before calling us.
+ * We provide hinting to the GlodaDatastore via the GlodaFolder so that it
+ * knows when it's a good time for it to go and detach from the database.
+ *
+ * @returns The nsIMsgDBHdr associated with this message if available, null on
+ * failure.
+ */
+ get folderMessage() {
+ if (this._folderID === null || this._messageKey === null)
+ return null;
+
+ // XXX like for folder and folderURI, return null if we can't map the folder
+ let glodaFolder;
+ try {
+ glodaFolder = this._datastore._mapFolderID(this._folderID);
+ }
+ catch (ex) {
+ return null;
+ }
+ let folder = glodaFolder.getXPCOMFolder(
+ glodaFolder.kActivityHeaderRetrieval);
+ if (folder) {
+ let folderMessage;
+ try {
+ folderMessage = folder.GetMessageHeader(this._messageKey);
+ }
+ catch (ex) {
+ folderMessage = null;
+ }
+ if (folderMessage !== null) {
+ // verify the message-id header matches what we expect...
+ if (folderMessage.messageId != this._headerMessageID) {
+ LOG.info("Message with message key " + this._messageKey +
+ " in folder '" + folder.URI + "' 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) {
+ // _datastore set on the prototype by GlodaDatastore
+ 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,
+ // set by GlodaDatastore
+ _datastore: null,
+
+ 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(/* datastore */ null, 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) {
+ // _datastore set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._contactID = aContactID;
+ this._contact = aContact;
+ this._kind = aKind;
+ this._value = aValue;
+ this._description = aDescription;
+ this._isRelay = aIsRelay;
+ /// Cached indication of whether there is an address book card for this
+ /// identity. We keep this up-to-date via address book listener
+ /// notifications in |GlodaABIndexer|.
+ this._hasAddressBookCard = undefined;
+}
+
+GlodaIdentity.prototype = {
+ NOUN_ID: 104,
+ // set by GlodaDatastore
+ _datastore: null,
+ 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;
+ },
+
+ toLocaleString: function gloda_identity_toLocaleString() {
+ if (this.contact.name == this.value)
+ return this.value;
+ return this.contact.name + " : " + this.value;
+ },
+
+ get abCard() {
+ // for our purposes, the address book only speaks email
+ if (this._kind != "email")
+ return false;
+ let card = GlodaUtils.getCardForEmail(this._value);
+ this._hasAddressBookCard = (card != null);
+ return card;
+ },
+
+ /**
+ * Indicates whether we have an address book card for this identity. This
+ * value is cached once looked-up and kept up-to-date by |GlodaABIndexer|
+ * and its notifications.
+ */
+ get inAddressBook() {
+ if (this._hasAddressBookCard !== undefined)
+ return this._hasAddressBookCard;
+ return (this.abCard && true) || false;
+ },
+
+ pictureURL: function(aSize) {
+ if (this.inAddressBook) {
+ // XXX should get the photo if we have it.
+ }
+ return "";
+ }
+};
+
+
+/**
+ * An attachment, with as much information as we can gather on it
+ */
+function GlodaAttachment(aGlodaMessage, aName, aContentType, aSize, aPart, aExternalUrl, aIsExternal) {
+ // _datastore set on the prototype by GlodaDatastore
+ this._glodaMessage = aGlodaMessage;
+ this._name = aName;
+ this._contentType = aContentType;
+ this._size = aSize;
+ this._part = aPart;
+ this._externalUrl = aExternalUrl;
+ this._isExternal = aIsExternal;
+}
+
+GlodaAttachment.prototype = {
+ NOUN_ID: 105,
+ // set by GlodaDatastore
+ get name() { return this._name; },
+ get contentType() { return this._contentType; },
+ get size() { return this._size; },
+ get url() {
+ if (this.isExternal)
+ return this._externalUrl;
+ else {
+ let uri = this._glodaMessage.folderMessageURI;
+ if (!uri)
+ throw new Error("The message doesn't exist anymore, unable to rebuild attachment URL");
+ let neckoURL = {};
+ let msgService = getMessenger().messageServiceFromURI(uri);
+ msgService.GetUrlForUri(uri, neckoURL, null);
+ let url = neckoURL.value.spec;
+ let hasParamAlready = url.match(/\?[a-z]+=[^\/]+$/);
+ let sep = hasParamAlready ? "&" : "?";
+ return url+sep+"part="+this._part+"&filename="+encodeURIComponent(this._name);
+ }
+ },
+ get isExternal() { return this._isExternal; },
+
+ toString: function gloda_attachment_toString() {
+ return "attachment: " + this._name + ":" + this._contentType;
+ },
+
+};