Bug 1926810 - Fail mbox reads if offset into mbox file doesn't start at a "From " separator. r=mkmelin
This patch makes the Mbox reader much more fussy. Previously, it'd just start reading from where it was told to, and ignored a missing "From " separator line at the beginning of a message. This patch makes it much more strict, throwing a Read() error if that "From " line is not there. Differential Revision: https://phabricator.services.mozilla.com/D227315 Depends on D227314 --HG-- extra : rebase_source : eadc9b10c9174402154e60c8ba7dc7319825038a extra : amend_source : dbcc189026d6824a1f01aace9ce0b6f4ce231a25
This commit is contained in:
Родитель
8ba1aeac72
Коммит
a3a8396749
|
@ -171,6 +171,9 @@ class nsIMsgFolder;
|
||||||
|
|
||||||
#define NS_MSG_ERROR_UNEXPECTED_SIZE NS_MSG_GENERATE_FAILURE(35)
|
#define NS_MSG_ERROR_UNEXPECTED_SIZE NS_MSG_GENERATE_FAILURE(35)
|
||||||
|
|
||||||
|
// Mbox message doesn't start with "From " separator line.
|
||||||
|
#define NS_MSG_ERROR_MBOX_MALFORMED NS_MSG_GENERATE_FAILURE(36)
|
||||||
|
|
||||||
/* Error codes for message compose are defined in
|
/* Error codes for message compose are defined in
|
||||||
compose\src\nsMsgComposeStringBundle.h. Message compose use the same error
|
compose\src\nsMsgComposeStringBundle.h. Message compose use the same error
|
||||||
code space as other mailnews modules. To avoid any conflict, values between
|
code space as other mailnews modules. To avoid any conflict, values between
|
||||||
|
|
|
@ -62,9 +62,16 @@ class MboxParser {
|
||||||
* via Drain().
|
* via Drain().
|
||||||
*/
|
*/
|
||||||
bool IsFinished() const {
|
bool IsFinished() const {
|
||||||
return Available() == 0 && (mState == eEOF || mState == eMessageComplete);
|
return Available() == 0 && (mState == eEOF || mState == eMessageComplete ||
|
||||||
|
mState == eMalformed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the parser has decided the mbox is malformed and refuses
|
||||||
|
* to proceed (e.g. Missing a "From " line at the beginning).
|
||||||
|
*/
|
||||||
|
bool IsMalformed() const { return mState == eMalformed; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when the end of the mbox has been reached (and the last
|
* Returns true when the end of the mbox has been reached (and the last
|
||||||
* message has been completely read out via Drain()).
|
* message has been completely read out via Drain()).
|
||||||
|
@ -113,7 +120,8 @@ class MboxParser {
|
||||||
while (true) {
|
while (true) {
|
||||||
// If a message is complete (or the mbox is finished), then
|
// If a message is complete (or the mbox is finished), then
|
||||||
// we stall.
|
// we stall.
|
||||||
if (mState == eMessageComplete || mState == eEOF) {
|
if (mState == eMessageComplete || mState == eEOF ||
|
||||||
|
mState == eMalformed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +220,7 @@ class MboxParser {
|
||||||
eEmitQuoting,
|
eEmitQuoting,
|
||||||
eEmitBodyLine,
|
eEmitBodyLine,
|
||||||
eMessageComplete, // Message is complete (or ended prematurely).
|
eMessageComplete, // Message is complete (or ended prematurely).
|
||||||
|
eMalformed, // Error. No initial "From " line was found.
|
||||||
eEOF, // End of mbox.
|
eEOF, // End of mbox.
|
||||||
} mState{eExpectFromLine};
|
} mState{eExpectFromLine};
|
||||||
|
|
||||||
|
@ -221,17 +230,11 @@ class MboxParser {
|
||||||
// the mbox file has been reached.
|
// the mbox file has been reached.
|
||||||
span handle(span data) {
|
span handle(span data) {
|
||||||
{
|
{
|
||||||
const char* stateName[] = {"eExpectFromLine",
|
const char* stateName[] = {
|
||||||
"eDiscardFromLine",
|
"eExpectFromLine", "eDiscardFromLine", "eExpectHeaderLine",
|
||||||
"eExpectHeaderLine",
|
"eEmitHeaderLine", "eEmitSeparator", "eExpectBodyLine",
|
||||||
"eEmitHeaderLine",
|
"eCountQuoting", "eEmitQuoting", "eEmitBodyLine",
|
||||||
"eEmitSeparator",
|
"eMessageComplete", "eMalformed", "eEOF"};
|
||||||
"eExpectBodyLine",
|
|
||||||
"eCountQuoting",
|
|
||||||
"eEmitQuoting",
|
|
||||||
"eEmitBodyLine",
|
|
||||||
"eMessageComplete",
|
|
||||||
"eEOF"};
|
|
||||||
MOZ_LOG(gMboxLog, LogLevel::Verbose,
|
MOZ_LOG(gMboxLog, LogLevel::Verbose,
|
||||||
("MboxParser - handle %s (%zu bytes: '%s')", stateName[mState],
|
("MboxParser - handle %s (%zu bytes: '%s')", stateName[mState],
|
||||||
data.Length(),
|
data.Length(),
|
||||||
|
@ -258,6 +261,8 @@ class MboxParser {
|
||||||
return handle_eEmitBodyLine(data);
|
return handle_eEmitBodyLine(data);
|
||||||
case eMessageComplete:
|
case eMessageComplete:
|
||||||
return handle_eMessageComplete(data);
|
return handle_eMessageComplete(data);
|
||||||
|
case eMalformed:
|
||||||
|
return handle_eMalformed(data);
|
||||||
case eEOF:
|
case eEOF:
|
||||||
return handle_eEOF(data);
|
return handle_eEOF(data);
|
||||||
default:
|
default:
|
||||||
|
@ -339,10 +344,9 @@ class MboxParser {
|
||||||
}
|
}
|
||||||
mState = eDiscardFromLine;
|
mState = eDiscardFromLine;
|
||||||
} else {
|
} else {
|
||||||
MOZ_LOG(gMboxLog, LogLevel::Warning,
|
MOZ_LOG(gMboxLog, LogLevel::Error,
|
||||||
("MboxParser - Missing 'From ' separator"));
|
("MboxParser - Missing 'From ' separator"));
|
||||||
// Just jump straight to header phase.
|
mState = eMalformed;
|
||||||
mState = eExpectHeaderLine;
|
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -521,6 +525,9 @@ class MboxParser {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Halt parsing, So this is a no-op.
|
||||||
|
span handle_eMalformed(span data) { return data; }
|
||||||
|
|
||||||
// All done, so this is a no-op.
|
// All done, so this is a no-op.
|
||||||
span handle_eEOF(span data) {
|
span handle_eEOF(span data) {
|
||||||
MOZ_ASSERT(data.IsEmpty());
|
MOZ_ASSERT(data.IsEmpty());
|
||||||
|
@ -871,6 +878,10 @@ nsresult MboxMsgInputStream::PumpData() {
|
||||||
mUnused -= consumed;
|
mUnused -= consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mParser->IsMalformed()) {
|
||||||
|
return NS_MSG_ERROR_MBOX_MALFORMED;
|
||||||
|
}
|
||||||
|
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,16 @@
|
||||||
|
|
||||||
namespace testing {
|
namespace testing {
|
||||||
|
|
||||||
void ExtractFromMbox(nsACString const& mbox, nsTArray<nsCString>& msgs,
|
// Parses all the messages in mbox returning them as an array.
|
||||||
size_t readSize) {
|
nsresult ExtractFromMbox(nsACString const& mbox, nsTArray<nsCString>& msgs,
|
||||||
|
size_t readSize) {
|
||||||
// Open stream for raw mbox.
|
// Open stream for raw mbox.
|
||||||
nsCOMPtr<nsIInputStream> raw;
|
nsCOMPtr<nsIInputStream> raw;
|
||||||
nsresult rv = NS_NewByteInputStream(getter_AddRefs(raw), mozilla::Span(mbox),
|
nsresult rv = NS_NewByteInputStream(getter_AddRefs(raw), mozilla::Span(mbox),
|
||||||
NS_ASSIGNMENT_COPY);
|
NS_ASSIGNMENT_COPY);
|
||||||
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
if (NS_FAILED(rv)) {
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
msgs.Clear();
|
msgs.Clear();
|
||||||
// Wrap with MboxMsgInputStream and read single message.
|
// Wrap with MboxMsgInputStream and read single message.
|
||||||
|
@ -27,7 +30,9 @@ void ExtractFromMbox(nsACString const& mbox, nsTArray<nsCString>& msgs,
|
||||||
while (true) {
|
while (true) {
|
||||||
nsAutoCString got;
|
nsAutoCString got;
|
||||||
rv = Slurp(rdr, readSize, got);
|
rv = Slurp(rdr, readSize, got);
|
||||||
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
if (NS_FAILED(rv)) {
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
// Corner case: suppress dud message for empty mbox file.
|
// Corner case: suppress dud message for empty mbox file.
|
||||||
if (!rdr->IsNullMessage()) {
|
if (!rdr->IsNullMessage()) {
|
||||||
msgs.AppendElement(got);
|
msgs.AppendElement(got);
|
||||||
|
@ -35,11 +40,14 @@ void ExtractFromMbox(nsACString const& mbox, nsTArray<nsCString>& msgs,
|
||||||
// Try and reuse the MboxMsgInputStream for the next message.
|
// Try and reuse the MboxMsgInputStream for the next message.
|
||||||
bool more;
|
bool more;
|
||||||
rv = rdr->Continue(more);
|
rv = rdr->Continue(more);
|
||||||
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
if (NS_FAILED(rv)) {
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
if (!more) {
|
if (!more) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read all the data out of a stream into a string, reading readSize
|
// Read all the data out of a stream into a string, reading readSize
|
||||||
|
@ -50,7 +58,9 @@ nsresult Slurp(nsIInputStream* src, size_t readSize, nsACString& out) {
|
||||||
while (true) {
|
while (true) {
|
||||||
uint32_t n;
|
uint32_t n;
|
||||||
nsresult rv = src->Read(readbuf.Elements(), readbuf.Length(), &n);
|
nsresult rv = src->Read(readbuf.Elements(), readbuf.Length(), &n);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
if (NS_FAILED(rv)) {
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
if (n == 0) {
|
if (n == 0) {
|
||||||
break; // EOF.
|
break; // EOF.
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,8 @@ namespace testing {
|
||||||
* produce bad results at different read sizes, so this can be used to
|
* produce bad results at different read sizes, so this can be used to
|
||||||
* shake it out by asking for, say, a single byte at a time.
|
* shake it out by asking for, say, a single byte at a time.
|
||||||
*/
|
*/
|
||||||
void ExtractFromMbox(nsACString const& mbox, nsTArray<nsCString>& msgs,
|
nsresult ExtractFromMbox(nsACString const& mbox, nsTArray<nsCString>& msgs,
|
||||||
size_t readSize = 4096);
|
size_t readSize = 4096);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slurp just reads the src stream until EOF, returning the data in
|
* Slurp just reads the src stream until EOF, returning the data in
|
||||||
|
|
|
@ -41,7 +41,8 @@ static void dumpCase(testing::MboxCase const& t,
|
||||||
// readSize lets the caller set the size of the stream Read() calls.
|
// readSize lets the caller set the size of the stream Read() calls.
|
||||||
static void runInputStreamCase(testing::MboxCase const& t, size_t readSize) {
|
static void runInputStreamCase(testing::MboxCase const& t, size_t readSize) {
|
||||||
nsTArray<nsCString> msgs;
|
nsTArray<nsCString> msgs;
|
||||||
testing::ExtractFromMbox(t.mbox, msgs, readSize);
|
nsresult rv = testing::ExtractFromMbox(t.mbox, msgs, readSize);
|
||||||
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
||||||
|
|
||||||
// Display test data + results before asserting to make troubleshooting
|
// Display test data + results before asserting to make troubleshooting
|
||||||
// simpler.
|
// simpler.
|
||||||
|
@ -154,3 +155,17 @@ TEST(TestMsgMboxRead, Ambiguities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test to handle missing From line.
|
||||||
|
// Not too exhaustive (more in xpcshell tests).
|
||||||
|
// Just make sure that a missing "From " line causes an error.
|
||||||
|
TEST(TestMsgMboxRead, MissingFromLine)
|
||||||
|
{
|
||||||
|
nsAutoCString borkedMbox("blah blah blah");
|
||||||
|
|
||||||
|
for (size_t s : {1, 3, 4096}) {
|
||||||
|
nsTArray<nsCString> msgs;
|
||||||
|
nsresult rv = testing::ExtractFromMbox(borkedMbox, msgs, s);
|
||||||
|
ASSERT_EQ(rv, NS_MSG_ERROR_MBOX_MALFORMED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -59,3 +59,41 @@ add_task(async function verifyDownloaded() {
|
||||||
await streamListener.promise;
|
await streamListener.promise;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For mbox, make sure that offline messages fail if the storeTokens
|
||||||
|
* don't point to the beginning of a message.
|
||||||
|
*/
|
||||||
|
add_task(
|
||||||
|
{
|
||||||
|
skip_if: () => IMAPPump.inbox.msgStore.storeType != "mbox",
|
||||||
|
},
|
||||||
|
async function checkBadStoreTokens() {
|
||||||
|
const inbox = IMAPPump.inbox;
|
||||||
|
|
||||||
|
// Corrupt the storeTokens by adding 3.
|
||||||
|
for (const msg of inbox.messages) {
|
||||||
|
const offset = Number(msg.storeToken) + 3;
|
||||||
|
msg.storeToken = offset.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure message reading fails.
|
||||||
|
const NS_MSG_ERROR_MBOX_MALFORMED = 0x80550024;
|
||||||
|
for (const msg of inbox.messages) {
|
||||||
|
const streamListener = new PromiseTestUtils.PromiseStreamListener();
|
||||||
|
const uri = inbox.getUriForMsg(msg);
|
||||||
|
const service = MailServices.messageServiceFromURI(uri);
|
||||||
|
try {
|
||||||
|
service.streamMessage(uri, streamListener, null, null, false, "", true);
|
||||||
|
await streamListener.promise;
|
||||||
|
Assert.ok(false, "Bad storeToken should cause error.");
|
||||||
|
} catch (e) {
|
||||||
|
Assert.equal(
|
||||||
|
e,
|
||||||
|
NS_MSG_ERROR_MBOX_MALFORMED,
|
||||||
|
"Bad storeToken causes NS_MSG_ERROR_MBOX_MALFORMED for mbox"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
const { PromiseTestUtils } = ChromeUtils.importESModule(
|
const { PromiseTestUtils } = ChromeUtils.importESModule(
|
||||||
"resource://testing-common/mailnews/PromiseTestUtils.sys.mjs"
|
"resource://testing-common/mailnews/PromiseTestUtils.sys.mjs"
|
||||||
);
|
);
|
||||||
|
const { MessageGenerator } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/mailnews/MessageGenerator.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
// Force mbox mailstore.
|
// Force mbox mailstore.
|
||||||
Services.prefs.setCharPref(
|
Services.prefs.setCharPref(
|
||||||
|
@ -60,3 +63,45 @@ add_task(async function test_unescapedMbox() {
|
||||||
// Clear up.
|
// Clear up.
|
||||||
localAccountUtils.clearAll();
|
localAccountUtils.clearAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that reading from bad offset fails.
|
||||||
|
*/
|
||||||
|
add_task(async function test_badStoreTokens() {
|
||||||
|
localAccountUtils.loadLocalMailAccount();
|
||||||
|
const inbox = localAccountUtils.inboxFolder;
|
||||||
|
|
||||||
|
// Add some messages to inbox.
|
||||||
|
const generator = new MessageGenerator();
|
||||||
|
inbox.addMessageBatch(
|
||||||
|
generator.makeMessages({ count: 10 }).map(m => m.toMessageString())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Corrupt the storeTokens by adding 3
|
||||||
|
for (const msg of inbox.messages) {
|
||||||
|
const offset = Number(msg.storeToken) + 3;
|
||||||
|
msg.storeToken = offset.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that message reads fail.
|
||||||
|
const NS_MSG_ERROR_MBOX_MALFORMED = 0x80550024;
|
||||||
|
for (const msg of inbox.messages) {
|
||||||
|
const streamListener = new PromiseTestUtils.PromiseStreamListener();
|
||||||
|
const uri = inbox.getUriForMsg(msg);
|
||||||
|
const service = MailServices.messageServiceFromURI(uri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.streamMessage(uri, streamListener, null, null, false, "", true);
|
||||||
|
await streamListener.promise;
|
||||||
|
} catch (e) {
|
||||||
|
Assert.equal(
|
||||||
|
e,
|
||||||
|
NS_MSG_ERROR_MBOX_MALFORMED,
|
||||||
|
"Bad read causes NS_MSG_ERROR_MBOX_MALFORMED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear up.
|
||||||
|
localAccountUtils.clearAll();
|
||||||
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче