summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources/MailTestUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/test/resources/MailTestUtils.jsm')
-rw-r--r--comm/mailnews/test/resources/MailTestUtils.jsm611
1 files changed, 611 insertions, 0 deletions
diff --git a/comm/mailnews/test/resources/MailTestUtils.jsm b/comm/mailnews/test/resources/MailTestUtils.jsm
new file mode 100644
index 0000000000..588e21521a
--- /dev/null
+++ b/comm/mailnews/test/resources/MailTestUtils.jsm
@@ -0,0 +1,611 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["mailTestUtils"];
+
+var { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// See Bug 903946
+function avoidUncaughtExceptionInExternalProtocolService() {
+ try {
+ Services.prefs.setCharPref(
+ "helpers.private_mime_types_file",
+ Services.prefs.getCharPref("helpers.global_mime_types_file")
+ );
+ } catch (ex) {}
+ try {
+ Services.prefs.setCharPref(
+ "helpers.private_mailcap_file",
+ Services.prefs.getCharPref("helpers.global_mailcap_file")
+ );
+ } catch (ex) {}
+}
+avoidUncaughtExceptionInExternalProtocolService();
+
+var mailTestUtils = {
+ // Loads a file to a string
+ // If aCharset is specified, treats the file as being of that charset
+ loadFileToString(aFile, aCharset) {
+ var data = "";
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(aFile, -1, 0, 0);
+
+ if (aCharset) {
+ var cstream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ cstream.init(fstream, aCharset, 4096, 0x0000);
+ let str = {};
+ while (cstream.readString(4096, str) != 0) {
+ data += str.value;
+ }
+
+ cstream.close();
+ } else {
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+
+ sstream.init(fstream);
+
+ let str = sstream.read(4096);
+ while (str.length > 0) {
+ data += str;
+ str = sstream.read(4096);
+ }
+
+ sstream.close();
+ }
+
+ fstream.close();
+
+ return data;
+ },
+
+ // Loads a message to a string
+ // If aCharset is specified, treats the file as being of that charset
+ loadMessageToString(aFolder, aMsgHdr, aCharset) {
+ var data = "";
+ let reusable = {};
+ let bytesLeft = aMsgHdr.messageSize;
+ let stream = aFolder.getMsgInputStream(aMsgHdr, reusable);
+ if (aCharset) {
+ let cstream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ cstream.init(stream, aCharset, 4096, 0x0000);
+ let str = {};
+ let bytesToRead = Math.min(bytesLeft, 4096);
+ while (cstream.readString(bytesToRead, str) != 0) {
+ data += str.value;
+ bytesLeft -= bytesToRead;
+ if (bytesLeft <= 0) {
+ break;
+ }
+ bytesToRead = Math.min(bytesLeft, 4096);
+ }
+ cstream.close();
+ } else {
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+
+ sstream.init(stream);
+
+ let bytesToRead = Math.min(bytesLeft, 4096);
+ var str = sstream.read(bytesToRead);
+ bytesLeft -= str.length;
+ while (str.length > 0) {
+ data += str;
+ if (bytesLeft <= 0) {
+ break;
+ }
+ bytesToRead = Math.min(bytesLeft, 4096);
+ str = sstream.read(bytesToRead);
+ bytesLeft -= str.length;
+ }
+ sstream.close();
+ }
+ stream.close();
+
+ return data;
+ },
+
+ // Loads a message to a UTF-16 string.
+ loadMessageToUTF16String(folder, msgHdr, charset) {
+ let str = this.loadMessageToString(folder, msgHdr, charset);
+ let arr = new Uint8Array(Array.from(str, x => x.charCodeAt(0)));
+ return new TextDecoder().decode(arr);
+ },
+
+ // Gets the first message header in a folder.
+ firstMsgHdr(folder) {
+ let enumerator = folder.msgDatabase.enumerateMessages();
+ let first = enumerator[Symbol.iterator]().next();
+ return first.done ? null : first.value;
+ },
+
+ // Gets message header number N (0 based index) in a folder.
+ getMsgHdrN(folder, n) {
+ let i = 0;
+ for (let next of folder.msgDatabase.enumerateMessages()) {
+ if (i == n) {
+ return next;
+ }
+ i++;
+ }
+ return null;
+ },
+
+ /**
+ * Returns the file system a particular file is on.
+ * Currently supported on Windows only.
+ *
+ * @param {nsIFile} aFile - The file to get the file system for.
+ * @returns {string} The file system a particular file is on, or 'null'
+ * if not on Windows.
+ */
+ get_file_system(aFile) {
+ if (!("@mozilla.org/windows-registry-key;1" in Cc)) {
+ dump("get_file_system() is supported on Windows only.\n");
+ return null;
+ }
+
+ // Win32 type and other constants.
+ const BOOL = ctypes.int32_t;
+ const MAX_PATH = 260;
+
+ let kernel32 = ctypes.open("kernel32.dll");
+
+ try {
+ // Returns the path of the volume a file is on.
+ let GetVolumePathName = kernel32.declare(
+ "GetVolumePathNameW",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ ctypes.char16_t.ptr, // in: lpszFileName
+ ctypes.char16_t.ptr, // out: lpszVolumePathName
+ ctypes.uint32_t // in: cchBufferLength
+ );
+
+ let filePath = aFile.path;
+ // The volume path should be at most 1 greater than than the length of the
+ // path -- add 1 for a trailing backslash if necessary, and 1 for the
+ // terminating null character. Note that the parentheses around the type are
+ // necessary for new to apply correctly.
+ let volumePath = new (ctypes.char16_t.array(filePath.length + 2))();
+
+ if (!GetVolumePathName(filePath, volumePath, volumePath.length)) {
+ throw new Error(
+ "Unable to get volume path for " +
+ filePath +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ // Returns information about the file system for the given volume path. We just need
+ // the file system name.
+ let GetVolumeInformation = kernel32.declare(
+ "GetVolumeInformationW",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ ctypes.char16_t.ptr, // in, optional: lpRootPathName
+ ctypes.char16_t.ptr, // out: lpVolumeNameBuffer
+ ctypes.uint32_t, // in: nVolumeNameSize
+ ctypes.uint32_t.ptr, // out, optional: lpVolumeSerialNumber
+ ctypes.uint32_t.ptr, // out, optional: lpMaximumComponentLength
+ ctypes.uint32_t.ptr, // out, optional: lpFileSystemFlags
+ ctypes.char16_t.ptr, // out: lpFileSystemNameBuffer
+ ctypes.uint32_t // in: nFileSystemNameSize
+ );
+
+ // We're only interested in the name of the file system.
+ let fsName = new (ctypes.char16_t.array(MAX_PATH + 1))();
+
+ if (
+ !GetVolumeInformation(
+ volumePath,
+ null,
+ 0,
+ null,
+ null,
+ null,
+ fsName,
+ fsName.length
+ )
+ ) {
+ throw new Error(
+ "Unable to get volume information for " +
+ volumePath.readString() +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ return fsName.readString();
+ } finally {
+ kernel32.close();
+ }
+ },
+
+ /**
+ * Try marking a region of a file as sparse, so that zeros don't consume
+ * significant amounts of disk space. This is a platform-dependent routine and
+ * is not supported on all platforms. The current status of this function is:
+ * - Windows: Supported, but only on NTFS volumes.
+ * - Mac: Not supported.
+ * - Linux: As long as you seek to a position before writing, happens automatically
+ * on most file systems, so this function is a no-op.
+ *
+ * @param {nsIFile} aFile - The file to mark as sparse.
+ * @param {integer} aRegionStart - The start position of the sparse region,
+ * in bytes.
+ * @param {integer} aRegionBytes - The number of bytes to mark as sparse.
+ * @returns {boolean} Whether the OS and file system supports marking files as
+ * sparse. If this is true, then the file has been marked as sparse.
+ * If this isfalse, then the underlying system doesn't support marking files as
+ * sparse. If an exception is thrown, then the system does support marking
+ * files as sparse, but an error occurred while doing so.
+ *
+ */
+ mark_file_region_sparse(aFile, aRegionStart, aRegionBytes) {
+ let fileSystem = this.get_file_system(aFile);
+ dump(
+ "[mark_file_region_sparse()] File system = " +
+ (fileSystem || "(unknown)") +
+ ", file region = at " +
+ this.toMiBString(aRegionStart) +
+ " for " +
+ this.toMiBString(aRegionBytes) +
+ "\n"
+ );
+
+ if ("@mozilla.org/windows-registry-key;1" in Cc) {
+ // On Windows, check whether the drive is NTFS. If it is, proceed.
+ // If it isn't, then bail out now, because in all probability it is
+ // FAT32, which doesn't support sparse files.
+ if (fileSystem != "NTFS") {
+ return false;
+ }
+
+ // Win32 type and other constants.
+ const BOOL = ctypes.int32_t;
+ const HANDLE = ctypes.voidptr_t;
+ // A BOOLEAN (= BYTE = unsigned char) is distinct from a BOOL.
+ // http://blogs.msdn.com/b/oldnewthing/archive/2004/12/22/329884.aspx
+ const BOOLEAN = ctypes.unsigned_char;
+ const FILE_SET_SPARSE_BUFFER = new ctypes.StructType(
+ "FILE_SET_SPARSE_BUFFER",
+ [{ SetSparse: BOOLEAN }]
+ );
+ // LARGE_INTEGER is actually a type union. We'll use the int64 representation
+ const LARGE_INTEGER = ctypes.int64_t;
+ const FILE_ZERO_DATA_INFORMATION = new ctypes.StructType(
+ "FILE_ZERO_DATA_INFORMATION",
+ [{ FileOffset: LARGE_INTEGER }, { BeyondFinalZero: LARGE_INTEGER }]
+ );
+
+ const GENERIC_WRITE = 0x40000000;
+ const OPEN_ALWAYS = 4;
+ const FILE_ATTRIBUTE_NORMAL = 0x80;
+ const INVALID_HANDLE_VALUE = new ctypes.Int64(-1);
+ const FSCTL_SET_SPARSE = 0x900c4;
+ const FSCTL_SET_ZERO_DATA = 0x980c8;
+ const FILE_BEGIN = 0;
+
+ let kernel32 = ctypes.open("kernel32.dll");
+
+ try {
+ let CreateFile = kernel32.declare(
+ "CreateFileW",
+ ctypes.winapi_abi,
+ HANDLE, // return type: handle to the file
+ ctypes.char16_t.ptr, // in: lpFileName
+ ctypes.uint32_t, // in: dwDesiredAccess
+ ctypes.uint32_t, // in: dwShareMode
+ ctypes.voidptr_t, // in, optional: lpSecurityAttributes (note that
+ // we're cheating here by not declaring a
+ // SECURITY_ATTRIBUTES structure -- that's because
+ // we're going to pass in null anyway)
+ ctypes.uint32_t, // in: dwCreationDisposition
+ ctypes.uint32_t, // in: dwFlagsAndAttributes
+ HANDLE // in, optional: hTemplateFile
+ );
+
+ let filePath = aFile.path;
+ let hFile = CreateFile(
+ filePath,
+ GENERIC_WRITE,
+ 0,
+ null,
+ OPEN_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL,
+ null
+ );
+ let hFileInt = ctypes.cast(hFile, ctypes.intptr_t);
+ if (ctypes.Int64.compare(hFileInt.value, INVALID_HANDLE_VALUE) == 0) {
+ throw new Error(
+ "CreateFile failed for " +
+ filePath +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ try {
+ let DeviceIoControl = kernel32.declare(
+ "DeviceIoControl",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE, // in: hDevice
+ ctypes.uint32_t, // in: dwIoControlCode
+ ctypes.voidptr_t, // in, optional: lpInBuffer
+ ctypes.uint32_t, // in: nInBufferSize
+ ctypes.voidptr_t, // out, optional: lpOutBuffer
+ ctypes.uint32_t, // in: nOutBufferSize
+ ctypes.uint32_t.ptr, // out, optional: lpBytesReturned
+ ctypes.voidptr_t // inout, optional: lpOverlapped (again, we're
+ // cheating here by not having this as an
+ // OVERLAPPED structure
+ );
+ // bytesReturned needs to be passed in, even though it's meaningless
+ let bytesReturned = new ctypes.uint32_t();
+ let sparseBuffer = new FILE_SET_SPARSE_BUFFER();
+ sparseBuffer.SetSparse = 1;
+
+ // Mark the file as sparse
+ if (
+ !DeviceIoControl(
+ hFile,
+ FSCTL_SET_SPARSE,
+ sparseBuffer.address(),
+ FILE_SET_SPARSE_BUFFER.size,
+ null,
+ 0,
+ bytesReturned.address(),
+ null
+ )
+ ) {
+ throw new Error(
+ "Unable to mark file as sparse, error " + ctypes.winLastError
+ );
+ }
+
+ let zdInfo = new FILE_ZERO_DATA_INFORMATION();
+ zdInfo.FileOffset = aRegionStart;
+ let regionEnd = aRegionStart + aRegionBytes;
+ zdInfo.BeyondFinalZero = regionEnd;
+ // Mark the region as a sparse region
+ if (
+ !DeviceIoControl(
+ hFile,
+ FSCTL_SET_ZERO_DATA,
+ zdInfo.address(),
+ FILE_ZERO_DATA_INFORMATION.size,
+ null,
+ 0,
+ bytesReturned.address(),
+ null
+ )
+ ) {
+ throw new Error(
+ "Unable to mark region as zero, error " + ctypes.winLastError
+ );
+ }
+
+ // Move to past the sparse region and mark it as the end of the file. The
+ // above DeviceIoControl call is useless unless followed by this.
+ let SetFilePointerEx = kernel32.declare(
+ "SetFilePointerEx",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE, // in: hFile
+ LARGE_INTEGER, // in: liDistanceToMove
+ LARGE_INTEGER.ptr, // out, optional: lpNewFilePointer
+ ctypes.uint32_t // in: dwMoveMethod
+ );
+ if (!SetFilePointerEx(hFile, regionEnd, null, FILE_BEGIN)) {
+ throw new Error(
+ "Unable to set file pointer to end, error " + ctypes.winLastError
+ );
+ }
+
+ let SetEndOfFile = kernel32.declare(
+ "SetEndOfFile",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE // in: hFile
+ );
+ if (!SetEndOfFile(hFile)) {
+ throw new Error(
+ "Unable to set end of file, error " + ctypes.winLastError
+ );
+ }
+
+ return true;
+ } finally {
+ let CloseHandle = kernel32.declare(
+ "CloseHandle",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE // in: hObject
+ );
+ CloseHandle(hFile);
+ }
+ } finally {
+ kernel32.close();
+ }
+ } else if ("nsILocalFileMac" in Ci) {
+ // Macs don't support marking files as sparse.
+ return false;
+ } else {
+ // Assuming Unix here. Unix file systems generally automatically sparsify
+ // files.
+ return true;
+ }
+ },
+
+ /**
+ * Converts a size in bytes into its mebibytes string representation.
+ * NB: 1 MiB = 1024 * 1024 = 1048576 B.
+ *
+ * @param {integer} aSize - The size in bytes.
+ * @returns {string} A string representing the size in mebibytes.
+ */
+ toMiBString(aSize) {
+ return aSize / 1048576 + " MiB";
+ },
+
+ /**
+ * A variant of do_timeout that accepts an actual function instead of
+ * requiring you to pass a string to evaluate. If the function throws an
+ * exception when invoked, we will use do_throw to ensure that the test fails.
+ *
+ * @param {integer} aDelayInMS - The number of milliseconds to wait before firing the timer.
+ * @param {Function} aFunc - The function to invoke when the timer fires.
+ * @param {object} [aFuncThis] - Optional 'this' pointer to use.
+ * @param {*[]} aFuncArgs - Optional list of arguments to pass to the function.
+ */
+ _timer: null,
+ do_timeout_function(aDelayInMS, aFunc, aFuncThis, aFuncArgs) {
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let wrappedFunc = function () {
+ try {
+ aFunc.apply(aFuncThis, aFuncArgs);
+ } catch (ex) {
+ // we want to make sure that if the thing we call throws an exception,
+ // that this terminates the test.
+ do_throw(ex);
+ }
+ };
+ this._timer.initWithCallback(
+ wrappedFunc,
+ aDelayInMS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ /**
+ * Ensure the given nsIMsgFolder's database is up-to-date, calling the provided
+ * callback once the folder has been loaded. (This may be instantly or
+ * after a re-parse.)
+ *
+ * @param {nsIMsgFolder} aFolder - The nsIMsgFolder whose database you want
+ * to ensure is up-to-date.
+ * @param {Function} aCallback - The callback function to invoke once the
+ * folder has been loaded.
+ * @param {object} aCallbackThis - The 'this' to use when calling the callback.
+ * Pass null if your callback does not rely on 'this'.
+ * @param {*[]} aCallbackArgs - A list of arguments to pass to the callback
+ * via apply. If you provide [1,2,3], we will effectively call:
+ * aCallbackThis.aCallback(1,2,3);
+ * @param {boolean} [aSomeoneElseWillTriggerTheUpdate=false] If this is true,
+ * we do not trigger the updateFolder call and it is assumed someone else is
+ * taking care of that.
+ */
+ updateFolderAndNotify(
+ aFolder,
+ aCallback,
+ aCallbackThis,
+ aCallbackArgs,
+ aSomeoneElseWillTriggerTheUpdate = false
+ ) {
+ // register for the folder loaded notification ahead of time... even though
+ // we may not need it...
+ let folderListener = {
+ onFolderEvent(aEventFolder, aEvent) {
+ if (aEvent == "FolderLoaded" && aFolder.URI == aEventFolder.URI) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ aCallback.apply(aCallbackThis, aCallbackArgs);
+ }
+ },
+ };
+
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.event
+ );
+
+ if (!aSomeoneElseWillTriggerTheUpdate) {
+ aFolder.updateFolder(null);
+ }
+ },
+
+ /**
+ * For when you want to compare elements non-strictly.
+ */
+ non_strict_index_of(aArray, aElem) {
+ for (let [i, elem] of aArray.entries()) {
+ if (elem == aElem) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Click on a particular cell in a tree. `window` is not defined here in this
+ * file, so we can't provide it as a default argument. Similarly, we pass in
+ * `EventUtils` as an argument because importing it here does not work
+ * because `window` is not defined.
+ *
+ * @param {object} EventUtils - The EventUtils object.
+ * @param {Window} win - The window the tree is in.
+ * @param {Element} tree - The tree element.
+ * @param {number} row - The tree row to click on.
+ * @param {number} column - The tree column to click on.
+ * @param {object} event - The mouse event to synthesize, e.g. `{ clickCount: 2 }`.
+ */
+ treeClick(EventUtils, win, tree, row, column, event) {
+ let coords = tree.getCoordsForCellItem(row, tree.columns[column], "cell");
+ let treeChildren = tree.lastElementChild;
+ EventUtils.synthesizeMouse(
+ treeChildren,
+ coords.x + coords.width / 2,
+ coords.y + coords.height / 2,
+ event,
+ win
+ );
+ },
+
+ /**
+ * For waiting until an element exists in a given document. Pass in the
+ * `MutationObserver` as an argument because importing it here does not work
+ * because `window` is not defined here.
+ *
+ * @param {object} MutationObserver - The MutationObserver object.
+ * @param {Document} doc - Document that contains the elements.
+ * @param {string} observedNodeId - Id of the element to observe.
+ * @param {string} awaitedNodeId - Id of the element that will soon exist.
+ * @returns {Promise.<undefined>} - A promise fulfilled when the element exists.
+ */
+ awaitElementExistence(MutationObserver, doc, observedNodeId, awaitedNodeId) {
+ return new Promise(resolve => {
+ let outerObserver = new MutationObserver((mutationsList, observer) => {
+ for (let mutation of mutationsList) {
+ if (mutation.type == "childList" && mutation.addedNodes.length) {
+ let element = doc.getElementById(awaitedNodeId);
+
+ if (element) {
+ observer.disconnect();
+ resolve();
+ return;
+ }
+ }
+ }
+ });
+
+ let nodeToObserve = doc.getElementById(observedNodeId);
+ outerObserver.observe(nodeToObserve, { childList: true });
+ });
+ },
+};