summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/search/content
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/search/content')
-rw-r--r--comm/mail/components/search/content/SpotlightIntegration.js240
-rw-r--r--comm/mail/components/search/content/WinSearchIntegration.js346
2 files changed, 586 insertions, 0 deletions
diff --git a/comm/mail/components/search/content/SpotlightIntegration.js b/comm/mail/components/search/content/SpotlightIntegration.js
new file mode 100644
index 0000000000..0757800ee6
--- /dev/null
+++ b/comm/mail/components/search/content/SpotlightIntegration.js
@@ -0,0 +1,240 @@
+/* -*- 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/. */
+
+// SearchIntegration.jsm
+/* globals SearchIntegration, SearchSupport, Services */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+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;
+ let 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
+ let folderPath = aFolder.filePath.path;
+ let fixedPath = folderPath.replace(
+ this._profileDir.path,
+ this._metadataDir.path
+ );
+ let 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) {
+ let folderPath = aPath.path.replace(
+ this._metadataDir.path,
+ this._profileDir.path
+ );
+ let 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();
+
+ let 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(request) {
+ try {
+ let 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
+ let 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>"
+ );
+
+ let 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(request, status) {
+ try {
+ // we want to write out the from, to, cc, and subject headers into the
+ // Text Content value, so they'll be indexed.
+ let stringStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ stringStream.setData(this._message, this._message.length);
+ let 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 {
+ let inStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ inStream.init(inputStream);
+
+ // It is necessary to read in data from the input stream
+ let 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();
diff --git a/comm/mail/components/search/content/WinSearchIntegration.js b/comm/mail/components/search/content/WinSearchIntegration.js
new file mode 100644
index 0000000000..2f4c51b0e3
--- /dev/null
+++ b/comm/mail/components/search/content/WinSearchIntegration.js
@@ -0,0 +1,346 @@
+/* -*- 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/. */
+
+// SearchIntegration.jsm
+/* globals SearchIntegration, SearchSupport, Services */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var MSG_DB_LARGE_COMMIT = 1;
+var 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";
+
+/**
+ * All the registry keys required for integration
+ */
+var gRegKeys = [
+ // This is the property handler
+ {
+ root: Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ key: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\PropertySystem\\PropertyHandlers\\.wdseml",
+ name: "",
+ value: "{5FA29220-36A1-40f9-89C6-F4B384B7642E}",
+ },
+ // These two are the association with the MIME IFilter
+ {
+ root: Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT,
+ key: ".wdseml",
+ name: "Content Type",
+ value: "message/rfc822",
+ },
+ {
+ root: Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT,
+ key: ".wdseml\\PersistentHandler",
+ name: "",
+ value: "{5645c8c4-e277-11cf-8fda-00aa00a14f93}",
+ },
+ // This is the association with the Windows mail preview handler
+ {
+ root: Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT,
+ key: ".wdseml\\shellex\\{8895B1C6-B41F-4C1C-A562-0D564250836F}",
+ name: "",
+ value: "{b9815375-5d7f-4ce2-9245-c9d4da436930}",
+ },
+ // This is the association made to display results under email
+ {
+ root: Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ key: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\KindMap",
+ name: ".wdseml",
+ value: "email;communication",
+ },
+];
+
+/**
+ * @namespace Windows Search-specific desktop search integration functionality
+ */
+// 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: "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++) {
+ let 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;
+ }
+ let 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(aPath) {
+ // 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.
+ this.osVersionTooLow = false;
+
+ let serviceRunning = false;
+ try {
+ serviceRunning = this._winSearchHelper.serviceRunning;
+ } catch (e) {}
+ // If the service isn't running, then we should stay in backoff mode
+ if (!serviceRunning) {
+ this._log.info("Windows Search service not running");
+ this.osComponentsNotRunning = true;
+ this._initSupport(false);
+ return;
+ }
+
+ let enabled = this.prefEnabled;
+
+ if (enabled) {
+ this._log.info("Initializing Windows Search integration");
+ }
+ this._initSupport(enabled);
+ },
+
+ /**
+ * Add necessary hooks to Windows
+ *
+ * @returns false if registration did not succeed, because the elevation
+ * request was denied
+ */
+ register() {
+ // If any of the two are not present, we need to elevate.
+ if (!this._foldersInCrawlScope || !this._regKeysPresent) {
+ try {
+ this._winSearchHelper.runSetup(true);
+ } catch (e) {
+ return false;
+ }
+ }
+
+ if (!this._winSearchHelper.isFileAssociationSet) {
+ try {
+ this._winSearchHelper.setFileAssociation();
+ } catch (e) {
+ this._log.warn("File association not set");
+ }
+ }
+ // Also set the FANCI bit to 0 for the profile directory
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ this._winSearchHelper.setFANCIBit(profD, false, true);
+
+ return true;
+ },
+
+ /**
+ * Remove integration from Windows. The only thing removed is the directory
+ * from the index list. This will ask for elevation.
+ *
+ * @returns false if deregistration did not succeed, because the elevation
+ * request was denied
+ */
+ deregister() {
+ try {
+ this._winSearchHelper.runSetup(false);
+ } catch (e) {
+ return false;
+ }
+
+ return true;
+ },
+
+ // The stream listener to read messages
+ _streamListener: {
+ __proto__: SearchSupport._streamListenerBase,
+
+ // Buffer to store the message
+ _message: "",
+
+ onStartRequest(request) {
+ try {
+ let 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(request, status) {
+ try {
+ // XXX Once the JS emitter gets checked in, this code should probably be
+ // switched over to use that
+ // Decode using getMsgTextFromStream
+ let stringStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ stringStream.setData(this._message, this._message.length);
+ let contentType = {};
+ let folder = this._msgHdr.folder;
+ let 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
+ let headers = this._message.split(/\r\n\r\n|\r\r|\n\n/, 1)[0];
+ let mimeHeaders = Cc[
+ "@mozilla.org/messenger/mimeheaders;1"
+ ].createInstance(Ci.nsIMimeHeaders);
+ mimeHeaders.initialize(headers);
+ let 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 {
+ let inStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ inStream.init(inputStream);
+
+ // It is necessary to read in data from the input stream
+ let 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();