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