summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources/MailTestUtils.jsm
blob: 588e21521ae29bd3786618f2e1835a07ae652491 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
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 });
    });
  },
};