Revision control

Copy as Markdown

Other Tools

/* 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 { MessageSend } = ChromeUtils.importESModule(
);
const { MailUtils } = ChromeUtils.importESModule(
);
const { TestUtils } = ChromeUtils.importESModule(
);
/* import-globals-from ../../../test/resources/alertTestUtils.js */
load("../../../resources/alertTestUtils.js");
let promptChoices;
let alertCount;
let testNumber = 0;
function confirmExPS() {
Assert.ok(
promptChoices.length,
"Each failed copy should have a recovery choice"
);
return promptChoices.shift();
}
function alertPS() {
alertCount++;
}
add_setup(function () {
localAccountUtils.loadLocalMailAccount();
registerAlertTestUtils();
});
async function checkCopyRecovery({
outcomes,
choices,
savedTo,
alerts = 0,
inspectStartupFailure = false,
withFcc2 = savedTo.includes("additional"),
startWithFcc2 = false,
deliverMode = Ci.nsIMsgSend.nsMsgDeliverNow,
fallbackFor = "primary",
specialFolderName = false,
}) {
const root = localAccountUtils.rootFolder;
const sentName = `Sent-${++testNumber}${specialFolderName ? " # copy" : ""}`;
const sentFolder = root.createLocalSubfolder(sentName);
const additionalFolder = root.createLocalSubfolder(
`Additional-${testNumber}`
);
const fallbackFolder =
fallbackFor == "additional" ? additionalFolder : sentFolder;
const fallbackURI = `${root.URI}/${encodeURIComponent(
`${fallbackFolder.localizedName}-${root.server.prettyName}`
)}`;
const identity = getSmtpIdentity(
"sender@example.invalid",
getBasicSmtpServer()
);
identity.fccFolderURI = sentFolder.URI;
const fields = Cc[
"@mozilla.org/messengercompose/composefields;1"
].createInstance(Ci.nsIMsgCompFields);
if (withFcc2) {
fields.fcc2 = additionalFolder.URI;
}
const messageFile = do_get_profile();
messageFile.append(`fcc-${testNumber}.eml`);
const message =
"From: sender@example.invalid\r\nTo: recipient@example.invalid\r\nSubject: FCC recovery\r\n\r\nPreserve this message.\r\n";
await IOUtils.writeUTF8(messageFile.path, message);
const send = new MessageSend();
const completed = Promise.withResolvers();
let completionCount = 0;
Object.assign(send, {
_userIdentity: identity,
_compFields: fields,
_fcc: sentFolder.URI,
_deliverMode: deliverMode,
_messageKey: 0xffffffff,
_messageFile: messageFile,
_shouldRemoveMessageFile: true,
_sendReport: Cc[
"@mozilla.org/messengercompose/sendreport;1"
].createInstance(Ci.nsIMsgSendReport),
_sendListener: {
...copyListener,
onStopCopy(status) {
completionCount++;
Assert.equal(
status,
Cr.NS_OK,
"Copy recovery should finish successfully"
);
completed.resolve();
},
onGetDraftFolderURI() {
if (remainingOutcomes[0] == "setup-failure") {
remainingOutcomes.shift();
copyFiles.push(send._copyFile.clone());
throw new Error("Copy setup failed");
}
},
},
});
const listenerPointer = Cc[
"@mozilla.org/supports-interface-pointer;1"
].createInstance(Ci.nsISupportsInterfacePointer);
listenerPointer.data = send._sendListener;
send._sendListener = listenerPointer.data.QueryInterface(
Ci.nsIMsgSendListener
);
Assert.ok(send._sendListener instanceof Ci.nsIMsgCopyServiceListener);
const contract = "@mozilla.org/messengercompose/msgcopy;1";
// Create the native copy objects before replacing their contract. Accessing
// a factory by contract ID is not part of the scriptable component manager
// API and is unavailable in some builds.
const realCopies = outcomes.map(() =>
Cc[contract].createInstance(Ci.nsIMsgCopy)
);
const copyFiles = [];
const destinations = [];
const remainingOutcomes = [...outcomes];
let startupError;
const mockCID = MockRegistrar.register(contract, () => {
const realCopy = realCopies.shift();
Assert.ok(realCopy, "Each mock copy should have a native copy instance");
return {
QueryInterface: ChromeUtils.generateQI(["nsIMsgCopy"]),
get dstFolder() {
return realCopy.dstFolder;
},
startCopyOperation(userIdentity, file, mode, listener, uri, replace) {
Assert.ok(
messageFile.exists(),
"The source should survive until recovery finishes"
);
Assert.ok(file.exists(), "Each attempt should have a copy file");
copyFiles.push(file.clone());
destinations.push(uri);
Assert.ok(remainingOutcomes.length, "No unexpected copy should occur");
switch (remainingOutcomes.shift()) {
case "local-failure": {
// Exercise the native local copy's callback followed by an error
// return, using a missing input file on every platform.
const missingFile = file.clone();
missingFile.leafName += ".missing";
realCopy.startCopyOperation(
userIdentity,
missingFile,
mode,
listener,
uri,
replace
);
do_throw("Copying a missing file should fail");
break;
}
case "async-failure":
TestUtils.executeSoon(() =>
listener.notifyListenerOnStopCopy(Cr.NS_ERROR_FAILURE)
);
break;
case "callback-then-throw": {
startupError = new Error("Copy could not start");
listener.notifyListenerOnStopCopy(Cr.NS_ERROR_FAILURE);
throw startupError;
}
case "throw":
throw Components.Exception(
"Copy could not start",
Cr.NS_ERROR_FILE_ACCESS_DENIED
);
case "throw-js":
throw new Error("Copy could not start");
case "success":
realCopy.startCopyOperation(
userIdentity,
file,
mode,
listener,
uri,
replace
);
break;
}
},
};
});
promptChoices = [...choices];
alertCount = 0;
try {
if (inspectStartupFailure) {
const error = await send
._copyToFcc(sentFolder.URI, Ci.nsIMsgSend.nsMsgDeliverNow)
.then(
() => null,
reason => reason
);
Assert.stringContains(
error.message,
startupError.message,
"The startup exception details should win over the synchronous callback"
);
Assert.notEqual(
error.result,
Cr.NS_ERROR_FAILURE,
"The generic callback failure should not replace the startup exception"
);
Assert.ok(error.stack, "The rejected exception should have a stack");
Assert.equal(remainingOutcomes.length, 0, "The copy should be attempted");
Assert.ok(
messageFile.exists(),
"The source message should survive the attempt"
);
Assert.ok(
!copyFiles[0].exists(),
"The attempt's copy file should be removed"
);
return;
}
if (startWithFcc2) {
await send._doFcc2();
} else {
await send._doFcc();
}
await completed.promise;
await TestUtils.waitForCondition(
() => !messageFile.exists(),
"Source file should be removed after completion"
);
Assert.equal(
completionCount,
1,
"There should be only one final completion"
);
Assert.equal(
remainingOutcomes.length,
0,
"All expected copies should be attempted"
);
Assert.equal(
promptChoices.length,
0,
"All recovery choices should be used"
);
Assert.equal(alertCount, alerts, "Failed fallbacks should show an alert");
for (const file of copyFiles) {
Assert.ok(
!file.exists(),
"Completed attempts should leave no temporary copy files"
);
}
const folders = {
primary: sentFolder,
fallback: MailUtils.getExistingFolder(fallbackURI),
additional: additionalFolder,
};
for (const [name, folder] of Object.entries(folders)) {
const expected = savedTo.includes(name) ? 1 : 0;
Assert.equal(
folder?.getTotalMessages(false) ?? 0,
expected,
`${name} should have the expected number of messages`
);
if (expected) {
Assert.ok(
destinations.includes(folder.URI),
`${name} should be a copy destination`
);
const stored = mailTestUtils.loadMessageToString(
folder,
mailTestUtils.firstMsgHdr(folder)
);
Assert.ok(
stored.includes(message),
"The complete message should be preserved"
);
}
}
} finally {
MockRegistrar.unregister(mockCID);
await IOUtils.remove(messageFile.path, { ignoreAbsent: true });
for (const file of copyFiles) {
await IOUtils.remove(file.path, { ignoreAbsent: true });
}
}
}
add_task(async function testRepeatedLocalFailure() {
await checkCopyRecovery({
outcomes: ["local-failure", "local-failure", "success", "success"],
choices: [0, 0],
savedTo: ["primary", "additional"],
});
});
add_task(async function testFailureWithoutCallback() {
for (const outcome of ["throw", "throw-js"]) {
await checkCopyRecovery({
outcomes: [outcome, "success"],
choices: [0],
savedTo: ["primary"],
});
}
});
add_task(async function testSetupFailure() {
await checkCopyRecovery({
outcomes: ["setup-failure", "success"],
choices: [0],
savedTo: ["primary"],
});
});
add_task(async function testSynchronousCallbackThenThrow() {
await checkCopyRecovery({
outcomes: ["callback-then-throw"],
choices: [],
savedTo: [],
inspectStartupFailure: true,
});
});
add_task(async function testAsynchronousFailure() {
await checkCopyRecovery({
outcomes: ["async-failure", "success"],
choices: [0],
savedTo: ["primary"],
});
});
add_task(async function testSaveLocally() {
for (const outcome of ["local-failure", "async-failure"]) {
await checkCopyRecovery({
outcomes: [outcome, "success", "success"],
choices: [2],
savedTo: ["fallback", "additional"],
});
}
});
add_task(async function testLocalFallbackEscapesFolderName() {
await checkCopyRecovery({
outcomes: ["local-failure", "success"],
choices: [2],
savedTo: ["fallback"],
withFcc2: false,
specialFolderName: true,
});
});
add_task(async function testFailedLocalFallback() {
for (const outcome of ["local-failure", "async-failure", "throw"]) {
await checkCopyRecovery({
outcomes: ["local-failure", outcome, "success"],
choices: [2, 1],
savedTo: ["additional"],
alerts: 1,
});
}
});
add_task(async function testRetryLocalFallback() {
await checkCopyRecovery({
outcomes: ["local-failure", "local-failure", "success", "success"],
choices: [2, 2],
savedTo: ["fallback", "additional"],
alerts: 1,
});
});
add_task(async function testRetryOriginalAfterLocalFallbackFailure() {
await checkCopyRecovery({
outcomes: ["local-failure", "local-failure", "success", "success"],
choices: [2, 0],
savedTo: ["primary", "additional"],
alerts: 1,
});
});
add_task(async function testRetryFcc2() {
await checkCopyRecovery({
outcomes: ["success", "async-failure", "success"],
choices: [0],
savedTo: ["primary", "additional"],
});
});
add_task(async function testRetryFcc2WhenQueueing() {
await checkCopyRecovery({
outcomes: ["async-failure", "success"],
choices: [0],
savedTo: ["additional"],
startWithFcc2: true,
deliverMode: Ci.nsIMsgSend.nsMsgQueueForLater,
});
});
add_task(async function testSaveFcc2Locally() {
await checkCopyRecovery({
outcomes: ["success", "async-failure", "success"],
choices: [2],
savedTo: ["primary", "fallback"],
withFcc2: true,
fallbackFor: "additional",
});
});
add_task(async function testDontSave() {
await checkCopyRecovery({
outcomes: ["local-failure", "success"],
choices: [1],
savedTo: ["additional"],
});
});
add_task(async function testUnrecoverableOutboxSetupFailureRejects() {
const root = localAccountUtils.rootFolder;
const identity = getSmtpIdentity(
"sender@example.invalid",
getBasicSmtpServer()
);
const fields = Cc[
"@mozilla.org/messengercompose/composefields;1"
].createInstance(Ci.nsIMsgCompFields);
for (const deliverMode of [
Ci.nsIMsgSend.nsMsgQueueForLater,
Ci.nsIMsgSend.nsMsgDeliverBackground,
]) {
const setupError = new Error("Could not create the Outbox copy file");
const send = new MessageSend();
Object.assign(send, {
_userIdentity: identity,
_compFields: fields,
_fcc: root.URI,
_deliverMode: deliverMode,
_createCopyFile() {
throw setupError;
},
});
await Assert.rejects(
send._mimeDoFcc(),
error => error === setupError,
"An Outbox setup failure should propagate"
);
}
});
add_task(async function testFolderResolutionFailureRejects() {
const resolutionError = new Error("Could not resolve the drafts folder");
const send = new MessageSend();
Object.assign(send, {
_userIdentity: {
async getOrCreateDraftsFolderAsync() {
throw resolutionError;
},
},
_deliverMode: Ci.nsIMsgSend.nsMsgSaveAsDraft,
});
await Assert.rejects(
send._mimeDoFcc(),
error => error === resolutionError,
"A folder-resolution failure should propagate"
);
});