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:
Martin Giger 2024-05-08 13:35:18 +03:00
Родитель 3477c1c2df
Коммит ebc9d593fd
7 изменённых файлов: 1448 добавлений и 1338 удалений

Просмотреть файл

@ -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 "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
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 "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
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 += [