Skip to content

Commit cb47736

Browse files
committed
Parse multiple zero-length literals when constructing IMAP command emailjs#147
Turns out the issue is fixed after rewriting _iterateIncomingBuffer to be more state machine like. Improves readability.
1 parent cc77a8c commit cb47736

File tree

2 files changed

+111
-90
lines changed

2 files changed

+111
-90
lines changed

src/emailjs-imap-client-imap.js

Lines changed: 105 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626

2727
var ASCII_PLUS = 43;
2828

29+
// State tracking when constructing an IMAP command from buffers.
30+
var BUFFER_STATE_LITERAL = 'literal';
31+
var BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_1 = 'literal_length_1';
32+
var BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_2 = 'literal_length_2';
33+
var BUFFER_STATE_DEFAULT = 'default';
34+
2935
/**
3036
* Creates a connection object to an IMAP server. Call `connect` method to inititate
3137
* the actual connection, the constructor only defines the properties but does not actually connect.
@@ -71,6 +77,7 @@
7177

7278
// As the server sends data in chunks, it needs to be split into separate lines. Helps parsing the input.
7379
this._incomingBuffers = [];
80+
this._bufferState = BUFFER_STATE_DEFAULT;
7481
this._literalRemaining = 0;
7582

7683
//
@@ -408,104 +415,112 @@
408415
};
409416

410417
Imap.prototype._iterateIncomingBuffer = function*() {
411-
let buf;
412-
if (this._concatLastTwoBuffers) {
413-
// allocate new buffer for the sake of simpler parsing
414-
delete this._concatLastTwoBuffers;
415-
const latest = this._incomingBuffers.pop();
416-
const prevBuf = this._incomingBuffers[this._incomingBuffers.length-1];
417-
buf = new Uint8Array(prevBuf.length + latest.length);
418-
buf.set(prevBuf);
419-
buf.set(latest, prevBuf.length);
420-
this._incomingBuffers[this._incomingBuffers.length-1] = buf;
421-
} else {
422-
buf = this._incomingBuffers[this._incomingBuffers.length-1];
423-
}
418+
let buf = this._incomingBuffers[this._incomingBuffers.length-1];
424419
let i = 0;
425420

426421
// loop invariant:
427422
// this._incomingBuffers starts with the beginning of incoming command.
428423
// buf is shorthand for last element of this._incomingBuffers.
429424
// buf[0..i-1] is part of incoming command.
430425
while (i < buf.length) {
431-
if (this._literalRemaining === 0) {
432-
const leftIdx = buf.indexOf(LEFT_CURLY_BRACKET, i);
433-
if (leftIdx > -1) {
434-
const leftOfLeftCurly = new Uint8Array(buf.buffer, i, leftIdx-i);
435-
if (leftOfLeftCurly.indexOf(LINE_FEED) === -1) {
436-
let j = leftIdx + 1;
437-
while (buf[j] >= 48 && buf[j] <= 57) { // digits
438-
j++;
439-
}
440-
if (j+3 >= buf.length) {
441-
// not enough info to determine if this is literal length
442-
this._concatLastTwoBuffers = true;
443-
return;
444-
}
445-
if (j > leftIdx + 1 &&
446-
buf[j] === RIGHT_CURLY_BRACKET &&
447-
buf[j+1] === CARRIAGE_RETURN &&
448-
buf[j+2] === LINE_FEED) {
449-
const numBuf = buf.subarray(leftIdx+1, j);
450-
this._literalRemaining = Number(mimecodec.fromTypedArray(numBuf));
451-
i = j + 3;
452-
} else {
453-
i = j;
454-
continue; // not a literal but there might still be one
455-
}
456-
}
457-
}
458-
}
459-
460-
const diff = Math.min(buf.length-i, this._literalRemaining);
461-
if (diff) {
462-
this._literalRemaining -= diff;
463-
i += diff;
464-
if (this._literalRemaining === 0) {
465-
continue; // find another literal
466-
}
467-
}
468-
469-
if (this._literalRemaining === 0 && i < buf.length) {
470-
const LFidx = buf.indexOf(LINE_FEED, i);
471-
if (LFidx > -1) {
472-
if (LFidx < buf.length-1) {
473-
this._incomingBuffers[this._incomingBuffers.length-1] = new Uint8Array(buf.buffer, 0, LFidx+1);
474-
}
475-
const commandLength = this._incomingBuffers.reduce((prev, curr) => prev + curr.length, 0) - 2; // 2 for CRLF
476-
const command = new Uint8Array(commandLength);
477-
let index = 0;
478-
while (this._incomingBuffers.length > 0) {
479-
let uint8Array = this._incomingBuffers.shift();
480-
481-
const remainingLength = commandLength - index;
482-
if (uint8Array.length > remainingLength) {
483-
const excessLength = uint8Array.length - remainingLength;
484-
uint8Array = uint8Array.subarray(0, -excessLength);
485-
486-
if (this._incomingBuffers.length > 0) {
487-
this._incomingBuffers = [];
488-
}
489-
}
490-
command.set(uint8Array, index);
491-
index += uint8Array.length;
492-
}
493-
yield command;
494-
if (LFidx < buf.length-1) {
495-
buf = new Uint8Array(buf.subarray(LFidx+1));
496-
this._incomingBuffers.push(buf);
497-
i = 0;
498-
} else {
499-
// clear the timeout when an entire command has arrived
500-
// and not waiting on more data for next command
501-
clearTimeout(this._socketTimeoutTimer);
502-
this._socketTimeoutTimer = null;
426+
switch (this._bufferState) {
427+
case BUFFER_STATE_LITERAL:
428+
const diff = Math.min(buf.length-i, this._literalRemaining);
429+
this._literalRemaining -= diff;
430+
i += diff;
431+
if (this._literalRemaining === 0) {
432+
this._bufferState = BUFFER_STATE_DEFAULT;
433+
}
434+
continue;
435+
436+
case BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_2:
437+
if (i < buf.length) {
438+
if (buf[i] === CARRIAGE_RETURN) {
439+
this._literalRemaining = Number(mimecodec.fromTypedArray(this._lengthBuffer)) + 2; // for CRLF
440+
this._bufferState = BUFFER_STATE_LITERAL;
441+
} else {
442+
this._bufferState = BUFFER_STATE_DEFAULT;
443+
}
444+
delete this._lengthBuffer;
445+
}
446+
continue;
447+
448+
case BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_1:
449+
const start = i;
450+
while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) { // digits
451+
i++;
452+
}
453+
if (start !== i) {
454+
const latest = buf.subarray(start, i);
455+
const prevBuf = this._lengthBuffer;
456+
this._lengthBuffer = new Uint8Array(prevBuf.length + latest.length);
457+
this._lengthBuffer.set(prevBuf);
458+
this._lengthBuffer.set(latest, prevBuf.length);
459+
}
460+
if (i < buf.length) {
461+
if (this._lengthBuffer.length > 0 && buf[i] === RIGHT_CURLY_BRACKET) {
462+
this._bufferState = BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_2;
463+
} else {
464+
delete this._lengthBuffer;
465+
this._bufferState = BUFFER_STATE_DEFAULT;
466+
}
467+
i++;
468+
}
469+
continue;
470+
471+
default:
472+
// find literal length
473+
const leftIdx = buf.indexOf(LEFT_CURLY_BRACKET, i);
474+
if (leftIdx > -1) {
475+
const leftOfLeftCurly = new Uint8Array(buf.buffer, i, leftIdx-i);
476+
if (leftOfLeftCurly.indexOf(LINE_FEED) === -1) {
477+
i = leftIdx + 1;
478+
this._lengthBuffer = new Uint8Array(0);
479+
this._bufferState = BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_1;
480+
continue;
481+
}
482+
}
483+
484+
// find end of command
485+
const LFidx = buf.indexOf(LINE_FEED, i);
486+
if (LFidx > -1) {
487+
if (LFidx < buf.length-1) {
488+
this._incomingBuffers[this._incomingBuffers.length-1] = new Uint8Array(buf.buffer, 0, LFidx+1);
489+
}
490+
const commandLength = this._incomingBuffers.reduce((prev, curr) => prev + curr.length, 0) - 2; // 2 for CRLF
491+
const command = new Uint8Array(commandLength);
492+
let index = 0;
493+
while (this._incomingBuffers.length > 0) {
494+
let uint8Array = this._incomingBuffers.shift();
495+
496+
const remainingLength = commandLength - index;
497+
if (uint8Array.length > remainingLength) {
498+
const excessLength = uint8Array.length - remainingLength;
499+
uint8Array = uint8Array.subarray(0, -excessLength);
500+
501+
if (this._incomingBuffers.length > 0) {
502+
this._incomingBuffers = [];
503+
}
504+
}
505+
command.set(uint8Array, index);
506+
index += uint8Array.length;
507+
}
508+
yield command;
509+
if (LFidx < buf.length-1) {
510+
buf = new Uint8Array(buf.subarray(LFidx+1));
511+
this._incomingBuffers.push(buf);
512+
i = 0;
513+
} else {
514+
// clear the timeout when an entire command has arrived
515+
// and not waiting on more data for next command
516+
clearTimeout(this._socketTimeoutTimer);
517+
this._socketTimeoutTimer = null;
518+
return;
519+
}
520+
} else {
503521
return;
504-
}
505-
} else {
506-
return;
507-
}
508-
}
522+
}
523+
}
509524
}
510525
};
511526

test/unit/emailjs-imap-client-imap-test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@
188188
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal ('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}") BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)');
189189
});
190190

191+
it('should parse multiple zero-length literals', () => {
192+
appendIncomingBuffer('* 126015 FETCH (UID 585599 BODY[1.2] {0}\r\n BODY[1.1] {0}\r\n)\r\n');
193+
var iterator = client._iterateIncomingBuffer();
194+
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 126015 FETCH (UID 585599 BODY[1.2] {0}\r\n BODY[1.1] {0}\r\n)');
195+
});
196+
191197
it('should process two commands when CRLF arrives in 2 parts', () => {
192198
appendIncomingBuffer('* 1 FETCH (UID 1)\r');
193199
var iterator1 = client._iterateIncomingBuffer();

0 commit comments

Comments
 (0)