Bug 1888655 - Use proper module structure for SearchIntegration. r=mkmelin,aleca
Differential Revision: https://phabricator.services.mozilla.com/D208875 --HG-- rename : mail/components/search/SearchIntegration.sys.mjs => mail/components/search/SearchSupport.sys.mjs rename : mail/components/search/content/SpotlightIntegration.js => mail/components/search/SpotlightIntegration.sys.mjs rename : mail/components/search/content/WinSearchIntegration.js => mail/components/search/WinSearchIntegration.sys.mjs extra : amend_source : 47d23ae1bbb60235292f7379a39b7ecc11e79a9d
This commit is contained in:
Родитель
3477c1c2df
Коммит
ebc9d593fd
|
@ -3,869 +3,25 @@
|
|||
* 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/. */
|
||||
|
||||
/**
|
||||
* Common, useful functions for desktop search integration components.
|
||||
*
|
||||
* The following symbols have to be defined for each component that includes this:
|
||||
* - gHdrIndexedProperty: the property in the database that indicates whether a message
|
||||
* has been indexed
|
||||
* - gFileExt: the file extension to be used for support files
|
||||
* - gPrefBase: the base for preferences that are stored
|
||||
* - gStreamListener: an nsIStreamListener to read message text
|
||||
*/
|
||||
|
||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||
|
||||
import { MailServices } from "resource:///modules/MailServices.sys.mjs";
|
||||
|
||||
var PERM_DIRECTORY = 0o755;
|
||||
var PERM_FILE = 0o644;
|
||||
|
||||
// SearchIntegration will be assigned per platform through
|
||||
// Services.scriptloader.loadSubScript - see bottom of file.
|
||||
export var SearchIntegration = null;
|
||||
|
||||
// SearchSupport is used by subscripts.
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
var SearchSupport = {
|
||||
/**
|
||||
* URI of last folder indexed. Kept in sync with the pref
|
||||
*/
|
||||
__lastFolderIndexedUri: null,
|
||||
set _lastFolderIndexedUri(uri) {
|
||||
this._prefBranch.setStringPref("lastFolderIndexedUri", uri);
|
||||
this.__lastFolderIndexedUri = uri;
|
||||
},
|
||||
get _lastFolderIndexedUri() {
|
||||
// If we don't know about it, get it from the pref branch
|
||||
if (this.__lastFolderIndexedUri === null) {
|
||||
this.__lastFolderIndexedUri = this._prefBranch.getStringPref(
|
||||
"lastFolderIndexedUri",
|
||||
""
|
||||
);
|
||||
}
|
||||
return this.__lastFolderIndexedUri;
|
||||
},
|
||||
|
||||
/**
|
||||
* Queue of message headers to index, along with reindex times for each header
|
||||
*/
|
||||
_msgHdrsToIndex: [],
|
||||
|
||||
/**
|
||||
* Messenger object, used primarily to get message URIs
|
||||
*/
|
||||
__messenger: null,
|
||||
get _messenger() {
|
||||
if (!this.__messenger) {
|
||||
this.__messenger = Cc["@mozilla.org/messenger;1"].createInstance(
|
||||
Ci.nsIMessenger
|
||||
);
|
||||
}
|
||||
return this.__messenger;
|
||||
},
|
||||
|
||||
// The preferences branch to use
|
||||
__prefBranch: null,
|
||||
get _prefBranch() {
|
||||
if (!this.__prefBranch) {
|
||||
this.__prefBranch = Services.prefs.getBranch(this._prefBase);
|
||||
}
|
||||
return this.__prefBranch;
|
||||
},
|
||||
|
||||
/**
|
||||
* If this is true, we won't show any UI because the OS doesn't have the
|
||||
* support we need
|
||||
*/
|
||||
osVersionTooLow: false,
|
||||
|
||||
/**
|
||||
* If this is true, we'll show disabled UI, because while the OS does have
|
||||
* the support we need, not all the OS components we need are running
|
||||
*/
|
||||
osComponentsNotRunning: false,
|
||||
|
||||
/**
|
||||
* Whether the preference is enabled. The module might be in a state where
|
||||
* the preference is on but "enabled" is false, so take care of that.
|
||||
*/
|
||||
get prefEnabled() {
|
||||
// Don't cache the value
|
||||
return this._prefBranch.getBoolPref("enable");
|
||||
},
|
||||
set prefEnabled(aEnabled) {
|
||||
if (this.prefEnabled != aEnabled) {
|
||||
this._prefBranch.setBoolPref("enable", aEnabled);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the first run has occurred. This will be used to determine if
|
||||
* a dialog box needs to be displayed.
|
||||
*/
|
||||
get firstRunDone() {
|
||||
// Don't cache this value either
|
||||
return this._prefBranch.getBoolPref("firstRunDone");
|
||||
},
|
||||
set firstRunDone(aAlwaysTrue) {
|
||||
this._prefBranch.setBoolPref("firstRunDone", true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Last global reindex time, used to check if reindexing is required.
|
||||
* Kept in sync with the pref
|
||||
*/
|
||||
_globalReindexTime: null,
|
||||
set globalReindexTime(aTime) {
|
||||
this._globalReindexTime = aTime;
|
||||
// Set the pref as well
|
||||
this._prefBranch.setCharPref("global_reindex_time", "" + aTime);
|
||||
},
|
||||
get globalReindexTime() {
|
||||
if (!this._globalReindexTime) {
|
||||
// Try getting the time from the preferences
|
||||
try {
|
||||
this._globalReindexTime = parseInt(
|
||||
this._prefBranch.getCharPref("global_reindex_time")
|
||||
);
|
||||
} catch (e) {
|
||||
// We don't have it defined, so set it (Unix time, in seconds)
|
||||
this._globalReindexTime = parseInt(Date.now() / 1000);
|
||||
this._prefBranch.setCharPref(
|
||||
"global_reindex_time",
|
||||
"" + this._globalReindexTime
|
||||
);
|
||||
}
|
||||
}
|
||||
return this._globalReindexTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Amount of time the user is idle before we (re)start an indexing sweep
|
||||
*/
|
||||
_idleThresholdSecs: 30,
|
||||
|
||||
/**
|
||||
* Reference to timer object
|
||||
*/
|
||||
__timer: null,
|
||||
get _timer() {
|
||||
if (!this.__timer) {
|
||||
this.__timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
}
|
||||
return this.__timer;
|
||||
},
|
||||
|
||||
_cancelTimer() {
|
||||
try {
|
||||
this._timer.cancel();
|
||||
} catch (ex) {}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enabled status.
|
||||
*
|
||||
* When we're enabled, then we get notifications about every message or folder
|
||||
* operation, including "message displayed" operations which we bump up in
|
||||
* priority. We also have a background sweep which we do on idle.
|
||||
*
|
||||
* We aren't fully disabled when we're "disabled", though. We still observe
|
||||
* message and folder moves and deletes, as we don't want to have support
|
||||
* files for non-existent messages.
|
||||
*/
|
||||
_enabled: null,
|
||||
set enabled(aEnable) {
|
||||
// Nothing to do if there's no change in state
|
||||
if (this._enabled == aEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.info(
|
||||
"Enabled status changing from " + this._enabled + " to " + aEnable
|
||||
);
|
||||
|
||||
this._removeObservers();
|
||||
|
||||
if (aEnable) {
|
||||
// This stuff we always need to do.
|
||||
// This code pre-dates msgsClassified.
|
||||
// Some events intentionally omitted.
|
||||
MailServices.mfn.addListener(
|
||||
this._msgFolderListener,
|
||||
MailServices.mfn.msgAdded |
|
||||
MailServices.mfn.msgsDeleted |
|
||||
MailServices.mfn.msgsMoveCopyCompleted |
|
||||
MailServices.mfn.folderDeleted |
|
||||
MailServices.mfn.folderMoveCopyCompleted |
|
||||
MailServices.mfn.folderRenamed
|
||||
);
|
||||
Services.obs.addObserver(this, "MsgMsgDisplayed");
|
||||
const idleService = Cc[
|
||||
"@mozilla.org/widget/useridleservice;1"
|
||||
].getService(Ci.nsIUserIdleService);
|
||||
idleService.addIdleObserver(this, this._idleThresholdSecs);
|
||||
} else {
|
||||
// We want to observe moves, deletes and renames in case we're disabled
|
||||
// If we don't, we'll have no idea the support files exist later
|
||||
MailServices.mfn.addListener(
|
||||
this._msgFolderListener,
|
||||
MailServices.mfn.msgsMoveCopyCompleted |
|
||||
MailServices.mfn.msgsDeleted |
|
||||
// folderAdded intentionally omitted
|
||||
MailServices.mfn.folderDeleted |
|
||||
MailServices.mfn.folderMoveCopyCompleted |
|
||||
MailServices.mfn.folderRenamed
|
||||
);
|
||||
}
|
||||
|
||||
this._enabled = aEnable;
|
||||
},
|
||||
get enabled() {
|
||||
return this._enabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove whatever observers are present. This is done while switching states
|
||||
*/
|
||||
_removeObservers() {
|
||||
if (this.enabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MailServices.mfn.removeListener(this._msgFolderListener);
|
||||
|
||||
if (this.enabled) {
|
||||
Services.obs.removeObserver(this, "MsgMsgDisplayed");
|
||||
const idleService = Cc[
|
||||
"@mozilla.org/widget/useridleservice;1"
|
||||
].getService(Ci.nsIUserIdleService);
|
||||
idleService.removeIdleObserver(this, this._idleThresholdSecs);
|
||||
|
||||
// in case there's a background sweep going on
|
||||
this._cancelTimer();
|
||||
}
|
||||
// We don't need to do anything extra if we're disabled
|
||||
},
|
||||
|
||||
/**
|
||||
* Init function -- this should be called from the component's init function
|
||||
*/
|
||||
_initSupport(enabled) {
|
||||
this._log.info(
|
||||
"Search integration running in " +
|
||||
(enabled ? "active" : "backoff") +
|
||||
" mode"
|
||||
);
|
||||
this.enabled = enabled;
|
||||
|
||||
// Set up a pref observer
|
||||
this._prefBranch.addObserver("enable", this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Current folder being indexed
|
||||
*/
|
||||
_currentFolderToIndex: null,
|
||||
|
||||
/**
|
||||
* For the current folder being indexed, an enumerator for all the headers in
|
||||
* the folder
|
||||
*/
|
||||
_headerEnumerator: null,
|
||||
|
||||
/*
|
||||
* These functions are to index already existing messages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generator to look for the next folder to index, and return it
|
||||
*
|
||||
* This first looks for folders that have their corresponding search results
|
||||
* folders missing. If it finds such a folder first, it'll yield return that
|
||||
* folder.
|
||||
*
|
||||
* Next, it looks for the next folder after the lastFolderIndexedUri. If it is
|
||||
* in such a folder, it'll yield return that folder, then set the
|
||||
* lastFolderIndexedUrl to the URI of that folder.
|
||||
*
|
||||
* It resets lastFolderIndexedUri to an empty string, then yield returns null
|
||||
* once iteration across all folders is complete.
|
||||
*/
|
||||
*_foldersToIndexGenerator() {
|
||||
// Stores whether we're after the last folder indexed or before that --
|
||||
// if the last folder indexed is empty, this needs to be true initially
|
||||
let afterLastFolderIndexed = this._lastFolderIndexedUri.length == 0;
|
||||
|
||||
for (const server of MailServices.accounts.allServers) {
|
||||
this._log.debug(
|
||||
"in find next folder, lastFolderIndexedUri = " +
|
||||
this._lastFolderIndexedUri
|
||||
);
|
||||
|
||||
for (var folder of server.rootFolder.descendants) {
|
||||
const searchPath = this._getSearchPathForFolder(folder);
|
||||
searchPath.leafName = searchPath.leafName + ".mozmsgs";
|
||||
// If after the last folder indexed, definitely index this
|
||||
if (afterLastFolderIndexed) {
|
||||
// Create the folder if it doesn't exist, so that we don't hit the
|
||||
// condition below later
|
||||
if (!searchPath.exists()) {
|
||||
searchPath.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
}
|
||||
|
||||
yield folder;
|
||||
// We're back after yielding -- set the last folder indexed
|
||||
this._lastFolderIndexedUri = folder.URI;
|
||||
} else {
|
||||
// If a folder's entire corresponding search results folder is
|
||||
// missing, we need to index it, and force a reindex of all the
|
||||
// messages in it
|
||||
if (!searchPath.exists()) {
|
||||
this._log.debug(
|
||||
"using folder " +
|
||||
folder.URI +
|
||||
" because " +
|
||||
"corresponding search folder does not exist"
|
||||
);
|
||||
// Create the folder, so that next time we're checking we don't hit
|
||||
// this
|
||||
searchPath.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
folder.setStringProperty(
|
||||
this._hdrIndexedProperty,
|
||||
"" + Date.now() / 1000
|
||||
);
|
||||
yield folder;
|
||||
} else if (this._pathNeedsReindexing(searchPath)) {
|
||||
// folder may need reindexing for other reasons
|
||||
folder.setStringProperty(
|
||||
this._hdrIndexedProperty,
|
||||
"" + Date.now() / 1000
|
||||
);
|
||||
yield folder;
|
||||
}
|
||||
|
||||
// Even if we yielded above, check if this is the last folder
|
||||
// indexed
|
||||
if (this._lastFolderIndexedUri == folder.URI) {
|
||||
afterLastFolderIndexed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We're done with one iteration of all the folders; time to reset the
|
||||
// lastFolderIndexedUri
|
||||
this._lastFolderIndexedUri = "";
|
||||
yield null;
|
||||
},
|
||||
|
||||
__foldersToIndex: null,
|
||||
get _foldersToIndex() {
|
||||
if (!this.__foldersToIndex) {
|
||||
this.__foldersToIndex = this._foldersToIndexGenerator();
|
||||
}
|
||||
return this.__foldersToIndex;
|
||||
},
|
||||
|
||||
_findNextHdrToIndex() {
|
||||
try {
|
||||
const reindexTime = this._getLastReindexTime(this._currentFolderToIndex);
|
||||
this._log.debug("Reindex time for this folder is " + reindexTime);
|
||||
if (!this._headerEnumerator) {
|
||||
// we need to create search terms for messages to index
|
||||
const searchSession = Cc[
|
||||
"@mozilla.org/messenger/searchSession;1"
|
||||
].createInstance(Ci.nsIMsgSearchSession);
|
||||
const searchTerms = [];
|
||||
|
||||
searchSession.addScopeTerm(
|
||||
Ci.nsMsgSearchScope.offlineMail,
|
||||
this._currentFolderToIndex
|
||||
);
|
||||
// first term: (_hdrIndexProperty < reindexTime)
|
||||
const searchTerm = searchSession.createTerm();
|
||||
searchTerm.booleanAnd = false; // actually don't care here
|
||||
searchTerm.attrib = Ci.nsMsgSearchAttrib.Uint32HdrProperty;
|
||||
searchTerm.op = Ci.nsMsgSearchOp.IsLessThan;
|
||||
const value = searchTerm.value;
|
||||
value.attrib = searchTerm.attrib;
|
||||
searchTerm.hdrProperty = this._hdrIndexedProperty;
|
||||
value.status = reindexTime;
|
||||
searchTerm.value = value;
|
||||
searchTerms.push(searchTerm);
|
||||
this._headerEnumerator =
|
||||
this._currentFolderToIndex.msgDatabase.getFilterEnumerator(
|
||||
searchTerms
|
||||
);
|
||||
}
|
||||
|
||||
// iterate over the folder finding the next message to index
|
||||
for (const msgHdr of this._headerEnumerator) {
|
||||
// Check if the file exists. If it does, then assume indexing to be
|
||||
// complete for this file
|
||||
if (this._getSupportFile(msgHdr).exists()) {
|
||||
this._log.debug(
|
||||
"Message time not set but file exists; setting " +
|
||||
"time to " +
|
||||
reindexTime
|
||||
);
|
||||
msgHdr.setUint32Property(this._hdrIndexedProperty, reindexTime);
|
||||
} else {
|
||||
return [msgHdr, reindexTime];
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
this._log.debug("Error while finding next header: " + ex);
|
||||
}
|
||||
|
||||
// If we couldn't find any headers to index, null out the enumerator
|
||||
this._headerEnumerator = null;
|
||||
if (!(this._currentFolderToIndex.flags & Ci.nsMsgFolderFlags.Inbox)) {
|
||||
this._currentFolderToIndex.msgDatabase = null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the last reindex time for this folder. This will be whichever's
|
||||
* greater, the global reindex time or the folder reindex time
|
||||
*/
|
||||
_getLastReindexTime() {
|
||||
let reindexTime = this.globalReindexTime;
|
||||
|
||||
// Check if this folder has a separate string property set
|
||||
let folderReindexTime;
|
||||
try {
|
||||
folderReindexTime = this._currentFolderToIndex.getStringProperty(
|
||||
this._hdrIndexedProperty
|
||||
);
|
||||
} catch (e) {
|
||||
folderReindexTime = "";
|
||||
}
|
||||
|
||||
if (folderReindexTime.length > 0) {
|
||||
const folderReindexTimeInt = parseInt(folderReindexTime);
|
||||
if (folderReindexTimeInt > reindexTime) {
|
||||
reindexTime = folderReindexTimeInt;
|
||||
}
|
||||
}
|
||||
return reindexTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether background indexing has been completed
|
||||
*/
|
||||
__backgroundIndexingDone: false,
|
||||
|
||||
/**
|
||||
* The main background sweeping function. It first looks for a folder to
|
||||
* start or continue indexing in, then for a header. If it can't find anything
|
||||
* to index, it resets the last folder indexed URI so that the sweep can
|
||||
* be restarted
|
||||
*/
|
||||
_continueSweep() {
|
||||
let msgHdrAndReindexTime = null;
|
||||
|
||||
if (this.__backgroundIndexingDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find the current folder we're working on
|
||||
if (!this._currentFolderToIndex) {
|
||||
this._currentFolderToIndex = this._foldersToIndex.next().value;
|
||||
}
|
||||
|
||||
// we'd like to index more than one message on each timer fire,
|
||||
// but since streaming is async, it's hard to know how long
|
||||
// it's going to take to stream any particular message.
|
||||
if (this._currentFolderToIndex) {
|
||||
msgHdrAndReindexTime = this._findNextHdrToIndex();
|
||||
} else {
|
||||
// We've cycled through all the folders. We should take a break
|
||||
// from indexing of existing messages.
|
||||
this.__backgroundIndexingDone = true;
|
||||
}
|
||||
|
||||
if (!msgHdrAndReindexTime) {
|
||||
this._log.debug("reached end of folder");
|
||||
if (this._currentFolderToIndex) {
|
||||
this._currentFolderToIndex = null;
|
||||
}
|
||||
} else {
|
||||
this._queueMessage(msgHdrAndReindexTime[0], msgHdrAndReindexTime[1]);
|
||||
}
|
||||
|
||||
// Restart the timer, and call ourselves
|
||||
this._cancelTimer();
|
||||
this._timer.initWithCallback(
|
||||
this._wrapContinueSweep,
|
||||
this._msgHdrsToIndex.length > 1 ? 5000 : 1000,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* A simple wrapper to make "this" be right for _continueSweep
|
||||
*/
|
||||
_wrapContinueSweep() {
|
||||
SearchIntegration._continueSweep();
|
||||
},
|
||||
|
||||
/**
|
||||
* Observer implementation. Consists of
|
||||
* - idle observer; starts running through folders when it receives an "idle"
|
||||
* notification, and cancels any timers when it receives a "back" notification
|
||||
* - msg displayed observer, queues the message if necessary
|
||||
* - pref observer, to see if the preference has been poked
|
||||
*/
|
||||
observe(aSubject, aTopic, aData) {
|
||||
if (aTopic == "idle") {
|
||||
this._log.debug("Idle detected, continuing sweep");
|
||||
this._continueSweep();
|
||||
} else if (aTopic == "back") {
|
||||
this._log.debug("Non-idle, so suspending sweep");
|
||||
this._cancelTimer();
|
||||
} else if (aTopic == "MsgMsgDisplayed") {
|
||||
this._log.debug("topic = " + aTopic + " uri = " + aData);
|
||||
const msgHdr = this._messenger.msgHdrFromURI(aData);
|
||||
const reindexTime = this._getLastReindexTime(msgHdr.folder);
|
||||
this._log.debug("Reindex time for this folder is " + reindexTime);
|
||||
if (msgHdr.getUint32Property(this._hdrIndexedProperty) < reindexTime) {
|
||||
// Check if the file exists. If it does, then assume indexing to be
|
||||
// complete for this file
|
||||
if (this._getSupportFile(msgHdr).exists()) {
|
||||
this._log.debug(
|
||||
"Message time not set but file exists; setting " +
|
||||
" time to " +
|
||||
reindexTime
|
||||
);
|
||||
msgHdr.setUint32Property(this._hdrIndexedProperty, reindexTime);
|
||||
} else {
|
||||
this._queueMessage(msgHdr, reindexTime);
|
||||
}
|
||||
}
|
||||
} else if (aTopic == "nsPref:changed" && aData == "enable") {
|
||||
const prefEnabled = this.prefEnabled;
|
||||
// Search integration turned on
|
||||
if (prefEnabled && this.register()) {
|
||||
this.enabled = true;
|
||||
} else if (!prefEnabled && this.deregister()) {
|
||||
// Search integration turned off
|
||||
this.enabled = false;
|
||||
} else {
|
||||
// The call to register or deregister has failed.
|
||||
// This is a hack to handle this case
|
||||
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback(
|
||||
function () {
|
||||
SearchIntegration._handleRegisterFailure(!prefEnabled);
|
||||
},
|
||||
200,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Handle failure to register or deregister
|
||||
_handleRegisterFailure(enabled) {
|
||||
// Remove ourselves from the observer list, flip the pref,
|
||||
// and add ourselves back
|
||||
this._prefBranch.removeObserver("enable", this);
|
||||
this.prefEnabled = enabled;
|
||||
this._prefBranch.addObserver("enable", this);
|
||||
},
|
||||
|
||||
/**
|
||||
* This object gets notifications for new/moved/copied/deleted messages/folders
|
||||
*/
|
||||
_msgFolderListener: {
|
||||
msgAdded(aMsg) {
|
||||
SearchIntegration._log.info("in msgAdded");
|
||||
// The message already being there is an expected case
|
||||
const file = SearchIntegration._getSupportFile(aMsg);
|
||||
if (!file.exists()) {
|
||||
SearchIntegration._queueMessage(
|
||||
aMsg,
|
||||
SearchIntegration._getLastReindexTime(aMsg.folder)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
msgsDeleted(aMsgs) {
|
||||
SearchIntegration._log.info("in msgsDeleted");
|
||||
for (const msgHdr of aMsgs) {
|
||||
const file = SearchIntegration._getSupportFile(msgHdr);
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder) {
|
||||
SearchIntegration._log.info("in msgsMoveCopyCompleted, aMove = " + aMove);
|
||||
// Forget about copies if disabled
|
||||
if (!aMove && !this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const count = aSrcMsgs.length;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const srcFile = SearchIntegration._getSupportFile(aSrcMsgs[i]);
|
||||
if (srcFile && srcFile.exists()) {
|
||||
const destFile =
|
||||
SearchIntegration._getSearchPathForFolder(aDestFolder);
|
||||
destFile.leafName = destFile.leafName + ".mozmsgs";
|
||||
if (!destFile.exists()) {
|
||||
try {
|
||||
// create the directory, if it doesn't exist
|
||||
destFile.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
} catch (ex) {
|
||||
SearchIntegration._log.warn(ex);
|
||||
}
|
||||
}
|
||||
SearchIntegration._log.debug("dst file path = " + destFile.path);
|
||||
SearchIntegration._log.debug("src file path = " + srcFile.path);
|
||||
// We're not going to copy in case we're not in active mode
|
||||
if (destFile.exists()) {
|
||||
if (aMove) {
|
||||
srcFile.moveTo(destFile, "");
|
||||
} else {
|
||||
srcFile.copyTo(destFile, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
folderDeleted(aFolder) {
|
||||
SearchIntegration._log.info(
|
||||
"in folderDeleted, folder name = " + aFolder.prettyName
|
||||
);
|
||||
const srcFile = SearchIntegration._getSearchPathForFolder(aFolder);
|
||||
srcFile.leafName = srcFile.leafName + ".mozmsgs";
|
||||
if (srcFile.exists()) {
|
||||
srcFile.remove(true);
|
||||
}
|
||||
},
|
||||
|
||||
folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
|
||||
SearchIntegration._log.info(
|
||||
"in folderMoveCopyCompleted, aMove = " + aMove
|
||||
);
|
||||
|
||||
// Forget about copies if disabled
|
||||
if (!aMove && !this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const srcFile = SearchIntegration._getSearchPathForFolder(aSrcFolder);
|
||||
const destFile = SearchIntegration._getSearchPathForFolder(aDestFolder);
|
||||
srcFile.leafName = srcFile.leafName + ".mozmsgs";
|
||||
destFile.leafName += ".sbd";
|
||||
SearchIntegration._log.debug("src file path = " + srcFile.path);
|
||||
SearchIntegration._log.debug("dst file path = " + destFile.path);
|
||||
if (srcFile.exists()) {
|
||||
// We're not going to copy if we aren't in active mode
|
||||
if (aMove) {
|
||||
srcFile.moveTo(destFile, "");
|
||||
} else {
|
||||
srcFile.copyTo(destFile, "");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
folderRenamed(aOrigFolder, aNewFolder) {
|
||||
SearchIntegration._log.info(
|
||||
"in folderRenamed, aOrigFolder = " +
|
||||
aOrigFolder.prettyName +
|
||||
", aNewFolder = " +
|
||||
aNewFolder.prettyName
|
||||
);
|
||||
const srcFile = SearchIntegration._getSearchPathForFolder(aOrigFolder);
|
||||
srcFile.leafName = srcFile.leafName + ".mozmsgs";
|
||||
const destName = aNewFolder.name + ".mozmsgs";
|
||||
SearchIntegration._log.debug("src file path = " + srcFile.path);
|
||||
SearchIntegration._log.debug("dst name = " + destName);
|
||||
if (srcFile.exists()) {
|
||||
srcFile.moveTo(null, destName);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
* Support functions to queue/generate files
|
||||
*/
|
||||
_queueMessage(msgHdr, reindexTime) {
|
||||
if (this._msgHdrsToIndex.push([msgHdr, reindexTime]) == 1) {
|
||||
this._log.info("generating support file for id = " + msgHdr.messageId);
|
||||
this._streamListener.startStreaming(msgHdr, reindexTime);
|
||||
} else {
|
||||
this._log.info(
|
||||
"queueing support file generation for id = " + msgHdr.messageId
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle results from the command line. This method is the inverse of the
|
||||
* _getSupportFile method below.
|
||||
*
|
||||
* @param aFile the file passed in by the command line
|
||||
* @returns the nsIMsgDBHdr corresponding to the file passed in
|
||||
*/
|
||||
handleResult(aFile) {
|
||||
// The file path has two components -- the search path, which needs to be
|
||||
// converted into a folder, and the message ID.
|
||||
const searchPath = aFile.parent;
|
||||
// Strip off ".mozmsgs" from the end (8 characters)
|
||||
searchPath.leafName = searchPath.leafName.slice(0, -8);
|
||||
|
||||
const folder = this._getFolderForSearchPath(searchPath);
|
||||
|
||||
// Get rid of the file extension at the end (7 characters), and unescape
|
||||
const messageID = decodeURIComponent(aFile.leafName.slice(0, -7));
|
||||
|
||||
// Look for the message ID in the folder
|
||||
return folder.msgDatabase.getMsgHdrForMessageID(messageID);
|
||||
},
|
||||
|
||||
_getSupportFile(msgHdr) {
|
||||
const folder = msgHdr.folder;
|
||||
if (folder) {
|
||||
const messageId = encodeURIComponent(msgHdr.messageId);
|
||||
this._log.debug("encoded message id = " + messageId);
|
||||
const file = this._getSearchPathForFolder(folder);
|
||||
file.leafName = file.leafName + ".mozmsgs";
|
||||
file.appendRelativePath(messageId + this._fileExt);
|
||||
this._log.debug("getting support file path = " + file.path);
|
||||
return file;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Base to use for stream listeners, extended by the respective
|
||||
* implementations
|
||||
*/
|
||||
_streamListenerBase: {
|
||||
// Output file
|
||||
_outputFile: null,
|
||||
|
||||
// Stream to use to write to the output file
|
||||
__outputStream: null,
|
||||
set _outputStream(stream) {
|
||||
if (this.__outputStream) {
|
||||
this.__outputStream.close();
|
||||
}
|
||||
this.__outputStream = stream;
|
||||
},
|
||||
get _outputStream() {
|
||||
return this.__outputStream;
|
||||
},
|
||||
|
||||
// Reference to message header
|
||||
_msgHdr: null,
|
||||
|
||||
// Reindex time for this message header
|
||||
_reindexTime: null,
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
|
||||
|
||||
// "Finish" function, cleans up behind itself if unsuccessful
|
||||
_onDoneStreaming(successful) {
|
||||
this._outputStream = null;
|
||||
if (!successful && this._msgHdr) {
|
||||
const file = SearchIntegration._getSupportFile(this._msgHdr);
|
||||
if (file && file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
}
|
||||
// should we try to delete the file on disk in case not successful?
|
||||
SearchIntegration._msgHdrsToIndex.shift();
|
||||
|
||||
if (SearchIntegration._msgHdrsToIndex.length > 0) {
|
||||
const [msgHdr, reindexTime] = SearchIntegration._msgHdrsToIndex[0];
|
||||
this.startStreaming(msgHdr, reindexTime);
|
||||
}
|
||||
},
|
||||
|
||||
// "Start" function
|
||||
startStreaming(msgHdr, reindexTime) {
|
||||
try {
|
||||
const folder = msgHdr.folder;
|
||||
if (folder) {
|
||||
const messageId = encodeURIComponent(msgHdr.messageId);
|
||||
SearchIntegration._log.info(
|
||||
"generating support file, id = " + messageId
|
||||
);
|
||||
const file = SearchIntegration._getSearchPathForFolder(folder);
|
||||
|
||||
file.leafName = file.leafName + ".mozmsgs";
|
||||
SearchIntegration._log.debug("file leafname = " + file.leafName);
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
// create the directory, if it doesn't exist
|
||||
file.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
} catch (ex) {
|
||||
this._log.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
file.appendRelativePath(messageId + SearchIntegration._fileExt);
|
||||
SearchIntegration._log.debug("file path = " + file.path);
|
||||
file.create(0, PERM_FILE);
|
||||
const uri = folder.getUriForMsg(msgHdr);
|
||||
const msgService = MailServices.messageServiceFromURI(uri);
|
||||
this._msgHdr = msgHdr;
|
||||
this._outputFile = file;
|
||||
this._reindexTime = reindexTime;
|
||||
try {
|
||||
// XXX For now, try getting the messages from the server. This has
|
||||
// to be improved so that we don't generate any excess network
|
||||
// traffic
|
||||
msgService.streamMessage(uri, this, null, null, false, "", false);
|
||||
} catch (ex) {
|
||||
// This is an expected case, in case we're offline
|
||||
SearchIntegration._log.warn(
|
||||
"StreamMessage unsuccessful for id = " + messageId
|
||||
);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
SearchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Logging functionality, shamelessly ripped from gloda
|
||||
* If enabled, warnings and above are logged to the error console, while dump
|
||||
* gets everything
|
||||
*/
|
||||
_log: null,
|
||||
_initLogging() {
|
||||
this._log = console.createInstance({
|
||||
prefix: this._prefBase.slice(0, -1),
|
||||
maxLogLevel: "Warn",
|
||||
maxLogLevelPref: `${this._prefBase}loglevel`,
|
||||
});
|
||||
this._log.info("Logging initialized");
|
||||
},
|
||||
};
|
||||
|
||||
if (AppConstants.platform == "win") {
|
||||
const scope = { SearchSupport, SearchIntegration };
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://messenger/content/WinSearchIntegration.js",
|
||||
scope
|
||||
let instance = null;
|
||||
|
||||
// If the helper service isn't present, we weren't compiled with the needed
|
||||
// support.
|
||||
if (
|
||||
AppConstants.platform == "win" &&
|
||||
"@mozilla.org/mail/windows-search-helper;1" in Cc
|
||||
) {
|
||||
const { SearchIntegration } = ChromeUtils.importESModule(
|
||||
"resource:///modules/WinSearchIntegration.sys.mjs"
|
||||
);
|
||||
instance = new SearchIntegration();
|
||||
} else if (AppConstants.platform == "macosx") {
|
||||
const scope = { SearchSupport, SearchIntegration };
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://messenger/content/SpotlightIntegration.js",
|
||||
scope
|
||||
const { SearchIntegration } = ChromeUtils.importESModule(
|
||||
"resource:///modules/SpotlightIntegration.sys.mjs"
|
||||
);
|
||||
instance = new SearchIntegration();
|
||||
}
|
||||
|
||||
export { instance as SearchIntegration };
|
||||
|
|
|
@ -0,0 +1,973 @@
|
|||
/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/. */
|
||||
|
||||
import { MailServices } from "resource:///modules/MailServices.sys.mjs";
|
||||
|
||||
const PERM_DIRECTORY = 0o755;
|
||||
const PERM_FILE = 0o644;
|
||||
|
||||
/**
|
||||
* Common, useful functions for desktop search integration components.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export class SearchSupport {
|
||||
/**
|
||||
* The property in the database that indicates whether a message has been
|
||||
* indexed. Needs to be set by subclasses.
|
||||
*
|
||||
* @type {?string}
|
||||
*/
|
||||
_hdrIndexedProperty = null;
|
||||
|
||||
/**
|
||||
* The file extension to be used for support files. Needs to be set by
|
||||
* subclasses.
|
||||
*
|
||||
* @type {?string}
|
||||
*/
|
||||
_fileExt = null;
|
||||
|
||||
/**
|
||||
* The base for preferences that are stored. Needs to be set by subclasses.
|
||||
*
|
||||
* @type {?string}
|
||||
*/
|
||||
_prefBase = null;
|
||||
|
||||
/**
|
||||
* An nsIStreamListener to read message text. Needs to be set by subclasses.
|
||||
*
|
||||
* @type {?BaseStreamListener}
|
||||
*/
|
||||
_streamListener = null;
|
||||
|
||||
/**
|
||||
* URI of last folder indexed. Kept in sync with the pref.
|
||||
*
|
||||
* @type {?string}
|
||||
*/
|
||||
#lastFolderIndexedUri = null;
|
||||
set _lastFolderIndexedUri(uri) {
|
||||
this._prefBranch.setStringPref("lastFolderIndexedUri", uri);
|
||||
this.#lastFolderIndexedUri = uri;
|
||||
}
|
||||
get _lastFolderIndexedUri() {
|
||||
// If we don't know about it, get it from the pref branch
|
||||
if (this.#lastFolderIndexedUri === null) {
|
||||
this.#lastFolderIndexedUri = this._prefBranch.getStringPref(
|
||||
"lastFolderIndexedUri",
|
||||
""
|
||||
);
|
||||
}
|
||||
return this.#lastFolderIndexedUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue of message headers to index, along with reindex times for each header.
|
||||
*
|
||||
* @type {[nsIMsgDBHdr, integer][]}
|
||||
*/
|
||||
_msgHdrsToIndex = [];
|
||||
|
||||
/**
|
||||
* Messenger object, used primarily to get message URIs
|
||||
*
|
||||
* @type {?nsIMessenger}
|
||||
*/
|
||||
#messenger = null;
|
||||
get _messenger() {
|
||||
if (!this.#messenger) {
|
||||
this.#messenger = Cc["@mozilla.org/messenger;1"].createInstance(
|
||||
Ci.nsIMessenger
|
||||
);
|
||||
}
|
||||
return this.#messenger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The preferences branch to use.
|
||||
*
|
||||
* @type {?nsIPrefBranch}
|
||||
*/
|
||||
#prefBranch = null;
|
||||
get _prefBranch() {
|
||||
if (!this.#prefBranch) {
|
||||
this.#prefBranch = Services.prefs.getBranch(this._prefBase);
|
||||
}
|
||||
return this.#prefBranch;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is true, we won't show any UI because the OS doesn't have the
|
||||
* support we need.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
osVersionTooLow = false;
|
||||
|
||||
/**
|
||||
* If this is true, we'll show disabled UI, because while the OS does have
|
||||
* the support we need, not all the OS components we need are running.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
osComponentsNotRunning = false;
|
||||
|
||||
/**
|
||||
* Whether the preference is enabled. The module might be in a state where
|
||||
* the preference is on but "enabled" is false, so take care of that.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get prefEnabled() {
|
||||
// Don't cache the value
|
||||
return this._prefBranch.getBoolPref("enable");
|
||||
}
|
||||
set prefEnabled(aEnabled) {
|
||||
if (this.prefEnabled != aEnabled) {
|
||||
this._prefBranch.setBoolPref("enable", aEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the first run has occurred. This will be used to determine if
|
||||
* a dialog box needs to be displayed.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get firstRunDone() {
|
||||
// Don't cache this value either
|
||||
return this._prefBranch.getBoolPref("firstRunDone");
|
||||
}
|
||||
set firstRunDone(aAlwaysTrue) {
|
||||
this._prefBranch.setBoolPref("firstRunDone", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Last global reindex time, used to check if reindexing is required.
|
||||
* Kept in sync with the pref. Unix time in seconds.
|
||||
*
|
||||
* @type {integer}
|
||||
*/
|
||||
#globalReindexTime = null;
|
||||
set globalReindexTime(aTime) {
|
||||
this.#globalReindexTime = aTime;
|
||||
// Set the pref as well
|
||||
this._prefBranch.setCharPref("global_reindex_time", "" + aTime);
|
||||
}
|
||||
get globalReindexTime() {
|
||||
if (!this.#globalReindexTime) {
|
||||
// Try getting the time from the preferences
|
||||
try {
|
||||
this.#globalReindexTime = parseInt(
|
||||
this._prefBranch.getCharPref("global_reindex_time")
|
||||
);
|
||||
} catch (e) {
|
||||
// We don't have it defined, so set it (Unix time, in seconds)
|
||||
this.#globalReindexTime = parseInt(Date.now() / 1000);
|
||||
this._prefBranch.setCharPref(
|
||||
"global_reindex_time",
|
||||
"" + this.#globalReindexTime
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.#globalReindexTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount of time the user is idle before we (re)start an indexing sweep. In
|
||||
* seconds.
|
||||
*
|
||||
* @type {integer}
|
||||
*/
|
||||
_idleThresholdSecs = 30;
|
||||
|
||||
/**
|
||||
* Reference to timer object
|
||||
*
|
||||
* @type {?nsITimer}
|
||||
*/
|
||||
#timer = null;
|
||||
get _timer() {
|
||||
if (!this.#timer) {
|
||||
this.#timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
}
|
||||
return this.#timer;
|
||||
}
|
||||
|
||||
#cancelTimer() {
|
||||
try {
|
||||
this._timer.cancel();
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled status.
|
||||
*
|
||||
* When we're enabled, then we get notifications about every message or folder
|
||||
* operation, including "message displayed" operations which we bump up in
|
||||
* priority. We also have a background sweep which we do on idle.
|
||||
*
|
||||
* We aren't fully disabled when we're "disabled", though. We still observe
|
||||
* message and folder moves and deletes, as we don't want to have support
|
||||
* files for non-existent messages.
|
||||
*
|
||||
* @type {?boolean}
|
||||
*/
|
||||
#enabled = null;
|
||||
set enabled(aEnable) {
|
||||
// Nothing to do if there's no change in state
|
||||
if (this.#enabled == aEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.info(
|
||||
"Enabled status changing from " + this.#enabled + " to " + aEnable
|
||||
);
|
||||
|
||||
this._removeObservers();
|
||||
|
||||
if (aEnable) {
|
||||
// This stuff we always need to do.
|
||||
// This code pre-dates msgsClassified.
|
||||
// Some events intentionally omitted.
|
||||
MailServices.mfn.addListener(
|
||||
this._msgFolderListener,
|
||||
MailServices.mfn.msgAdded |
|
||||
MailServices.mfn.msgsDeleted |
|
||||
MailServices.mfn.msgsMoveCopyCompleted |
|
||||
MailServices.mfn.folderDeleted |
|
||||
MailServices.mfn.folderMoveCopyCompleted |
|
||||
MailServices.mfn.folderRenamed
|
||||
);
|
||||
Services.obs.addObserver(this, "MsgMsgDisplayed");
|
||||
const idleService = Cc[
|
||||
"@mozilla.org/widget/useridleservice;1"
|
||||
].getService(Ci.nsIUserIdleService);
|
||||
idleService.addIdleObserver(this, this._idleThresholdSecs);
|
||||
} else {
|
||||
// We want to observe moves, deletes and renames in case we're disabled
|
||||
// If we don't, we'll have no idea the support files exist later
|
||||
MailServices.mfn.addListener(
|
||||
this._msgFolderListener,
|
||||
MailServices.mfn.msgsMoveCopyCompleted |
|
||||
MailServices.mfn.msgsDeleted |
|
||||
// folderAdded intentionally omitted
|
||||
MailServices.mfn.folderDeleted |
|
||||
MailServices.mfn.folderMoveCopyCompleted |
|
||||
MailServices.mfn.folderRenamed
|
||||
);
|
||||
}
|
||||
|
||||
this.#enabled = aEnable;
|
||||
}
|
||||
get enabled() {
|
||||
return this.#enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove whatever observers are present. This is done while switching states
|
||||
*/
|
||||
_removeObservers() {
|
||||
if (this.enabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MailServices.mfn.removeListener(this._msgFolderListener);
|
||||
|
||||
if (this.enabled) {
|
||||
Services.obs.removeObserver(this, "MsgMsgDisplayed");
|
||||
const idleService = Cc[
|
||||
"@mozilla.org/widget/useridleservice;1"
|
||||
].getService(Ci.nsIUserIdleService);
|
||||
idleService.removeIdleObserver(this, this._idleThresholdSecs);
|
||||
|
||||
// in case there's a background sweep going on
|
||||
this.#cancelTimer();
|
||||
}
|
||||
// We don't need to do anything extra if we're disabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Init function -- this should be called from the component's init function
|
||||
*
|
||||
* @param {boolean} enabled
|
||||
*/
|
||||
_initSupport(enabled) {
|
||||
this._log.info(
|
||||
"Search integration running in " +
|
||||
(enabled ? "active" : "backoff") +
|
||||
" mode"
|
||||
);
|
||||
this.enabled = enabled;
|
||||
|
||||
// Set up a pref observer
|
||||
this._prefBranch.addObserver("enable", this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current folder being indexed.
|
||||
*
|
||||
* @type {?nsIMsgFolder}
|
||||
*/
|
||||
#currentFolderToIndex = null;
|
||||
|
||||
/**
|
||||
* For the current folder being indexed, an enumerator for all the headers in
|
||||
* the folder.
|
||||
*
|
||||
* @type {?nsIMsgEnumerator}
|
||||
*/
|
||||
#headerEnumerator = null;
|
||||
|
||||
/*
|
||||
* These functions are to index already existing messages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generator to look for the next folder to index, and return it
|
||||
*
|
||||
* This first looks for folders that have their corresponding search results
|
||||
* folders missing. If it finds such a folder first, it'll yield return that
|
||||
* folder.
|
||||
*
|
||||
* Next, it looks for the next folder after the lastFolderIndexedUri. If it is
|
||||
* in such a folder, it'll yield return that folder, then set the
|
||||
* lastFolderIndexedUrl to the URI of that folder.
|
||||
*
|
||||
* It resets lastFolderIndexedUri to an empty string, then yield returns null
|
||||
* once iteration across all folders is complete.
|
||||
*
|
||||
* @yields {?nsIMsgFolder}
|
||||
*/
|
||||
*_foldersToIndexGenerator() {
|
||||
// Stores whether we're after the last folder indexed or before that --
|
||||
// if the last folder indexed is empty, this needs to be true initially
|
||||
let afterLastFolderIndexed = this._lastFolderIndexedUri.length == 0;
|
||||
|
||||
for (const server of MailServices.accounts.allServers) {
|
||||
this._log.debug(
|
||||
"in find next folder, lastFolderIndexedUri = " +
|
||||
this._lastFolderIndexedUri
|
||||
);
|
||||
|
||||
for (var folder of server.rootFolder.descendants) {
|
||||
const searchPath = this._getSearchPathForFolder(folder);
|
||||
searchPath.leafName = searchPath.leafName + ".mozmsgs";
|
||||
// If after the last folder indexed, definitely index this
|
||||
if (afterLastFolderIndexed) {
|
||||
// Create the folder if it doesn't exist, so that we don't hit the
|
||||
// condition below later
|
||||
if (!searchPath.exists()) {
|
||||
searchPath.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
}
|
||||
|
||||
yield folder;
|
||||
// We're back after yielding -- set the last folder indexed
|
||||
this._lastFolderIndexedUri = folder.URI;
|
||||
} else {
|
||||
// If a folder's entire corresponding search results folder is
|
||||
// missing, we need to index it, and force a reindex of all the
|
||||
// messages in it
|
||||
if (!searchPath.exists()) {
|
||||
this._log.debug(
|
||||
"using folder " +
|
||||
folder.URI +
|
||||
" because " +
|
||||
"corresponding search folder does not exist"
|
||||
);
|
||||
// Create the folder, so that next time we're checking we don't hit
|
||||
// this
|
||||
searchPath.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
folder.setStringProperty(
|
||||
this._hdrIndexedProperty,
|
||||
"" + Date.now() / 1000
|
||||
);
|
||||
yield folder;
|
||||
} else if (this._pathNeedsReindexing(searchPath)) {
|
||||
// folder may need reindexing for other reasons
|
||||
folder.setStringProperty(
|
||||
this._hdrIndexedProperty,
|
||||
"" + Date.now() / 1000
|
||||
);
|
||||
yield folder;
|
||||
}
|
||||
|
||||
// Even if we yielded above, check if this is the last folder
|
||||
// indexed
|
||||
if (this._lastFolderIndexedUri == folder.URI) {
|
||||
afterLastFolderIndexed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We're done with one iteration of all the folders; time to reset the
|
||||
// lastFolderIndexedUri
|
||||
this._lastFolderIndexedUri = "";
|
||||
yield null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {?Generator<nsIMsgFolder, undefined, void>}
|
||||
*/
|
||||
#foldersToIndex = null;
|
||||
get _foldersToIndex() {
|
||||
if (!this.#foldersToIndex) {
|
||||
this.#foldersToIndex = this._foldersToIndexGenerator();
|
||||
}
|
||||
return this.#foldersToIndex;
|
||||
}
|
||||
|
||||
_findNextHdrToIndex() {
|
||||
try {
|
||||
const reindexTime = this._getLastReindexTime(this.#currentFolderToIndex);
|
||||
this._log.debug("Reindex time for this folder is " + reindexTime);
|
||||
if (!this.#headerEnumerator) {
|
||||
// we need to create search terms for messages to index
|
||||
const searchSession = Cc[
|
||||
"@mozilla.org/messenger/searchSession;1"
|
||||
].createInstance(Ci.nsIMsgSearchSession);
|
||||
const searchTerms = [];
|
||||
|
||||
searchSession.addScopeTerm(
|
||||
Ci.nsMsgSearchScope.offlineMail,
|
||||
this.#currentFolderToIndex
|
||||
);
|
||||
// first term: (_hdrIndexProperty < reindexTime)
|
||||
const searchTerm = searchSession.createTerm();
|
||||
searchTerm.booleanAnd = false; // actually don't care here
|
||||
searchTerm.attrib = Ci.nsMsgSearchAttrib.Uint32HdrProperty;
|
||||
searchTerm.op = Ci.nsMsgSearchOp.IsLessThan;
|
||||
const value = searchTerm.value;
|
||||
value.attrib = searchTerm.attrib;
|
||||
searchTerm.hdrProperty = this._hdrIndexedProperty;
|
||||
value.status = reindexTime;
|
||||
searchTerm.value = value;
|
||||
searchTerms.push(searchTerm);
|
||||
this.#headerEnumerator =
|
||||
this.#currentFolderToIndex.msgDatabase.getFilterEnumerator(
|
||||
searchTerms
|
||||
);
|
||||
}
|
||||
|
||||
// iterate over the folder finding the next message to index
|
||||
for (const msgHdr of this.#headerEnumerator) {
|
||||
// Check if the file exists. If it does, then assume indexing to be
|
||||
// complete for this file
|
||||
if (this._getSupportFile(msgHdr).exists()) {
|
||||
this._log.debug(
|
||||
"Message time not set but file exists; setting " +
|
||||
"time to " +
|
||||
reindexTime
|
||||
);
|
||||
msgHdr.setUint32Property(this._hdrIndexedProperty, reindexTime);
|
||||
} else {
|
||||
return [msgHdr, reindexTime];
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
this._log.debug("Error while finding next header: " + ex);
|
||||
}
|
||||
|
||||
// If we couldn't find any headers to index, null out the enumerator
|
||||
this.#headerEnumerator = null;
|
||||
if (!(this.#currentFolderToIndex.flags & Ci.nsMsgFolderFlags.Inbox)) {
|
||||
this.#currentFolderToIndex.msgDatabase = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last reindex time for this folder. This will be whichever's
|
||||
* greater, the global reindex time or the folder reindex time
|
||||
*
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getLastReindexTime() {
|
||||
let reindexTime = this.globalReindexTime;
|
||||
|
||||
// Check if this folder has a separate string property set
|
||||
let folderReindexTime;
|
||||
try {
|
||||
folderReindexTime = this.#currentFolderToIndex.getStringProperty(
|
||||
this._hdrIndexedProperty
|
||||
);
|
||||
} catch (e) {
|
||||
folderReindexTime = "";
|
||||
}
|
||||
|
||||
if (folderReindexTime.length > 0) {
|
||||
const folderReindexTimeInt = parseInt(folderReindexTime);
|
||||
if (folderReindexTimeInt > reindexTime) {
|
||||
reindexTime = folderReindexTimeInt;
|
||||
}
|
||||
}
|
||||
return reindexTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether background indexing has been completed
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
#backgroundIndexingDone = false;
|
||||
|
||||
/**
|
||||
* The main background sweeping function. It first looks for a folder to
|
||||
* start or continue indexing in, then for a header. If it can't find anything
|
||||
* to index, it resets the last folder indexed URI so that the sweep can
|
||||
* be restarted
|
||||
*/
|
||||
_continueSweep() {
|
||||
let msgHdrAndReindexTime = null;
|
||||
|
||||
if (this.#backgroundIndexingDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find the current folder we're working on
|
||||
if (!this.#currentFolderToIndex) {
|
||||
this.#currentFolderToIndex = this._foldersToIndex.next().value;
|
||||
}
|
||||
|
||||
// we'd like to index more than one message on each timer fire,
|
||||
// but since streaming is async, it's hard to know how long
|
||||
// it's going to take to stream any particular message.
|
||||
if (this.#currentFolderToIndex) {
|
||||
msgHdrAndReindexTime = this._findNextHdrToIndex();
|
||||
} else {
|
||||
// We've cycled through all the folders. We should take a break
|
||||
// from indexing of existing messages.
|
||||
this.#backgroundIndexingDone = true;
|
||||
}
|
||||
|
||||
if (!msgHdrAndReindexTime) {
|
||||
this._log.debug("reached end of folder");
|
||||
if (this.#currentFolderToIndex) {
|
||||
this.#currentFolderToIndex = null;
|
||||
}
|
||||
} else {
|
||||
this._queueMessage(msgHdrAndReindexTime[0], msgHdrAndReindexTime[1]);
|
||||
}
|
||||
|
||||
// Restart the timer, and call ourselves
|
||||
this.#cancelTimer();
|
||||
this._timer.initWithCallback(
|
||||
this.#wrapContinueSweep,
|
||||
this._msgHdrsToIndex.length > 1 ? 5000 : 1000,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple wrapper to make "this" be right for _continueSweep
|
||||
*/
|
||||
#wrapContinueSweep = () => {
|
||||
this._continueSweep();
|
||||
};
|
||||
|
||||
/**
|
||||
* Observer implementation. Consists of
|
||||
* - idle observer; starts running through folders when it receives an "idle"
|
||||
* notification, and cancels any timers when it receives a "back" notification
|
||||
* - msg displayed observer, queues the message if necessary
|
||||
* - pref observer, to see if the preference has been poked
|
||||
*/
|
||||
observe(aSubject, aTopic, aData) {
|
||||
if (aTopic == "idle") {
|
||||
this._log.debug("Idle detected, continuing sweep");
|
||||
this._continueSweep();
|
||||
} else if (aTopic == "back") {
|
||||
this._log.debug("Non-idle, so suspending sweep");
|
||||
this.#cancelTimer();
|
||||
} else if (aTopic == "MsgMsgDisplayed") {
|
||||
this._log.debug("topic = " + aTopic + " uri = " + aData);
|
||||
const msgHdr = this._messenger.msgHdrFromURI(aData);
|
||||
const reindexTime = this._getLastReindexTime(msgHdr.folder);
|
||||
this._log.debug("Reindex time for this folder is " + reindexTime);
|
||||
if (msgHdr.getUint32Property(this._hdrIndexedProperty) < reindexTime) {
|
||||
// Check if the file exists. If it does, then assume indexing to be
|
||||
// complete for this file
|
||||
if (this._getSupportFile(msgHdr).exists()) {
|
||||
this._log.debug(
|
||||
"Message time not set but file exists; setting " +
|
||||
" time to " +
|
||||
reindexTime
|
||||
);
|
||||
msgHdr.setUint32Property(this._hdrIndexedProperty, reindexTime);
|
||||
} else {
|
||||
this._queueMessage(msgHdr, reindexTime);
|
||||
}
|
||||
}
|
||||
} else if (aTopic == "nsPref:changed" && aData == "enable") {
|
||||
const prefEnabled = this.prefEnabled;
|
||||
// Search integration turned on
|
||||
if (prefEnabled && this.register()) {
|
||||
this.enabled = true;
|
||||
} else if (!prefEnabled && this.deregister()) {
|
||||
// Search integration turned off
|
||||
this.enabled = false;
|
||||
} else {
|
||||
// The call to register or deregister has failed.
|
||||
// This is a hack to handle this case
|
||||
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback(
|
||||
() => {
|
||||
this._handleRegisterFailure(!prefEnabled);
|
||||
},
|
||||
200,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failure to register or deregister
|
||||
*
|
||||
* @param {boolean} enabled
|
||||
*/
|
||||
_handleRegisterFailure(enabled) {
|
||||
// Remove ourselves from the observer list, flip the pref,
|
||||
// and add ourselves back
|
||||
this._prefBranch.removeObserver("enable", this);
|
||||
this.prefEnabled = enabled;
|
||||
this._prefBranch.addObserver("enable", this);
|
||||
}
|
||||
|
||||
/**
|
||||
* This object gets notifications for new/moved/copied/deleted messages/folders.
|
||||
*
|
||||
* @implements {nsIMsgFolderListener}
|
||||
*/
|
||||
_msgFolderListener = {
|
||||
_searchIntegration: this,
|
||||
|
||||
msgAdded(aMsg) {
|
||||
this._searchIntegration._log.info("in msgAdded");
|
||||
// The message already being there is an expected case
|
||||
const file = this._searchIntegration._getSupportFile(aMsg);
|
||||
if (!file.exists()) {
|
||||
this._searchIntegration._queueMessage(
|
||||
aMsg,
|
||||
this._searchIntegration._getLastReindexTime(aMsg.folder)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
msgsDeleted(aMsgs) {
|
||||
this._searchIntegration._log.info("in msgsDeleted");
|
||||
for (const msgHdr of aMsgs) {
|
||||
const file = this._searchIntegration._getSupportFile(msgHdr);
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder) {
|
||||
this._searchIntegration._log.info(
|
||||
"in msgsMoveCopyCompleted, aMove = " + aMove
|
||||
);
|
||||
// Forget about copies if disabled
|
||||
if (!aMove && !this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const count = aSrcMsgs.length;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const srcFile = this._searchIntegration._getSupportFile(aSrcMsgs[i]);
|
||||
if (srcFile && srcFile.exists()) {
|
||||
const destFile =
|
||||
this._searchIntegration._getSearchPathForFolder(aDestFolder);
|
||||
destFile.leafName = destFile.leafName + ".mozmsgs";
|
||||
if (!destFile.exists()) {
|
||||
try {
|
||||
// create the directory, if it doesn't exist
|
||||
destFile.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
} catch (ex) {
|
||||
this._searchIntegration._log.warn(ex);
|
||||
}
|
||||
}
|
||||
this._searchIntegration._log.debug(
|
||||
"dst file path = " + destFile.path
|
||||
);
|
||||
this._searchIntegration._log.debug("src file path = " + srcFile.path);
|
||||
// We're not going to copy in case we're not in active mode
|
||||
if (destFile.exists()) {
|
||||
if (aMove) {
|
||||
srcFile.moveTo(destFile, "");
|
||||
} else {
|
||||
srcFile.copyTo(destFile, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
folderDeleted(aFolder) {
|
||||
this._searchIntegration._log.info(
|
||||
"in folderDeleted, folder name = " + aFolder.prettyName
|
||||
);
|
||||
const srcFile = this._searchIntegration._getSearchPathForFolder(aFolder);
|
||||
srcFile.leafName = srcFile.leafName + ".mozmsgs";
|
||||
if (srcFile.exists()) {
|
||||
srcFile.remove(true);
|
||||
}
|
||||
},
|
||||
|
||||
folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
|
||||
this._searchIntegration._log.info(
|
||||
"in folderMoveCopyCompleted, aMove = " + aMove
|
||||
);
|
||||
|
||||
// Forget about copies if disabled
|
||||
if (!aMove && !this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const srcFile =
|
||||
this._searchIntegration._getSearchPathForFolder(aSrcFolder);
|
||||
const destFile =
|
||||
this._searchIntegration._getSearchPathForFolder(aDestFolder);
|
||||
srcFile.leafName = srcFile.leafName + ".mozmsgs";
|
||||
destFile.leafName += ".sbd";
|
||||
this._searchIntegration._log.debug("src file path = " + srcFile.path);
|
||||
this._searchIntegration._log.debug("dst file path = " + destFile.path);
|
||||
if (srcFile.exists()) {
|
||||
// We're not going to copy if we aren't in active mode
|
||||
if (aMove) {
|
||||
srcFile.moveTo(destFile, "");
|
||||
} else {
|
||||
srcFile.copyTo(destFile, "");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
folderRenamed(aOrigFolder, aNewFolder) {
|
||||
this._searchIntegration._log.info(
|
||||
"in folderRenamed, aOrigFolder = " +
|
||||
aOrigFolder.prettyName +
|
||||
", aNewFolder = " +
|
||||
aNewFolder.prettyName
|
||||
);
|
||||
const srcFile =
|
||||
this._searchIntegration._getSearchPathForFolder(aOrigFolder);
|
||||
srcFile.leafName = srcFile.leafName + ".mozmsgs";
|
||||
const destName = aNewFolder.name + ".mozmsgs";
|
||||
this._searchIntegration._log.debug("src file path = " + srcFile.path);
|
||||
this._searchIntegration._log.debug("dst name = " + destName);
|
||||
if (srcFile.exists()) {
|
||||
srcFile.moveTo(null, destName);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Support functions to queue/generate files.
|
||||
*
|
||||
* @param {nsIMsgDBHdr} msgHdr
|
||||
* @param {integer} reindexTime
|
||||
*/
|
||||
_queueMessage(msgHdr, reindexTime) {
|
||||
if (this._msgHdrsToIndex.push([msgHdr, reindexTime]) == 1) {
|
||||
this._log.info("generating support file for id = " + msgHdr.messageId);
|
||||
this._streamListener.startStreaming(msgHdr, reindexTime);
|
||||
} else {
|
||||
this._log.info(
|
||||
"queueing support file generation for id = " + msgHdr.messageId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle results from the command line. This method is the inverse of the
|
||||
* _getSupportFile method below.
|
||||
*
|
||||
* @param {nsIFile} aFile the file passed in by the command line
|
||||
* @returns {nsIMsgDBHdr} the nsIMsgDBHdr corresponding to the file passed in
|
||||
*/
|
||||
handleResult(aFile) {
|
||||
// The file path has two components -- the search path, which needs to be
|
||||
// converted into a folder, and the message ID.
|
||||
const searchPath = aFile.parent;
|
||||
// Strip off ".mozmsgs" from the end (8 characters)
|
||||
searchPath.leafName = searchPath.leafName.slice(0, -8);
|
||||
|
||||
const folder = this._getFolderForSearchPath(searchPath);
|
||||
|
||||
// Get rid of the file extension at the end (7 characters), and unescape
|
||||
const messageID = decodeURIComponent(aFile.leafName.slice(0, -7));
|
||||
|
||||
// Look for the message ID in the folder
|
||||
return folder.msgDatabase.getMsgHdrForMessageID(messageID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {nsIMsgDBHdr} msgHdr
|
||||
* @returns {?nsIFile}
|
||||
*/
|
||||
_getSupportFile(msgHdr) {
|
||||
const folder = msgHdr.folder;
|
||||
if (folder) {
|
||||
const messageId = encodeURIComponent(msgHdr.messageId);
|
||||
this._log.debug("encoded message id = " + messageId);
|
||||
const file = this._getSearchPathForFolder(folder);
|
||||
file.leafName = file.leafName + ".mozmsgs";
|
||||
file.appendRelativePath(messageId + this._fileExt);
|
||||
this._log.debug("getting support file path = " + file.path);
|
||||
return file;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging functionality, shamelessly ripped from gloda.
|
||||
* If enabled, warnings and above are logged to the error console, while dump
|
||||
* gets everything.
|
||||
*
|
||||
* @type {ConsoleInstance}
|
||||
*/
|
||||
_log = null;
|
||||
_initLogging() {
|
||||
this._log = console.createInstance({
|
||||
prefix: this._prefBase.slice(0, -1),
|
||||
maxLogLevel: "Warn",
|
||||
maxLogLevelPref: `${this._prefBase}loglevel`,
|
||||
});
|
||||
this._log.info("Logging initialized");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base to use for stream listeners, extended by the respective
|
||||
* implementations. Is missing the onDataAvailable implementation.
|
||||
*
|
||||
* @implements {nsIStreamListener}
|
||||
*/
|
||||
export class StreamListenerBase {
|
||||
QueryInterface = ChromeUtils.generateQI(["nsIStreamListener"]);
|
||||
|
||||
/**
|
||||
* @param {SearchSupport} parent - Instance that owns this stream listener.
|
||||
*/
|
||||
constructor(parent) {
|
||||
this._searchIntegration = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output file.
|
||||
*
|
||||
* @type {?nsIFile}
|
||||
*/
|
||||
_outputFile = null;
|
||||
|
||||
/**
|
||||
* Stream to use to write to the output file.
|
||||
*
|
||||
* @type {?nsIConverterOutputStream}
|
||||
*/
|
||||
#outputStream = null;
|
||||
set _outputStream(stream) {
|
||||
if (this.#outputStream) {
|
||||
this.#outputStream.close();
|
||||
}
|
||||
this.#outputStream = stream;
|
||||
}
|
||||
get _outputStream() {
|
||||
return this.#outputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to message header
|
||||
*
|
||||
* @type {?nsIMsgDBHdr}
|
||||
*/
|
||||
_msgHdr = null;
|
||||
|
||||
/**
|
||||
* Reindex time for this message header
|
||||
*
|
||||
* @type {?integer}
|
||||
*/
|
||||
_reindexTime = null;
|
||||
|
||||
/**
|
||||
* "Finish" function, cleans up behind itself if unsuccessful
|
||||
*
|
||||
* @param {boolean} successful
|
||||
*/
|
||||
_onDoneStreaming(successful) {
|
||||
this._outputStream = null;
|
||||
if (!successful && this._msgHdr) {
|
||||
const file = this._searchIntegration._getSupportFile(this._msgHdr);
|
||||
if (file && file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
}
|
||||
// should we try to delete the file on disk in case not successful?
|
||||
this._searchIntegration._msgHdrsToIndex.shift();
|
||||
|
||||
if (this._searchIntegration._msgHdrsToIndex.length > 0) {
|
||||
const [msgHdr, reindexTime] = this._searchIntegration._msgHdrsToIndex[0];
|
||||
this.startStreaming(msgHdr, reindexTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Start" function
|
||||
*
|
||||
* @param {nsIMsgDBHdr} msgHdr
|
||||
* @param {integer} reindexTime
|
||||
*/
|
||||
startStreaming(msgHdr, reindexTime) {
|
||||
try {
|
||||
const folder = msgHdr.folder;
|
||||
if (folder) {
|
||||
const messageId = encodeURIComponent(msgHdr.messageId);
|
||||
this._searchIntegration._log.info(
|
||||
"generating support file, id = " + messageId
|
||||
);
|
||||
const file = this._searchIntegration._getSearchPathForFolder(folder);
|
||||
|
||||
file.leafName = file.leafName + ".mozmsgs";
|
||||
this._searchIntegration._log.debug("file leafname = " + file.leafName);
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
// create the directory, if it doesn't exist
|
||||
file.create(Ci.nsIFile.DIRECTORY_TYPE, PERM_DIRECTORY);
|
||||
} catch (ex) {
|
||||
this._log.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
file.appendRelativePath(messageId + this._searchIntegration._fileExt);
|
||||
this._searchIntegration._log.debug("file path = " + file.path);
|
||||
file.create(0, PERM_FILE);
|
||||
const uri = folder.getUriForMsg(msgHdr);
|
||||
const msgService = MailServices.messageServiceFromURI(uri);
|
||||
this._msgHdr = msgHdr;
|
||||
this._outputFile = file;
|
||||
this._reindexTime = reindexTime;
|
||||
try {
|
||||
// XXX For now, try getting the messages from the server. This has
|
||||
// to be improved so that we don't generate any excess network
|
||||
// traffic
|
||||
msgService.streamMessage(uri, this, null, null, false, "", false);
|
||||
} catch (ex) {
|
||||
// This is an expected case, in case we're offline
|
||||
this._searchIntegration._log.warn(
|
||||
"StreamMessage unsuccessful for id = " + messageId
|
||||
);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
this._searchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/. */
|
||||
|
||||
import { MailUtils } from "resource:///modules/MailUtils.sys.mjs";
|
||||
import {
|
||||
SearchSupport,
|
||||
StreamListenerBase,
|
||||
} from "resource:///modules/SearchSupport.sys.mjs";
|
||||
|
||||
const gFileHeader =
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.\ncom/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>';
|
||||
|
||||
class SpotlightStreamListener extends StreamListenerBase {
|
||||
/**
|
||||
* Buffer to store the message
|
||||
*/
|
||||
#message = null;
|
||||
|
||||
/**
|
||||
* Encodes reserved XML characters
|
||||
*/
|
||||
#xmlEscapeString(s) {
|
||||
return s.replace(/[<>&]/g, function (s) {
|
||||
switch (s) {
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
default:
|
||||
throw new Error("Unexpected match");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onStartRequest() {
|
||||
try {
|
||||
const outputFileStream = Cc[
|
||||
"@mozilla.org/network/file-output-stream;1"
|
||||
].createInstance(Ci.nsIFileOutputStream);
|
||||
outputFileStream.init(this._outputFile, -1, -1, 0);
|
||||
this._outputStream = Cc[
|
||||
"@mozilla.org/intl/converter-output-stream;1"
|
||||
].createInstance(Ci.nsIConverterOutputStream);
|
||||
this._outputStream.init(outputFileStream, "UTF-8");
|
||||
|
||||
this._outputStream.writeString(gFileHeader);
|
||||
this._outputStream.writeString("<key>kMDItemLastUsedDate</key><string>");
|
||||
// need to write the date as a string
|
||||
const curTimeStr = new Date().toLocaleString();
|
||||
this._outputStream.writeString(curTimeStr);
|
||||
|
||||
// need to write the subject in utf8 as the title
|
||||
this._outputStream.writeString(
|
||||
"</string>\n<key>kMDItemTitle</key>\n<string>"
|
||||
);
|
||||
|
||||
const escapedSubject = this.#xmlEscapeString(
|
||||
this._msgHdr.mime2DecodedSubject
|
||||
);
|
||||
this._outputStream.writeString(escapedSubject);
|
||||
|
||||
this._outputStream.writeString(
|
||||
"</string>\n<key>kMDItemDisplayName</key>\n<string>"
|
||||
);
|
||||
this._outputStream.writeString(escapedSubject);
|
||||
|
||||
this._outputStream.writeString(
|
||||
"</string>\n<key>kMDItemTextContent</key>\n<string>"
|
||||
);
|
||||
this._outputStream.writeString(
|
||||
this.#xmlEscapeString(this._msgHdr.mime2DecodedAuthor)
|
||||
);
|
||||
this._outputStream.writeString(
|
||||
this.#xmlEscapeString(this._msgHdr.mime2DecodedRecipients)
|
||||
);
|
||||
|
||||
this._outputStream.writeString(escapedSubject);
|
||||
this._outputStream.writeString(" ");
|
||||
} catch (ex) {
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
}
|
||||
|
||||
onStopRequest() {
|
||||
try {
|
||||
// we want to write out the from, to, cc, and subject headers into the
|
||||
// Text Content value, so they'll be indexed.
|
||||
const stringStream = Cc[
|
||||
"@mozilla.org/io/string-input-stream;1"
|
||||
].createInstance(Ci.nsIStringInputStream);
|
||||
stringStream.setData(this.#message, this.#message.length);
|
||||
const folder = this._msgHdr.folder;
|
||||
let text = folder.getMsgTextFromStream(
|
||||
stringStream,
|
||||
this._msgHdr.charset,
|
||||
20000,
|
||||
20000,
|
||||
false,
|
||||
true,
|
||||
{}
|
||||
);
|
||||
text = this.#xmlEscapeString(text);
|
||||
this._searchIntegration._log.debug(
|
||||
"escaped text = *****************\n" + text
|
||||
);
|
||||
this._outputStream.writeString(text);
|
||||
// close out the content, dict, and plist
|
||||
this._outputStream.writeString("</string>\n</dict>\n</plist>\n");
|
||||
|
||||
this._msgHdr.setUint32Property(
|
||||
this._searchIntegration._hdrIndexedProperty,
|
||||
this._reindexTime
|
||||
);
|
||||
folder.msgDatabase.commit(Ci.nsMsgDBCommitType.kLargeCommit);
|
||||
|
||||
this._message = "";
|
||||
} catch (ex) {
|
||||
this._searchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
return;
|
||||
}
|
||||
this._onDoneStreaming(true);
|
||||
}
|
||||
|
||||
onDataAvailable(request, inputStream, offset, count) {
|
||||
try {
|
||||
const inStream = Cc[
|
||||
"@mozilla.org/scriptableinputstream;1"
|
||||
].createInstance(Ci.nsIScriptableInputStream);
|
||||
inStream.init(inputStream);
|
||||
|
||||
// It is necessary to read in data from the input stream
|
||||
const inData = inStream.read(count);
|
||||
|
||||
// ignore stuff after the first 20K or so
|
||||
if (this.#message && this.#message.length > 20000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#message += inData;
|
||||
} catch (ex) {
|
||||
this._searchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchIntegration extends SearchSupport {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._initLogging();
|
||||
|
||||
const enabled = this._prefBranch.getBoolPref("enable", false);
|
||||
if (enabled) {
|
||||
this._log.info("Initializing Spotlight integration");
|
||||
}
|
||||
this._initSupport(enabled);
|
||||
}
|
||||
|
||||
// The property of the header and (sometimes) folders that's used to check
|
||||
// if a message is indexed
|
||||
_hdrIndexedProperty = "spotlight_reindex_time";
|
||||
|
||||
// The file extension that is used for support files of this component
|
||||
_fileExt = ".mozeml";
|
||||
|
||||
// The Spotlight pref base
|
||||
_prefBase = "mail.spotlight.";
|
||||
|
||||
// The user's profile dir, which we'll cache and use a lot for path clean-up
|
||||
get _profileDir() {
|
||||
delete this._profileDir;
|
||||
return (this._profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile));
|
||||
}
|
||||
|
||||
get _metadataDir() {
|
||||
delete this._metadataDir;
|
||||
const metadataDir = Services.dirsvc.get("Home", Ci.nsIFile);
|
||||
metadataDir.append("Library");
|
||||
metadataDir.append("Caches");
|
||||
metadataDir.append("Metadata");
|
||||
metadataDir.append("Thunderbird");
|
||||
return (this._metadataDir = metadataDir);
|
||||
}
|
||||
|
||||
// Spotlight won't index files in the profile dir, but will use ~/Library/Caches/Metadata
|
||||
_getSearchPathForFolder(aFolder) {
|
||||
// Swap the metadata dir for the profile dir prefix in the folder's path
|
||||
const folderPath = aFolder.filePath.path;
|
||||
const fixedPath = folderPath.replace(
|
||||
this._profileDir.path,
|
||||
this._metadataDir.path
|
||||
);
|
||||
const searchPath = Cc["@mozilla.org/file/local;1"].createInstance(
|
||||
Ci.nsIFile
|
||||
);
|
||||
searchPath.initWithPath(fixedPath);
|
||||
return searchPath;
|
||||
}
|
||||
|
||||
// Replace ~/Library/Caches/Metadata with the profile directory, then convert
|
||||
_getFolderForSearchPath(aPath) {
|
||||
const folderPath = aPath.path.replace(
|
||||
this._metadataDir.path,
|
||||
this._profileDir.path
|
||||
);
|
||||
const folderFile = Cc["@mozilla.org/file/local;1"].createInstance(
|
||||
Ci.nsIFile
|
||||
);
|
||||
folderFile.initWithPath(folderPath);
|
||||
return MailUtils.getFolderForFileInProfile(folderFile);
|
||||
}
|
||||
|
||||
_pathNeedsReindexing(aPath) {
|
||||
// We used to set permissions incorrectly (see bug 670566).
|
||||
const PERM_DIRECTORY = parseInt("0755", 8);
|
||||
if (aPath.permissions != PERM_DIRECTORY) {
|
||||
aPath.permissions = PERM_DIRECTORY;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* These two functions won't do anything, as Spotlight integration is handled
|
||||
* using Info.plist files
|
||||
*/
|
||||
register() {
|
||||
return true;
|
||||
}
|
||||
|
||||
deregister() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The stream listener to read messages
|
||||
_streamListener = new SpotlightStreamListener(this);
|
||||
}
|
|
@ -3,33 +3,25 @@
|
|||
* 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 file gets loaded through Services.scriptloader.loadSubScript.
|
||||
// by SearchIntegration.sys.mjs.
|
||||
import { MailUtils } from "resource:///modules/MailUtils.sys.mjs";
|
||||
import {
|
||||
SearchSupport,
|
||||
StreamListenerBase,
|
||||
} from "resource:///modules/SearchSupport.sys.mjs";
|
||||
|
||||
/* globals SearchIntegration, SearchSupport */ // from SearchIntegration.sys.mjs
|
||||
|
||||
var { MailUtils } = ChromeUtils.importESModule(
|
||||
"resource:///modules/MailUtils.sys.mjs"
|
||||
);
|
||||
|
||||
var MSG_DB_LARGE_COMMIT = 1;
|
||||
var CRLF = "\r\n";
|
||||
const MSG_DB_LARGE_COMMIT = 1;
|
||||
const CRLF = "\r\n";
|
||||
|
||||
/**
|
||||
* Required to access the 64-bit registry, even though we're probably a 32-bit
|
||||
* program
|
||||
*/
|
||||
var ACCESS_WOW64_64KEY = 0x0100;
|
||||
|
||||
/**
|
||||
* The contract ID for the helper service.
|
||||
*/
|
||||
var WINSEARCHHELPER_CONTRACTID = "@mozilla.org/mail/windows-search-helper;1";
|
||||
const ACCESS_WOW64_64KEY = 0x0100;
|
||||
|
||||
/**
|
||||
* All the registry keys required for integration
|
||||
*/
|
||||
var gRegKeys = [
|
||||
const gRegKeys = [
|
||||
// This is the property handler
|
||||
{
|
||||
root: Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
|
||||
|
@ -66,96 +58,127 @@ var gRegKeys = [
|
|||
},
|
||||
];
|
||||
|
||||
class WinSearchStreamListener extends StreamListenerBase {
|
||||
/**
|
||||
* Buffer to store the message
|
||||
*/
|
||||
#message = "";
|
||||
|
||||
onStartRequest() {
|
||||
try {
|
||||
const outputFileStream = Cc[
|
||||
"@mozilla.org/network/file-output-stream;1"
|
||||
].createInstance(Ci.nsIFileOutputStream);
|
||||
outputFileStream.init(this._outputFile, -1, -1, 0);
|
||||
this._outputStream = Cc[
|
||||
"@mozilla.org/intl/converter-output-stream;1"
|
||||
].createInstance(Ci.nsIConverterOutputStream);
|
||||
this._outputStream.init(outputFileStream, "UTF-8");
|
||||
} catch (ex) {
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
}
|
||||
|
||||
onStopRequest() {
|
||||
try {
|
||||
// XXX Once the JS emitter gets checked in, this code should probably be
|
||||
// switched over to use that
|
||||
// Decode using getMsgTextFromStream
|
||||
const stringStream = Cc[
|
||||
"@mozilla.org/io/string-input-stream;1"
|
||||
].createInstance(Ci.nsIStringInputStream);
|
||||
stringStream.setData(this.#message, this.#message.length);
|
||||
const contentType = {};
|
||||
const folder = this._msgHdr.folder;
|
||||
const text = folder.getMsgTextFromStream(
|
||||
stringStream,
|
||||
this._msgHdr.charset,
|
||||
65536,
|
||||
50000,
|
||||
false,
|
||||
false,
|
||||
contentType
|
||||
);
|
||||
|
||||
// To get the Received header, we need to parse the message headers.
|
||||
// We only need the first header, which contains the latest received
|
||||
// date
|
||||
const headers = this.#message.split(/\r\n\r\n|\r\r|\n\n/, 1)[0];
|
||||
const mimeHeaders = Cc[
|
||||
"@mozilla.org/messenger/mimeheaders;1"
|
||||
].createInstance(Ci.nsIMimeHeaders);
|
||||
mimeHeaders.initialize(headers);
|
||||
const receivedHeader = mimeHeaders.extractHeader("Received", false);
|
||||
|
||||
this._outputStream.writeString("From: " + this._msgHdr.author + CRLF);
|
||||
// If we're a newsgroup, then add the name of the folder as the
|
||||
// newsgroups header
|
||||
if (folder instanceof Ci.nsIMsgNewsFolder) {
|
||||
this._outputStream.writeString("Newsgroups: " + folder.name + CRLF);
|
||||
} else {
|
||||
this._outputStream.writeString("To: " + this._msgHdr.recipients + CRLF);
|
||||
}
|
||||
this._outputStream.writeString("CC: " + this._msgHdr.ccList + CRLF);
|
||||
this._outputStream.writeString("Subject: " + this._msgHdr.subject + CRLF);
|
||||
if (receivedHeader) {
|
||||
this._outputStream.writeString("Received: " + receivedHeader + CRLF);
|
||||
}
|
||||
this._outputStream.writeString(
|
||||
"Date: " + new Date(this._msgHdr.date / 1000).toUTCString() + CRLF
|
||||
);
|
||||
this._outputStream.writeString(
|
||||
"Content-Type: " + contentType.value + "; charset=utf-8" + CRLF + CRLF
|
||||
);
|
||||
|
||||
this._outputStream.writeString(text + CRLF + CRLF);
|
||||
|
||||
this._msgHdr.setUint32Property(
|
||||
this._searchIntegration._hdrIndexedProperty,
|
||||
this._reindexTime
|
||||
);
|
||||
folder.msgDatabase.commit(MSG_DB_LARGE_COMMIT);
|
||||
|
||||
this.#message = "";
|
||||
this._searchIntegration._log.info("Successfully written file");
|
||||
} catch (ex) {
|
||||
this._searchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
return;
|
||||
}
|
||||
this._onDoneStreaming(true);
|
||||
}
|
||||
|
||||
onDataAvailable(request, inputStream, offset, count) {
|
||||
try {
|
||||
const inStream = Cc[
|
||||
"@mozilla.org/scriptableinputstream;1"
|
||||
].createInstance(Ci.nsIScriptableInputStream);
|
||||
inStream.init(inputStream);
|
||||
|
||||
// It is necessary to read in data from the input stream
|
||||
const inData = inStream.read(count);
|
||||
|
||||
// Ignore stuff after the first 50K or so
|
||||
if (this.#message && this.#message.length > 50000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#message += inData;
|
||||
} catch (ex) {
|
||||
this._searchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @namespace Windows Search-specific desktop search integration functionality
|
||||
*/
|
||||
// eslint-disable-next-line no-global-assign
|
||||
SearchIntegration = {
|
||||
__proto__: SearchSupport,
|
||||
export class SearchIntegration extends SearchSupport {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// The property of the header and (sometimes) folders that's used to check
|
||||
// if a message is indexed
|
||||
_hdrIndexedProperty: "winsearch_reindex_time",
|
||||
|
||||
// The file extension that is used for support files of this component
|
||||
_fileExt: ".wdseml",
|
||||
|
||||
// The Windows Search pref base
|
||||
_prefBase: "mail.winsearch.",
|
||||
|
||||
// Helper (native) component
|
||||
__winSearchHelper: null,
|
||||
get _winSearchHelper() {
|
||||
if (!this.__winSearchHelper) {
|
||||
this.__winSearchHelper = Cc[WINSEARCHHELPER_CONTRACTID].getService(
|
||||
Ci.nsIMailWinSearchHelper
|
||||
);
|
||||
}
|
||||
return this.__winSearchHelper;
|
||||
},
|
||||
|
||||
// Whether the folders are already in the crawl scope
|
||||
get _foldersInCrawlScope() {
|
||||
return this._winSearchHelper.foldersInCrawlScope;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether all the required registry keys are present
|
||||
* We'll be optimistic here and assume that once the registry keys have been
|
||||
* added, they won't be removed, at least while Thunderbird is open
|
||||
*/
|
||||
__regKeysPresent: false,
|
||||
get _regKeysPresent() {
|
||||
if (!this.__regKeysPresent) {
|
||||
for (let i = 0; i < gRegKeys.length; i++) {
|
||||
const regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
|
||||
Ci.nsIWindowsRegKey
|
||||
);
|
||||
try {
|
||||
regKey.open(
|
||||
gRegKeys[i].root,
|
||||
gRegKeys[i].key,
|
||||
regKey.ACCESS_READ | ACCESS_WOW64_64KEY
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
const valuePresent =
|
||||
regKey.hasValue(gRegKeys[i].name) &&
|
||||
regKey.readStringValue(gRegKeys[i].name) == gRegKeys[i].value;
|
||||
regKey.close();
|
||||
if (!valuePresent) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.__regKeysPresent = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// Use the folder's path (i.e., in profile dir) as is
|
||||
_getSearchPathForFolder(aFolder) {
|
||||
return aFolder.filePath;
|
||||
},
|
||||
|
||||
// Use the search path as is
|
||||
_getFolderForSearchPath(aDir) {
|
||||
return MailUtils.getFolderForFileInProfile(aDir);
|
||||
},
|
||||
|
||||
_pathNeedsReindexing() {
|
||||
// only needed on MacOSX (see bug 670566).
|
||||
return false;
|
||||
},
|
||||
|
||||
_init() {
|
||||
this._initLogging();
|
||||
// If the helper service isn't present, we weren't compiled with the needed
|
||||
// support. Mark ourselves null and return
|
||||
if (!(WINSEARCHHELPER_CONTRACTID in Cc)) {
|
||||
SearchIntegration = null; // eslint-disable-line no-global-assign
|
||||
return;
|
||||
}
|
||||
|
||||
// The search module is currently only enabled on Vista and above,
|
||||
// and the app can only be installed on Windows 7 and above.
|
||||
|
@ -179,7 +202,82 @@ SearchIntegration = {
|
|||
this._log.info("Initializing Windows Search integration");
|
||||
}
|
||||
this._initSupport(enabled);
|
||||
},
|
||||
}
|
||||
|
||||
// The property of the header and (sometimes) folders that's used to check
|
||||
// if a message is indexed
|
||||
_hdrIndexedProperty = "winsearch_reindex_time";
|
||||
|
||||
// The file extension that is used for support files of this component
|
||||
_fileExt = ".wdseml";
|
||||
|
||||
// The Windows Search pref base
|
||||
_prefBase = "mail.winsearch.";
|
||||
|
||||
// Helper (native) component
|
||||
#winSearchHelper = null;
|
||||
get _winSearchHelper() {
|
||||
if (!this.#winSearchHelper) {
|
||||
this.#winSearchHelper = Cc[
|
||||
"@mozilla.org/mail/windows-search-helper;1"
|
||||
].getService(Ci.nsIMailWinSearchHelper);
|
||||
}
|
||||
return this.#winSearchHelper;
|
||||
}
|
||||
|
||||
// Whether the folders are already in the crawl scope
|
||||
get _foldersInCrawlScope() {
|
||||
return this._winSearchHelper.foldersInCrawlScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether all the required registry keys are present
|
||||
* We'll be optimistic here and assume that once the registry keys have been
|
||||
* added, they won't be removed, at least while Thunderbird is open
|
||||
*/
|
||||
#regKeysPresent = false;
|
||||
get _regKeysPresent() {
|
||||
if (!this.#regKeysPresent) {
|
||||
for (let i = 0; i < gRegKeys.length; i++) {
|
||||
const regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
|
||||
Ci.nsIWindowsRegKey
|
||||
);
|
||||
try {
|
||||
regKey.open(
|
||||
gRegKeys[i].root,
|
||||
gRegKeys[i].key,
|
||||
regKey.ACCESS_READ | ACCESS_WOW64_64KEY
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
const valuePresent =
|
||||
regKey.hasValue(gRegKeys[i].name) &&
|
||||
regKey.readStringValue(gRegKeys[i].name) == gRegKeys[i].value;
|
||||
regKey.close();
|
||||
if (!valuePresent) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.#regKeysPresent = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use the folder's path (i.e., in profile dir) as is
|
||||
_getSearchPathForFolder(aFolder) {
|
||||
return aFolder.filePath;
|
||||
}
|
||||
|
||||
// Use the search path as is
|
||||
_getFolderForSearchPath(aDir) {
|
||||
return MailUtils.getFolderForFileInProfile(aDir);
|
||||
}
|
||||
|
||||
_pathNeedsReindexing() {
|
||||
// only needed on MacOSX (see bug 670566).
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add necessary hooks to Windows
|
||||
|
@ -209,7 +307,7 @@ SearchIntegration = {
|
|||
this._winSearchHelper.setFANCIBit(profD, false, true);
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove integration from Windows. The only thing removed is the directory
|
||||
|
@ -226,125 +324,8 @@ SearchIntegration = {
|
|||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
// The stream listener to read messages
|
||||
_streamListener: {
|
||||
__proto__: SearchSupport._streamListenerBase,
|
||||
|
||||
// Buffer to store the message
|
||||
_message: "",
|
||||
|
||||
onStartRequest() {
|
||||
try {
|
||||
const outputFileStream = Cc[
|
||||
"@mozilla.org/network/file-output-stream;1"
|
||||
].createInstance(Ci.nsIFileOutputStream);
|
||||
outputFileStream.init(this._outputFile, -1, -1, 0);
|
||||
this._outputStream = Cc[
|
||||
"@mozilla.org/intl/converter-output-stream;1"
|
||||
].createInstance(Ci.nsIConverterOutputStream);
|
||||
this._outputStream.init(outputFileStream, "UTF-8");
|
||||
} catch (ex) {
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
},
|
||||
|
||||
onStopRequest() {
|
||||
try {
|
||||
// XXX Once the JS emitter gets checked in, this code should probably be
|
||||
// switched over to use that
|
||||
// Decode using getMsgTextFromStream
|
||||
const stringStream = Cc[
|
||||
"@mozilla.org/io/string-input-stream;1"
|
||||
].createInstance(Ci.nsIStringInputStream);
|
||||
stringStream.setData(this._message, this._message.length);
|
||||
const contentType = {};
|
||||
const folder = this._msgHdr.folder;
|
||||
const text = folder.getMsgTextFromStream(
|
||||
stringStream,
|
||||
this._msgHdr.charset,
|
||||
65536,
|
||||
50000,
|
||||
false,
|
||||
false,
|
||||
contentType
|
||||
);
|
||||
|
||||
// To get the Received header, we need to parse the message headers.
|
||||
// We only need the first header, which contains the latest received
|
||||
// date
|
||||
const headers = this._message.split(/\r\n\r\n|\r\r|\n\n/, 1)[0];
|
||||
const mimeHeaders = Cc[
|
||||
"@mozilla.org/messenger/mimeheaders;1"
|
||||
].createInstance(Ci.nsIMimeHeaders);
|
||||
mimeHeaders.initialize(headers);
|
||||
const receivedHeader = mimeHeaders.extractHeader("Received", false);
|
||||
|
||||
this._outputStream.writeString("From: " + this._msgHdr.author + CRLF);
|
||||
// If we're a newsgroup, then add the name of the folder as the
|
||||
// newsgroups header
|
||||
if (folder instanceof Ci.nsIMsgNewsFolder) {
|
||||
this._outputStream.writeString("Newsgroups: " + folder.name + CRLF);
|
||||
} else {
|
||||
this._outputStream.writeString(
|
||||
"To: " + this._msgHdr.recipients + CRLF
|
||||
);
|
||||
}
|
||||
this._outputStream.writeString("CC: " + this._msgHdr.ccList + CRLF);
|
||||
this._outputStream.writeString(
|
||||
"Subject: " + this._msgHdr.subject + CRLF
|
||||
);
|
||||
if (receivedHeader) {
|
||||
this._outputStream.writeString("Received: " + receivedHeader + CRLF);
|
||||
}
|
||||
this._outputStream.writeString(
|
||||
"Date: " + new Date(this._msgHdr.date / 1000).toUTCString() + CRLF
|
||||
);
|
||||
this._outputStream.writeString(
|
||||
"Content-Type: " + contentType.value + "; charset=utf-8" + CRLF + CRLF
|
||||
);
|
||||
|
||||
this._outputStream.writeString(text + CRLF + CRLF);
|
||||
|
||||
this._msgHdr.setUint32Property(
|
||||
SearchIntegration._hdrIndexedProperty,
|
||||
this._reindexTime
|
||||
);
|
||||
folder.msgDatabase.commit(MSG_DB_LARGE_COMMIT);
|
||||
|
||||
this._message = "";
|
||||
SearchIntegration._log.info("Successfully written file");
|
||||
} catch (ex) {
|
||||
SearchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
return;
|
||||
}
|
||||
this._onDoneStreaming(true);
|
||||
},
|
||||
|
||||
onDataAvailable(request, inputStream, offset, count) {
|
||||
try {
|
||||
const inStream = Cc[
|
||||
"@mozilla.org/scriptableinputstream;1"
|
||||
].createInstance(Ci.nsIScriptableInputStream);
|
||||
inStream.init(inputStream);
|
||||
|
||||
// It is necessary to read in data from the input stream
|
||||
const inData = inStream.read(count);
|
||||
|
||||
// Ignore stuff after the first 50K or so
|
||||
if (this._message && this._message.length > 50000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._message += inData;
|
||||
} catch (ex) {
|
||||
SearchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
SearchIntegration._init();
|
||||
_streamListener = new WinSearchStreamListener(this);
|
||||
}
|
|
@ -1,248 +0,0 @@
|
|||
/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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 file gets loaded through Services.scriptloader.loadSubScript.
|
||||
// by SearchIntegration.sys.mjs.
|
||||
|
||||
/* globals SearchIntegration, SearchSupport */ // from SearchIntegration.sys.mjs
|
||||
|
||||
var { MailUtils } = ChromeUtils.importESModule(
|
||||
"resource:///modules/MailUtils.sys.mjs"
|
||||
);
|
||||
|
||||
var MSG_DB_LARGE_COMMIT = 1;
|
||||
var gFileHeader =
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.\ncom/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>';
|
||||
|
||||
// eslint-disable-next-line no-global-assign
|
||||
SearchIntegration = {
|
||||
__proto__: SearchSupport,
|
||||
|
||||
// The property of the header and (sometimes) folders that's used to check
|
||||
// if a message is indexed
|
||||
_hdrIndexedProperty: "spotlight_reindex_time",
|
||||
|
||||
// The file extension that is used for support files of this component
|
||||
_fileExt: ".mozeml",
|
||||
|
||||
// The Spotlight pref base
|
||||
_prefBase: "mail.spotlight.",
|
||||
|
||||
// The user's profile dir, which we'll cache and use a lot for path clean-up
|
||||
get _profileDir() {
|
||||
delete this._profileDir;
|
||||
return (this._profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile));
|
||||
},
|
||||
|
||||
get _metadataDir() {
|
||||
delete this._metadataDir;
|
||||
const metadataDir = Services.dirsvc.get("Home", Ci.nsIFile);
|
||||
metadataDir.append("Library");
|
||||
metadataDir.append("Caches");
|
||||
metadataDir.append("Metadata");
|
||||
metadataDir.append("Thunderbird");
|
||||
return (this._metadataDir = metadataDir);
|
||||
},
|
||||
|
||||
// Spotlight won't index files in the profile dir, but will use ~/Library/Caches/Metadata
|
||||
_getSearchPathForFolder(aFolder) {
|
||||
// Swap the metadata dir for the profile dir prefix in the folder's path
|
||||
const folderPath = aFolder.filePath.path;
|
||||
const fixedPath = folderPath.replace(
|
||||
this._profileDir.path,
|
||||
this._metadataDir.path
|
||||
);
|
||||
const searchPath = Cc["@mozilla.org/file/local;1"].createInstance(
|
||||
Ci.nsIFile
|
||||
);
|
||||
searchPath.initWithPath(fixedPath);
|
||||
return searchPath;
|
||||
},
|
||||
|
||||
// Replace ~/Library/Caches/Metadata with the profile directory, then convert
|
||||
_getFolderForSearchPath(aPath) {
|
||||
const folderPath = aPath.path.replace(
|
||||
this._metadataDir.path,
|
||||
this._profileDir.path
|
||||
);
|
||||
const folderFile = Cc["@mozilla.org/file/local;1"].createInstance(
|
||||
Ci.nsIFile
|
||||
);
|
||||
folderFile.initWithPath(folderPath);
|
||||
return MailUtils.getFolderForFileInProfile(folderFile);
|
||||
},
|
||||
|
||||
_pathNeedsReindexing(aPath) {
|
||||
// We used to set permissions incorrectly (see bug 670566).
|
||||
const PERM_DIRECTORY = parseInt("0755", 8);
|
||||
if (aPath.permissions != PERM_DIRECTORY) {
|
||||
aPath.permissions = PERM_DIRECTORY;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* These two functions won't do anything, as Spotlight integration is handled
|
||||
* using Info.plist files
|
||||
*/
|
||||
register() {
|
||||
return true;
|
||||
},
|
||||
|
||||
deregister() {
|
||||
return true;
|
||||
},
|
||||
|
||||
_init() {
|
||||
this._initLogging();
|
||||
|
||||
const enabled = this._prefBranch.getBoolPref("enable", false);
|
||||
if (enabled) {
|
||||
this._log.info("Initializing Spotlight integration");
|
||||
}
|
||||
this._initSupport(enabled);
|
||||
},
|
||||
|
||||
// The stream listener to read messages
|
||||
_streamListener: {
|
||||
__proto__: SearchSupport._streamListenerBase,
|
||||
|
||||
// Buffer to store the message
|
||||
_message: null,
|
||||
|
||||
// Encodes reserved XML characters
|
||||
_xmlEscapeString(s) {
|
||||
return s.replace(/[<>&]/g, function (s) {
|
||||
switch (s) {
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
default:
|
||||
throw new Error("Unexpected match");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onStartRequest() {
|
||||
try {
|
||||
const outputFileStream = Cc[
|
||||
"@mozilla.org/network/file-output-stream;1"
|
||||
].createInstance(Ci.nsIFileOutputStream);
|
||||
outputFileStream.init(this._outputFile, -1, -1, 0);
|
||||
this._outputStream = Cc[
|
||||
"@mozilla.org/intl/converter-output-stream;1"
|
||||
].createInstance(Ci.nsIConverterOutputStream);
|
||||
this._outputStream.init(outputFileStream, "UTF-8");
|
||||
|
||||
this._outputStream.writeString(gFileHeader);
|
||||
this._outputStream.writeString(
|
||||
"<key>kMDItemLastUsedDate</key><string>"
|
||||
);
|
||||
// need to write the date as a string
|
||||
const curTimeStr = new Date().toLocaleString();
|
||||
this._outputStream.writeString(curTimeStr);
|
||||
|
||||
// need to write the subject in utf8 as the title
|
||||
this._outputStream.writeString(
|
||||
"</string>\n<key>kMDItemTitle</key>\n<string>"
|
||||
);
|
||||
|
||||
const escapedSubject = this._xmlEscapeString(
|
||||
this._msgHdr.mime2DecodedSubject
|
||||
);
|
||||
this._outputStream.writeString(escapedSubject);
|
||||
|
||||
this._outputStream.writeString(
|
||||
"</string>\n<key>kMDItemDisplayName</key>\n<string>"
|
||||
);
|
||||
this._outputStream.writeString(escapedSubject);
|
||||
|
||||
this._outputStream.writeString(
|
||||
"</string>\n<key>kMDItemTextContent</key>\n<string>"
|
||||
);
|
||||
this._outputStream.writeString(
|
||||
this._xmlEscapeString(this._msgHdr.mime2DecodedAuthor)
|
||||
);
|
||||
this._outputStream.writeString(
|
||||
this._xmlEscapeString(this._msgHdr.mime2DecodedRecipients)
|
||||
);
|
||||
|
||||
this._outputStream.writeString(escapedSubject);
|
||||
this._outputStream.writeString(" ");
|
||||
} catch (ex) {
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
},
|
||||
|
||||
onStopRequest() {
|
||||
try {
|
||||
// we want to write out the from, to, cc, and subject headers into the
|
||||
// Text Content value, so they'll be indexed.
|
||||
const stringStream = Cc[
|
||||
"@mozilla.org/io/string-input-stream;1"
|
||||
].createInstance(Ci.nsIStringInputStream);
|
||||
stringStream.setData(this._message, this._message.length);
|
||||
const folder = this._msgHdr.folder;
|
||||
let text = folder.getMsgTextFromStream(
|
||||
stringStream,
|
||||
this._msgHdr.charset,
|
||||
20000,
|
||||
20000,
|
||||
false,
|
||||
true,
|
||||
{}
|
||||
);
|
||||
text = this._xmlEscapeString(text);
|
||||
SearchIntegration._log.debug(
|
||||
"escaped text = *****************\n" + text
|
||||
);
|
||||
this._outputStream.writeString(text);
|
||||
// close out the content, dict, and plist
|
||||
this._outputStream.writeString("</string>\n</dict>\n</plist>\n");
|
||||
|
||||
this._msgHdr.setUint32Property(
|
||||
SearchIntegration._hdrIndexedProperty,
|
||||
this._reindexTime
|
||||
);
|
||||
folder.msgDatabase.commit(MSG_DB_LARGE_COMMIT);
|
||||
|
||||
this._message = "";
|
||||
} catch (ex) {
|
||||
SearchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
return;
|
||||
}
|
||||
this._onDoneStreaming(true);
|
||||
},
|
||||
|
||||
onDataAvailable(request, inputStream, offset, count) {
|
||||
try {
|
||||
const inStream = Cc[
|
||||
"@mozilla.org/scriptableinputstream;1"
|
||||
].createInstance(Ci.nsIScriptableInputStream);
|
||||
inStream.init(inputStream);
|
||||
|
||||
// It is necessary to read in data from the input stream
|
||||
const inData = inStream.read(count);
|
||||
|
||||
// ignore stuff after the first 20K or so
|
||||
if (this._message && this._message.length > 20000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._message += inData;
|
||||
} catch (ex) {
|
||||
SearchIntegration._log.error(ex);
|
||||
this._onDoneStreaming(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
SearchIntegration._init();
|
|
@ -4,12 +4,6 @@
|
|||
|
||||
messenger.jar:
|
||||
search-extensions/ (extensions/**)
|
||||
#ifdef XP_MACOSX
|
||||
content/messenger/SpotlightIntegration.js (content/SpotlightIntegration.js)
|
||||
#endif
|
||||
#ifdef XP_WIN
|
||||
content/messenger/WinSearchIntegration.js (content/WinSearchIntegration.js)
|
||||
#endif
|
||||
|
||||
% resource search-plugins %searchplugins/ contentaccessible=yes
|
||||
% resource search-extensions %search-extensions/ contentaccessible=yes
|
||||
|
|
|
@ -14,8 +14,19 @@ DIRS += ["public"]
|
|||
|
||||
EXTRA_JS_MODULES += [
|
||||
"SearchIntegration.sys.mjs",
|
||||
"SearchSupport.sys.mjs",
|
||||
]
|
||||
|
||||
if CONFIG["OS_ARCH"] == "Darwin":
|
||||
EXTRA_JS_MODULES += [
|
||||
"SpotlightIntegration.sys.mjs",
|
||||
]
|
||||
|
||||
if CONFIG["OS_ARCH"] == "WINNT":
|
||||
EXTRA_JS_MODULES += [
|
||||
"WinSearchIntegration.sys.mjs",
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ["jar.mn"]
|
||||
|
||||
XPCOM_MANIFESTS += [
|
||||
|
|
Загрузка…
Ссылка в новой задаче