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 { IMAPServer } = ChromeUtils.importESModule(
);
const { TestUtils } = ChromeUtils.importESModule(
);
let incomingServer;
let server;
let inbox;
add_setup(async function () {
Services.prefs.setBoolPref(
"mail.server.default.autosync_offline_stores",
false
);
Services.prefs.setBoolPref("mail.biff.play_sound", false);
Services.prefs.setBoolPref("mail.biff.show_alert", false);
server = new IMAPServer({ extensions: ["RFC2177"] });
const account = MailServices.accounts.createAccount();
account.addIdentity(MailServices.accounts.createIdentity());
incomingServer = MailServices.accounts.createIncomingServer(
"user",
"localhost",
"imap"
);
incomingServer.password = "password";
incomingServer.port = server.port;
incomingServer.QueryInterface(Ci.nsIImapIncomingServer).useIdle = true;
account.incomingServer = incomingServer;
const listener = new PromiseTestUtils.PromiseUrlListener();
MailServices.imap.discoverAllFolders(
incomingServer.rootFolder,
listener,
null
);
await listener.promise;
inbox = incomingServer.rootFolder
.getChildNamed("INBOX")
.QueryInterface(Ci.nsIMsgImapMailFolder);
const updateListener = new PromiseTestUtils.PromiseUrlListener();
inbox.updateFolderWithListener(null, updateListener);
await updateListener.promise;
registerCleanupFunction(() => {
incomingServer.closeCachedConnections();
MailServices.accounts.removeIncomingServer(incomingServer, false);
});
});
/**
* Wait until the client connection for a mailbox is in RFC 2177 IDLE.
*
* @param {string} mailboxName
*/
async function waitForIdle(mailboxName) {
await TestUtils.waitForCondition(
() =>
server.daemon.getConnections(mailboxName).some(handler => handler.idling),
`${mailboxName} should enter IDLE`
);
}
async function waitForConnectionsToClose() {
await TestUtils.waitForCondition(
() => !server.daemon.getConnections().length,
"all cached IMAP connections should close"
);
}
async function updateFolder(folder) {
const listener = new PromiseTestUtils.PromiseUrlListener();
folder.updateFolderWithListener(null, listener);
await listener.promise;
}
/**
* Make the subset of an nsIMailboxSpec consumed by
* nsImapMailFolder::UpdateImapMailboxStatus.
*
* @param {object} values
* @returns {nsIMailboxSpec}
*/
function makeMailboxSpec(values) {
return {
QueryInterface: ChromeUtils.generateQI(["nsIMailboxSpec"]),
numMessages: values.numMessages,
numUnseenMessages: values.numUnseenMessages,
numRecentMessages: 0,
nextUID: values.nextUID,
folderSelected: true,
};
}
add_task(async function testIdleDeliveryBiffs() {
await waitForIdle("INBOX");
const folderListener = {
QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
biffNotifications: [],
onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
if (property == "BiffState" || property == "NewMailReceived") {
this.biffNotifications.push({ folder, property, oldValue, newValue });
}
},
};
MailServices.mailSession.AddFolderListener(
folderListener,
Ci.nsIFolderListener.intPropertyChanged
);
const generator = new MessageGenerator();
await server.addMessages("INBOX", [generator.makeMessage()], false);
const messageAdded = PromiseTestUtils.promiseFolderNotification(
inbox,
"msgAdded"
);
Assert.equal(
server.daemon.sendUnsolicited("INBOX", "* 1 EXISTS"),
1,
"one idling connection should receive the new message count"
);
await messageAdded;
await TestUtils.waitForCondition(
() => incomingServer.biffState == Ci.nsIMsgFolder.nsMsgBiffState_NewMail,
"the unsolicited EXISTS response should result in biff"
);
Assert.equal(inbox.getTotalMessages(false), 1, "one header was downloaded");
Assert.equal(inbox.getNumUnread(false), 1, "the new message is unread");
Assert.equal(
inbox.getNumNewMessages(false),
1,
"the new message contributes to the biff count"
);
Assert.equal(
folderListener.biffNotifications.length,
1,
"the message produces one biff notification"
);
MailServices.mailSession.RemoveFolderListener(folderListener);
});
add_task(async function testStatusDefersBiffUntilFlagSync() {
// Build a three-message local cache with one older message still unread and
// new, matching the state left behind before the sleep/wake race.
incomingServer.closeCachedConnections();
await waitForConnectionsToClose();
const generator = new MessageGenerator();
await server.addMessages(
"INBOX",
generator.makeMessages({ count: 2 }),
false
);
const oldServerMessages = server.getMessagesInFolder(inbox);
oldServerMessages[1].setFlag("\\Seen");
oldServerMessages[2].setFlag("\\Seen");
await updateFolder(inbox);
Assert.equal(inbox.getTotalMessages(false), 3, "three messages are cached");
Assert.equal(inbox.getNumUnread(false), 1, "one cached message is unread");
Assert.equal(
inbox.getNumNewMessages(false),
1,
"the unread cached message remains new"
);
incomingServer.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NoMail;
incomingServer.QueryInterface(Ci.nsIImapIncomingServer).useIdle = false;
incomingServer.closeCachedConnections();
await waitForConnectionsToClose();
// The old message is read on another device while one batch of four newer
// messages arrives. No further server mutation occurs after this point.
oldServerMessages[0].setFlag("\\Seen");
await server.addMessages(
"INBOX",
generator.makeMessages({ count: 4 }),
false
);
const biffNotifications = [];
const folderListener = {
QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
if (property == "BiffState" || property == "NewMailReceived") {
biffNotifications.push({ folder, property, oldValue, newValue });
}
},
};
MailServices.mailSession.AddFolderListener(
folderListener,
Ci.nsIFolderListener.intPropertyChanged
);
const statusListener = new PromiseTestUtils.PromiseUrlListener();
inbox.updateStatus(statusListener, null);
await statusListener.promise;
await TestUtils.waitForCondition(
() => inbox.getTotalMessages(false) == 7 && inbox.getNumUnread(false) == 4,
"the STATUS-triggered folder synchronization should finish"
);
info(
`After synchronization: new=${inbox.getNumNewMessages(false)}, ` +
`biffState=${incomingServer.biffState}, ` +
`performingBiff=${incomingServer.performingBiff}, ` +
`notifications=${biffNotifications.length}`
);
await TestUtils.waitForCondition(
() => inbox.getNumNewMessages(false) == 4 && !incomingServer.performingBiff,
"the synchronized folder should finish biff"
);
Assert.equal(inbox.getNumUnread(false), 4, "only the new batch is unread");
const oldHeader = inbox.msgDatabase.getMsgHdrForKey(oldServerMessages[0].uid);
Assert.ok(oldHeader.isRead, "the old message is reconciled as read");
Assert.ok(
!(oldHeader.flags & Ci.nsMsgMessageFlags.New),
"the old message is no longer locally new"
);
Assert.equal(
biffNotifications.filter(
event =>
event.property != "BiffState" ||
event.newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail
).length,
1,
"the stable server batch should produce only one positive biff"
);
MailServices.mailSession.RemoveFolderListener(folderListener);
});
add_task(function testNoFreshUnseenDoesNotBiff() {
const sink = inbox.QueryInterface(Ci.nsIImapMailFolderSink);
// Prime a last-known server count one above the reconciled local count,
// then model local processing having consumed the pending delta.
sink.UpdateImapMailboxStatus(
null,
makeMailboxSpec({ numMessages: 8, numUnseenMessages: 5, nextUID: 9 })
);
inbox.changeNumPendingUnread(-inbox.numPendingUnread);
incomingServer.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NoMail;
const biffNotifications = [];
const folderListener = {
QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
if (property == "BiffState") {
biffNotifications.push({ folder, oldValue, newValue });
}
},
};
MailServices.mailSession.AddFolderListener(
folderListener,
Ci.nsIFolderListener.intPropertyChanged
);
// A selected-folder NOOP may update UIDNEXT, but it supplies no UNSEEN
// count. The last known count must not be interpreted as a fresh delta.
sink.UpdateImapMailboxStatus(
null,
makeMailboxSpec({ numMessages: 9, numUnseenMessages: -1, nextUID: 10 })
);
Assert.equal(
incomingServer.biffState,
Ci.nsIMsgFolder.nsMsgBiffState_NoMail,
"NOOP without a fresh UNSEEN count should not biff"
);
Assert.deepEqual(
biffNotifications,
[],
"NOOP without a fresh UNSEEN count should not notify biff"
);
Assert.equal(
inbox.numPendingUnread,
0,
"the pending count should not change"
);
Assert.equal(
inbox.serverUnseen,
5,
"the last known UNSEEN count should remain"
);
Assert.equal(
inbox.serverNextUID,
10,
"the fresh UIDNEXT value should be kept"
);
// A later STATUS with a genuine unseen increase must still biff once.
inbox.changeNumPendingUnread(1);
sink.UpdateImapMailboxStatus(
null,
makeMailboxSpec({ numMessages: 9, numUnseenMessages: 6, nextUID: 10 })
);
Assert.equal(
incomingServer.biffState,
Ci.nsIMsgFolder.nsMsgBiffState_NewMail,
"a fresh increase in UNSEEN should still biff"
);
Assert.equal(inbox.getNumNewMessages(false), 1, "one new unread should biff");
Assert.equal(
biffNotifications.length,
1,
"the fresh increase should produce exactly one biff notification"
);
MailServices.mailSession.RemoveFolderListener(folderListener);
});