emailjs-imap-client
Version:
JavaScript IMAP client
778 lines (639 loc) • 24.6 kB
JavaScript
/* eslint-disable no-unused-expressions */
import ImapClient from './imap'
import { toTypedArray } from './common'
const host = 'localhost'
const port = 10000
describe('browserbox imap unit tests', () => {
var client, socketStub
/* jshint indent:false */
beforeEach(() => {
client = new ImapClient(host, port)
expect(client).to.exist
client.logger = {
debug: () => { },
error: () => { }
}
var Socket = function () { }
Socket.open = () => { }
Socket.prototype.close = () => { }
Socket.prototype.send = () => { }
Socket.prototype.suspend = () => { }
Socket.prototype.resume = () => { }
Socket.prototype.upgradeToSecure = () => { }
socketStub = sinon.createStubInstance(Socket)
sinon.stub(Socket, 'open').withArgs(host, port).returns(socketStub)
var promise = client.connect(Socket).then(() => {
expect(Socket.open.callCount).to.equal(1)
expect(socketStub.onerror).to.exist
expect(socketStub.onopen).to.exist
expect(socketStub.onclose).to.exist
expect(socketStub.ondata).to.exist
})
setTimeout(() => socketStub.onopen(), 10)
return promise
})
describe.skip('#close', () => {
it('should call socket.close', () => {
client.socket.readyState = 'open'
setTimeout(() => socketStub.onclose(), 10)
return client.close().then(() => {
expect(socketStub.close.callCount).to.equal(1)
})
})
it('should not call socket.close', () => {
client.socket.readyState = 'not open. duh.'
setTimeout(() => socketStub.onclose(), 10)
return client.close().then(() => {
expect(socketStub.close.called).to.be.false
})
})
})
describe('#upgrade', () => {
it('should upgrade socket', () => {
client.secureMode = false
client.upgrade()
})
it('should not upgrade socket', () => {
client.secureMode = true
client.upgrade()
})
})
describe('#setHandler', () => {
it('should set global handler for keyword', () => {
var handler = () => { }
client.setHandler('fetch', handler)
expect(client._globalAcceptUntagged.FETCH).to.equal(handler)
})
})
describe('#socket.onerror', () => {
it('should emit error and close connection', (done) => {
client.socket.onerror({
data: new Error('err')
})
client.onerror = () => {
done()
}
})
})
describe('#socket.onclose', () => {
it('should emit error ', (done) => {
client.socket.onclose()
client.onerror = () => {
done()
}
})
})
describe('#_onData', () => {
it('should process input', () => {
sinon.stub(client, '_parseIncomingCommands')
sinon.stub(client, '_iterateIncomingBuffer')
client._onData({
data: toTypedArray('foobar').buffer
})
expect(client._parseIncomingCommands.calledOnce).to.be.true
expect(client._iterateIncomingBuffer.calledOnce).to.be.true
})
})
describe('rateIncomingBuffer', () => {
it('should iterate chunked input', () => {
appendIncomingBuffer('* 1 FETCH (UID 1)\r\n* 2 FETCH (UID 2)\r\n* 3 FETCH (UID 3)\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1)')
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID 2)')
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 3 FETCH (UID 3)')
expect(iterator.next().value).to.be.undefined
})
it('should process chunked literals', () => {
appendIncomingBuffer('* 1 FETCH (UID {1}\r\n1)\r\n* 2 FETCH (UID {4}\r\n2345)\r\n* 3 FETCH (UID {4}\r\n3789)\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID {1}\r\n1)')
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID {4}\r\n2345)')
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 3 FETCH (UID {4}\r\n3789)')
expect(iterator.next().value).to.be.undefined
})
it('should process chunked literals 2', () => {
appendIncomingBuffer('* 1 FETCH (UID 1)\r\n* 2 FETCH (UID {4}\r\n2345)\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1)')
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID {4}\r\n2345)')
expect(iterator.next().value).to.be.undefined
})
it('should process chunked literals 3', () => {
appendIncomingBuffer('* 1 FETCH (UID {1}\r\n1)\r\n* 2 FETCH (UID 4)\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID {1}\r\n1)')
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID 4)')
expect(iterator.next().value).to.be.undefined
})
it('should process chunked literals 4', () => {
appendIncomingBuffer('* SEARCH {1}\r\n1 {1}\r\n2\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* SEARCH {1}\r\n1 {1}\r\n2')
})
it('should process CRLF literal', () => {
appendIncomingBuffer('* 1 FETCH (UID 20 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 20 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)')
})
it('should process CRLF literal 2', () => {
appendIncomingBuffer('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}") BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)\r\n')
var iterator = client._iterateIncomingBuffer()
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)')
})
it('should parse multiple zero-length literals', () => {
appendIncomingBuffer('* 126015 FETCH (UID 585599 BODY[1.2] {0}\r\n BODY[1.1] {0}\r\n)\r\n')
var iterator = client._iterateIncomingBuffer()
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)')
})
it('should process two commands when CRLF arrives in 2 parts', () => {
appendIncomingBuffer('* 1 FETCH (UID 1)\r')
var iterator1 = client._iterateIncomingBuffer()
expect(iterator1.next().value).to.be.undefined
appendIncomingBuffer('\n* 2 FETCH (UID 2)\r\n')
var iterator2 = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID 1)')
expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 2 FETCH (UID 2)')
expect(iterator2.next().value).to.be.undefined
})
it('should process literal when literal count arrives in 2 parts', () => {
appendIncomingBuffer('* 1 FETCH (UID {')
var iterator1 = client._iterateIncomingBuffer()
expect(iterator1.next().value).to.be.undefined
appendIncomingBuffer('2}\r\n12)\r\n')
var iterator2 = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID {2}\r\n12)')
expect(iterator2.next().value).to.be.undefined
})
it('should process literal when literal count arrives in 2 parts 2', () => {
appendIncomingBuffer('* 1 FETCH (UID {1')
var iterator1 = client._iterateIncomingBuffer()
expect(iterator1.next().value).to.be.undefined
appendIncomingBuffer('0}\r\n0123456789)\r\n')
var iterator2 = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID {10}\r\n0123456789)')
expect(iterator2.next().value).to.be.undefined
})
it('should process literal when literal count arrives in 2 parts 3', () => {
appendIncomingBuffer('* 1 FETCH (UID {')
var iterator1 = client._iterateIncomingBuffer()
expect(iterator1.next().value).to.be.undefined
appendIncomingBuffer('10}\r\n1234567890)\r\n')
var iterator2 = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID {10}\r\n1234567890)')
expect(iterator2.next().value).to.be.undefined
})
it('should process literal when literal count arrives in 2 parts 4', () => {
appendIncomingBuffer('* 1 FETCH (UID 1 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r')
var iterator1 = client._iterateIncomingBuffer()
expect(iterator1.next().value).to.be.undefined
appendIncomingBuffer('\nXX)\r\n')
var iterator2 = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID 1 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\nXX)')
})
it('should process literal when literal count arrives in 3 parts', () => {
appendIncomingBuffer('* 1 FETCH (UID {')
var iterator1 = client._iterateIncomingBuffer()
expect(iterator1.next().value).to.be.undefined
appendIncomingBuffer('1')
var iterator2 = client._iterateIncomingBuffer()
expect(iterator2.next().value).to.be.undefined
appendIncomingBuffer('}\r\n1)\r\n')
var iterator3 = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator3.next().value)).to.equal('* 1 FETCH (UID {1}\r\n1)')
expect(iterator3.next().value).to.be.undefined
})
it('should process SEARCH response when it arrives in 2 parts', () => {
appendIncomingBuffer('* SEARCH 1 2')
var iterator1 = client._iterateIncomingBuffer()
expect(iterator1.next().value).to.be.undefined
appendIncomingBuffer(' 3 4\r\n')
var iterator2 = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* SEARCH 1 2 3 4')
expect(iterator2.next().value).to.be.undefined
})
it('should not process {} in string as literal 1', () => {
appendIncomingBuffer('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}"))\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}"))')
})
it('should not process {} in string as literal 2', () => {
appendIncomingBuffer('* 1 FETCH (UID 1 ENVELOPE ("string with number in parenthesis {123}"))\r\n')
var iterator = client._iterateIncomingBuffer()
expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1 ENVELOPE ("string with number in parenthesis {123}"))')
})
function appendIncomingBuffer (content) {
client._incomingBuffers.push(toTypedArray(content))
}
})
describe('#_parseIncomingCommands', () => {
it('should process a tagged item from the queue', () => {
client.onready = sinon.stub()
sinon.stub(client, '_handleResponse')
function * gen () { yield toTypedArray('OK Hello world!') }
client._parseIncomingCommands(gen())
expect(client.onready.callCount).to.equal(1)
expect(client._handleResponse.withArgs({
tag: 'OK',
command: 'Hello',
attributes: [{
type: 'ATOM',
value: 'world!'
}]
}).calledOnce).to.be.true
})
it('should process an untagged item from the queue', () => {
sinon.stub(client, '_handleResponse')
function * gen () { yield toTypedArray('* 1 EXISTS') }
client._parseIncomingCommands(gen())
expect(client._handleResponse.withArgs({
tag: '*',
command: 'EXISTS',
attributes: [],
nr: 1
}).calledOnce).to.be.true
})
it('should process a plus tagged item from the queue', () => {
sinon.stub(client, 'send')
function * gen () { yield toTypedArray('+ Please continue') }
client._currentCommand = {
data: ['literal data']
}
client._parseIncomingCommands(gen())
expect(client.send.withArgs('literal data\r\n').callCount).to.equal(1)
})
it('should process an XOAUTH2 error challenge', () => {
sinon.stub(client, 'send')
function * gen () { yield toTypedArray('+ FOOBAR') }
client._currentCommand = {
data: [],
errorResponseExpectsEmptyLine: true
}
client._parseIncomingCommands(gen())
expect(client.send.withArgs('\r\n').callCount).to.equal(1)
})
})
describe('#_handleResponse', () => {
it('should invoke global handler by default', () => {
sinon.stub(client, '_processResponse')
sinon.stub(client, '_sendRequest')
client._globalAcceptUntagged.TEST = () => { }
sinon.stub(client._globalAcceptUntagged, 'TEST')
client._currentCommand = false
client._handleResponse({
tag: '*',
command: 'test'
})
expect(client._sendRequest.callCount).to.equal(1)
expect(client._globalAcceptUntagged.TEST.withArgs({
tag: '*',
command: 'test'
}).callCount).to.equal(1)
})
it('should invoke global handler if needed', () => {
sinon.stub(client, '_processResponse')
client._globalAcceptUntagged.TEST = () => { }
sinon.stub(client._globalAcceptUntagged, 'TEST')
sinon.stub(client, '_sendRequest')
client._currentCommand = {
payload: {}
}
client._handleResponse({
tag: '*',
command: 'test'
})
expect(client._sendRequest.callCount).to.equal(0)
expect(client._globalAcceptUntagged.TEST.withArgs({
tag: '*',
command: 'test'
}).callCount).to.equal(1)
})
it('should push to payload', () => {
sinon.stub(client, '_processResponse')
client._globalAcceptUntagged.TEST = () => { }
sinon.stub(client._globalAcceptUntagged, 'TEST')
client._currentCommand = {
payload: {
TEST: []
}
}
client._handleResponse({
tag: '*',
command: 'test'
})
expect(client._globalAcceptUntagged.TEST.callCount).to.equal(0)
expect(client._currentCommand.payload.TEST).to.deep.equal([{
tag: '*',
command: 'test'
}])
})
it('should invoke command callback', () => {
sinon.stub(client, '_processResponse')
sinon.stub(client, '_sendRequest')
client._globalAcceptUntagged.TEST = () => { }
sinon.stub(client._globalAcceptUntagged, 'TEST')
client._currentCommand = {
tag: 'A',
callback: (response) => {
expect(response).to.deep.equal({
tag: 'A',
command: 'test',
payload: {
TEST: 'abc'
}
})
},
payload: {
TEST: 'abc'
}
}
client._handleResponse({
tag: 'A',
command: 'test'
})
expect(client._sendRequest.callCount).to.equal(1)
expect(client._globalAcceptUntagged.TEST.callCount).to.equal(0)
})
})
describe('#enqueueCommand', () => {
it('should reject on NO/BAD', () => {
sinon.stub(client, '_sendRequest').callsFake(() => {
client._clientQueue[0].callback({ command: 'NO' })
})
client._tagCounter = 100
client._clientQueue = []
client._canSend = true
return client.enqueueCommand({
command: 'abc'
}, ['def'], {
t: 1
}).catch((err) => {
expect(err).to.exist
})
})
it('should invoke sending', () => {
sinon.stub(client, '_sendRequest').callsFake(() => {
client._clientQueue[0].callback({})
})
client._tagCounter = 100
client._clientQueue = []
client._canSend = true
return client.enqueueCommand({
command: 'abc'
}, ['def'], {
t: 1
}).then(() => {
expect(client._sendRequest.callCount).to.equal(1)
expect(client._clientQueue.length).to.equal(1)
expect(client._clientQueue[0].tag).to.equal('W101')
expect(client._clientQueue[0].request).to.deep.equal({
command: 'abc',
tag: 'W101'
})
expect(client._clientQueue[0].t).to.equal(1)
})
})
it('should only queue', () => {
sinon.stub(client, '_sendRequest')
client._tagCounter = 100
client._clientQueue = []
client._canSend = false
setTimeout(() => { client._clientQueue[0].callback({}) }, 0)
return client.enqueueCommand({
command: 'abc'
}, ['def'], {
t: 1
}).then(() => {
expect(client._sendRequest.callCount).to.equal(0)
expect(client._clientQueue.length).to.equal(1)
expect(client._clientQueue[0].tag).to.equal('W101')
})
})
it('should store valueAsString option in the command', () => {
sinon.stub(client, '_sendRequest')
client._tagCounter = 100
client._clientQueue = []
client._canSend = false
setTimeout(() => { client._clientQueue[0].callback({}) }, 0)
return client.enqueueCommand({
command: 'abc',
valueAsString: false
}, ['def'], {
t: 1
}).then(() => {
expect(client._clientQueue[0].request.valueAsString).to.equal(false)
})
})
})
describe('#_sendRequest', () => {
it('should enter idle if nothing is to process', () => {
sinon.stub(client, '_enterIdle')
client._clientQueue = []
client._sendRequest()
expect(client._enterIdle.callCount).to.equal(1)
})
it('should send data', () => {
sinon.stub(client, '_clearIdle')
sinon.stub(client, 'send')
client._clientQueue = [{
request: {
tag: 'W101',
command: 'TEST'
}
}]
client._sendRequest()
expect(client._clearIdle.callCount).to.equal(1)
expect(client.send.args[0][0]).to.equal('W101 TEST\r\n')
})
it('should send partial data', () => {
sinon.stub(client, '_clearIdle')
sinon.stub(client, 'send')
client._clientQueue = [{
request: {
tag: 'W101',
command: 'TEST',
attributes: [{
type: 'LITERAL',
value: 'abc'
}]
}
}]
client._sendRequest()
expect(client._clearIdle.callCount).to.equal(1)
expect(client.send.args[0][0]).to.equal('W101 TEST {3}\r\n')
expect(client._currentCommand.data).to.deep.equal(['abc'])
})
it('should run precheck', (done) => {
sinon.stub(client, '_clearIdle')
client._canSend = true
client._clientQueue = [{
request: {
tag: 'W101',
command: 'TEST',
attributes: [{
type: 'LITERAL',
value: 'abc'
}]
},
precheck: (ctx) => {
expect(ctx).to.exist
expect(client._canSend).to.be.true
client._sendRequest = () => {
expect(client._clientQueue.length).to.equal(2)
expect(client._clientQueue[0].tag).to.include('.p')
expect(client._clientQueue[0].request.tag).to.include('.p')
client._clearIdle.restore()
done()
}
client.enqueueCommand({}, undefined, {
ctx: ctx
})
return Promise.resolve()
}
}]
client._sendRequest()
})
})
describe('#_enterIdle', () => {
it('should set idle timer', (done) => {
client.onidle = () => {
done()
}
client.timeoutEnterIdle = 1
client._enterIdle()
})
})
describe('#_processResponse', () => {
it('should set humanReadable', () => {
var response = {
tag: '*',
command: 'OK',
attributes: [{
type: 'TEXT',
value: 'Some random text'
}]
}
client._processResponse(response)
expect(response.humanReadable).to.equal('Some random text')
})
it('should set response code', () => {
var response = {
tag: '*',
command: 'OK',
attributes: [{
type: 'ATOM',
section: [{
type: 'ATOM',
value: 'CAPABILITY'
}, {
type: 'ATOM',
value: 'IMAP4REV1'
}, {
type: 'ATOM',
value: 'UIDPLUS'
}]
}, {
type: 'TEXT',
value: 'Some random text'
}]
}
client._processResponse(response)
expect(response.code).to.equal('CAPABILITY')
expect(response.capability).to.deep.equal(['IMAP4REV1', 'UIDPLUS'])
})
})
describe('#isError', () => {
it('should detect if an object is an error', () => {
expect(client.isError(new RangeError('abc'))).to.be.true
expect(client.isError('abc')).to.be.false
})
})
describe('#enableCompression', () => {
it('should create inflater and deflater streams', () => {
client.socket.ondata = () => { }
sinon.stub(client.socket, 'ondata')
expect(client.compressed).to.be.false
client.enableCompression()
expect(client.compressed).to.be.true
const payload = 'asdasd'
const expected = payload.split('').map(char => char.charCodeAt(0))
client.send(payload)
const actualOut = socketStub.send.args[0][0]
client.socket.ondata({ data: actualOut })
expect(Buffer.from(client._socketOnData.args[0][0].data)).to.deep.equal(Buffer.from(expected))
})
})
describe('#getPreviouslyQueued', () => {
const ctx = {}
it('should return undefined with empty queue and no current command', () => {
client._currentCommand = undefined
client._clientQueue = []
expect(testAndGetAttribute()).to.be.undefined
})
it('should return undefined with empty queue and non-SELECT current command', () => {
client._currentCommand = createCommand('TEST')
client._clientQueue = []
expect(testAndGetAttribute()).to.be.undefined
})
it('should return current command with empty queue and SELECT current command', () => {
client._currentCommand = createCommand('SELECT', 'ATTR')
client._clientQueue = []
expect(testAndGetAttribute()).to.equal('ATTR')
})
it('should return current command with non-SELECT commands in queue and SELECT current command', () => {
client._currentCommand = createCommand('SELECT', 'ATTR')
client._clientQueue = [
createCommand('TEST01'),
createCommand('TEST02')
]
expect(testAndGetAttribute()).to.equal('ATTR')
})
it('should return last SELECT before ctx with multiple SELECT commands in queue (1)', () => {
client._currentCommand = createCommand('SELECT', 'ATTR01')
client._clientQueue = [
createCommand('SELECT', 'ATTR'),
createCommand('TEST'),
ctx,
createCommand('SELECT', 'ATTR03')
]
expect(testAndGetAttribute()).to.equal('ATTR')
})
it('should return last SELECT before ctx with multiple SELECT commands in queue (2)', () => {
client._clientQueue = [
createCommand('SELECT', 'ATTR02'),
createCommand('SELECT', 'ATTR'),
ctx,
createCommand('SELECT', 'ATTR03')
]
expect(testAndGetAttribute()).to.equal('ATTR')
})
it('should return last SELECT before ctx with multiple SELECT commands in queue (3)', () => {
client._clientQueue = [
createCommand('SELECT', 'ATTR02'),
createCommand('SELECT', 'ATTR'),
createCommand('TEST'),
ctx,
createCommand('SELECT', 'ATTR03')
]
expect(testAndGetAttribute()).to.equal('ATTR')
})
function testAndGetAttribute () {
const data = client.getPreviouslyQueued(['SELECT'], ctx)
if (data) {
return data.request.attributes[0].value
}
}
function createCommand (command, attribute) {
const attributes = []
const data = {
request: { command, attributes }
}
if (attribute) {
data.request.attributes.push({
type: 'STRING',
value: attribute
})
}
return data
}
})
})