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
/**
* Tests that a truncated chunked body download is not stored as a complete
* offline message.
*
* When a message body is downloaded in chunks and the server stops delivering
* data partway through without any protocol error (e.g. a premature connection
* close), the short chunk must not be treated as the final chunk and stored as
* a *complete* offline copy (nsMsgMessageFlags::Offline). Doing so would mean
* the message is never re-fetched and the user silently sees a truncated
* message (e.g. missing attachments). Instead, the download must be detected as
* incomplete -- fewer octets received than the server announced in RFC822.SIZE
* -- and aborted, so the partial copy is discarded and re-fetched on next
* access.
*
* The test uses the fakeserver's `_truncateBodyAt` hook to simulate the
* truncation while keeping RFC822.SIZE reporting the true (larger) size; this
* is distinct from the wrong-RFC822.SIZE case (where the server under-reports
*
*/
var { PromiseTestUtils } = ChromeUtils.importESModule(
);
// Chunk parameters -- small so the body spans several chunks.
const CHUNK_SIZE = 4000;
const CHUNK_THRESHOLD = 2000;
// A body large enough to require multiple chunks. The truncation point is set
// roughly in the middle, on a chunk boundary that is *before* the end of the
// message, so the short chunk is mistaken for the final one.
const BODY_LINE =
"This is a line of the test message body, padding it out.\r\n"; // 58 bytes
const BODY_LINES = 800; // ~41 KB body
const TRUNCATE_AT = 20000; // deliver only the first ~20 KB
var gIMAPService;
var gFullMsgFile, gTruncMsgFile;
var gFullMsg, gTruncMsg;
function writeMessageFile(subject) {
const headers =
`From: sender@example.com\r\n` +
`To: receiver@example.com\r\n` +
`Subject: ${subject}\r\n` +
`Message-ID: <${subject}@example.com>\r\n` +
`Content-Type: text/plain\r\n` +
`\r\n`;
const body = BODY_LINE.repeat(BODY_LINES);
const content = headers + body;
const file = Services.dirsvc.get("TmpD", Ci.nsIFile);
file.append(`truncated-offline-${subject}.eml`);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
const fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
Ci.nsIFileOutputStream
);
fos.init(file, 0x02 | 0x08 | 0x20, 0o600, 0); // write | create | truncate
fos.write(content, content.length);
fos.close();
return file;
}
function addMessageToServer(file, mailbox) {
const uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
const msg = new ImapMessage(uri.spec, mailbox.uidnext++, []);
// Do NOT call setSize(): RFC822.SIZE then reports the true file size, so the
// truncated delivery is genuinely short of what the server announced.
mailbox.addMessage(msg);
return msg;
}
async function streamMessageToOfflineStore(msgHdr) {
const listener = new PromiseTestUtils.PromiseUrlListener();
const streamListener = new PromiseTestUtils.PromiseStreamListener();
gIMAPService.streamMessage(
IMAPPump.inbox.getUriForMsg(msgHdr),
streamListener,
null,
listener,
false,
"",
false
);
// The truncated download aborts, so the stream/url may resolve with an error.
// Swallow it -- the assertions below check the resulting offline state.
await streamListener.promise.catch(() => {});
await listener.promise.catch(() => {});
}
add_setup(async function () {
Services.prefs.setBoolPref("mail.biff.play_sound", false);
Services.prefs.setBoolPref("mail.biff.show_alert", false);
Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
Services.prefs.setBoolPref(
"mail.server.server1.autosync_offline_stores",
false
);
Services.prefs.setBoolPref("mail.server.server1.offline_download", true);
// Force download-by-chunks with small chunks so the body is fetched in
// several BODY[]<start.count> requests.
Services.prefs.setBoolPref("mail.server.default.fetch_by_chunks", true);
Services.prefs.setIntPref("mail.imap.chunk_size", CHUNK_SIZE);
Services.prefs.setIntPref(
"mail.imap.min_chunk_size_threshold",
CHUNK_THRESHOLD
);
Services.prefs.setIntPref("mail.imap.chunk_add", 0);
setupIMAPPump();
IMAPPump.inbox.hierarchyDelimiter = "/";
IMAPPump.inbox.verifiedAsOnlineFolder = true;
gFullMsgFile = writeMessageFile("full");
gTruncMsgFile = writeMessageFile("truncated");
const inbox = IMAPPump.daemon.getMailbox("INBOX");
gFullMsg = addMessageToServer(gFullMsgFile, inbox);
gTruncMsg = addMessageToServer(gTruncMsgFile, inbox);
// Make the second message deliver a truncated body.
gTruncMsg._truncateBodyAt = TRUNCATE_AT;
const listener = new PromiseTestUtils.PromiseUrlListener();
IMAPPump.inbox.updateFolderWithListener(null, listener);
await listener.promise;
gIMAPService = Cc[
"@mozilla.org/messenger/messageservice;1?type=imap"
].getService(Ci.nsIMsgMessageService);
});
/**
* Positive control: a complete chunked download must be stored offline.
*/
add_task(async function fullMessageIsMarkedOffline() {
const db = IMAPPump.inbox.msgDatabase;
const hdr = db.getMsgHdrForMessageID("full@example.com");
Assert.ok(!!hdr, "header for full message exists");
await streamMessageToOfflineStore(hdr);
Assert.notEqual(
hdr.flags & Ci.nsMsgMessageFlags.Offline,
0,
"a fully-downloaded chunked message is marked as offline-complete"
);
});
/**
* A truncated chunked download must NOT be stored as a complete offline copy.
*/
add_task(async function truncatedMessageIsNotMarkedOffline() {
const db = IMAPPump.inbox.msgDatabase;
const hdr = db.getMsgHdrForMessageID("truncated@example.com");
Assert.ok(!!hdr, "header for truncated message exists");
await streamMessageToOfflineStore(hdr);
Assert.equal(
hdr.flags & Ci.nsMsgMessageFlags.Offline,
0,
"a truncated chunked message must not be marked as offline-complete"
);
});
add_task(function endTest() {
teardownIMAPPump();
try {
gFullMsgFile.remove(false);
gTruncMsgFile.remove(false);
} catch (ex) {
// best effort cleanup
}
});