summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/db/gloda/modules/GlodaDataModel.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/db/gloda/modules/GlodaDataModel.jsm')
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaDataModel.jsm1020
1 files changed, 1020 insertions, 0 deletions
diff --git a/comm/mailnews/db/gloda/modules/GlodaDataModel.jsm b/comm/mailnews/db/gloda/modules/GlodaDataModel.jsm
new file mode 100644
index 0000000000..d9361c079c
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaDataModel.jsm
@@ -0,0 +1,1020 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = [
+ "GlodaAttributeDBDef",
+ "GlodaAccount",
+ "GlodaConversation",
+ "GlodaFolder",
+ "GlodaMessage",
+ "GlodaContact",
+ "GlodaIdentity",
+ "GlodaAttachment",
+];
+
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var LOG = console.createInstance({
+ prefix: "gloda.datamodel",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+/**
+ * @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 really 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.
+ *
+ * @returns
+ */
+ 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(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 if ("toParamAndValue" in nounDef) {
+ // 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.
+ for (let instanceValue of aInstanceValues) {
+ dbAttributes.push([
+ this._id,
+ nounDef.toParamAndValue(instanceValue)[1],
+ ]);
+ }
+ }
+ return dbAttributes;
+ },
+
+ toString() {
+ return this._compoundName;
+ },
+};
+
+var GlodaHasAttributesMixIn = {
+ *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 if (value.length) {
+ // ignore attributes with no values
+ yield [attrDef, value];
+ }
+ }
+ },
+
+ 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] of Object.entries(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() {
+ return "Account: " + this.id;
+ },
+
+ 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: GlodaConstants.NOUN_CONVERSATION,
+ // 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(aListener, aData) {
+ let query = new GlodaMessage.prototype.NOUN_DEF.queryClass();
+ query.conversation(this._id).orderBy("date");
+ return query.getCollection(aListener, aData);
+ },
+
+ toString() {
+ return "Conversation:" + this._id;
+ },
+
+ 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._account = null;
+ this._activeIndexing = false;
+ this._indexingPriority = aIndexingPriority;
+ this._deleted = false;
+ this._compacting = false;
+}
+
+GlodaFolder.prototype = {
+ NOUN_ID: GlodaConstants.NOUN_FOLDER,
+ // 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() {
+ 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(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(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() {
+ return "Folder:" + this._id;
+ },
+
+ toLocaleString() {
+ let xpcomFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+ if (!xpcomFolder) {
+ return this._prettyName;
+ }
+ return (
+ xpcomFolder.prettyName + " (" + xpcomFolder.rootFolder.prettyName + ")"
+ );
+ },
+
+ 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;
+ },
+
+ /**
+ * 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.
+ * @returns The nsIMsgFolder if available, null on failure.
+ */
+ getXPCOMFolder(aActivity) {
+ 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:
+ case this.kActivityFolderOnlyNoData:
+ // we don't have to do anything here.
+ break;
+ }
+
+ return MailServices.folderLookup.getFolderForURL(this.uri);
+ },
+
+ /**
+ * Retrieve a GlodaAccount instance corresponding to this folder.
+ *
+ * @returns The GlodaAccount instance.
+ */
+ getAccount() {
+ if (!this._account) {
+ let msgFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+ this._account = new GlodaAccount(msgFolder.server);
+ }
+ return this._account;
+ },
+};
+
+/**
+ * @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: GlodaConstants.NOUN_MESSAGE,
+ // 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() {
+ // uh, this is a tough one...
+ return "Message:" + this._id;
+ },
+
+ _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(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() {
+ 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() {
+ 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() {
+ 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);
+ }
+ 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: GlodaConstants.NOUN_CONTACT,
+ // 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() {
+ return "Contact:" + this._id;
+ },
+
+ get accessibleLabel() {
+ return "Contact: " + this._name;
+ },
+
+ _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: GlodaConstants.NOUN_IDENTITY,
+ // 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() {
+ return "Identity:" + this._kind + ":" + this._value;
+ },
+
+ 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 = MailServices.ab.cardForEmailAddress(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;
+ },
+};
+
+/**
+ * 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: GlodaConstants.NOUN_ATTACHMENT,
+ // 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;
+ }
+
+ let uri = this._glodaMessage.folderMessageURI;
+ if (!uri) {
+ throw new Error(
+ "The message doesn't exist anymore, unable to rebuild attachment URL"
+ );
+ }
+ let msgService = MailServices.messageServiceFromURI(uri);
+ let neckoURL = msgService.getUrlForUri(uri);
+ let url = neckoURL.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() {
+ return "attachment: " + this._name + ":" + this._contentType;
+ },
+};