/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */ 'use strict'; let chai = require('chai'); let imapHandler = require('../lib/handler/imap-handler'); let mimetorture = require('./fixtures/mimetorture'); let expect = chai.expect; chai.config.includeStack = true; describe('IMAP Command Parser', function () { describe('get tag', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD').tag).to.equal('TAG1'); }); it('should fail for unexpected WS', function () { expect(function () { imapHandler.parser(' TAG CMD'); }).to.throw(Error); }); it('should * OK ', function () { expect(function () { imapHandler.parser(' TAG CMD'); }).to.throw(Error); }); it('should + OK ', function () { expect(imapHandler.parser('+ TAG CMD').tag).to.equal('+'); }); it('should allow untagged', function () { expect(function () { imapHandler.parser('* CMD'); }).to.not.throw(Error); }); it('should fail for empty tag', function () { expect(function () { imapHandler.parser(''); }).to.throw(Error); }); it('should fail for unexpected end', function () { expect(function () { imapHandler.parser('TAG1'); }).to.throw(Error); }); it('should fail for invalid char', function () { expect(function () { imapHandler.parser('TAG"1 CMD'); }).to.throw(Error); }); }); describe('get arguments', function () { it('should allow trailing whitespace and empty arguments', function () { expect(function () { imapHandler.parser('* SEARCH '); }).to.not.throw(Error); }); }); describe('get command', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD').command).to.equal('CMD'); }); it('should work for multi word command', function () { expect(imapHandler.parser('TAG1 UID FETCH').command).to.equal('UID FETCH'); }); it('should fail for unexpected WS', function () { expect(function () { imapHandler.parser('TAG1 CMD'); }).to.throw(Error); }); it('should fail for empty command', function () { expect(function () { imapHandler.parser('TAG1 '); }).to.throw(Error); }); it('should fail for invalid char', function () { expect(function () { imapHandler.parser('TAG1 CM=D'); }).to.throw(Error); }); }); describe('get attribute', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD FED').attributes).to.deep.equal([{ type: 'ATOM', value: 'FED' }]); }); it('should succeed for single whitespace between values', function () { expect(imapHandler.parser('TAG1 CMD FED TED').attributes).to.deep.equal([{ type: 'ATOM', value: 'FED' }, { type: 'ATOM', value: 'TED' }]); }); it('should succeed for ATOM', function () { expect(imapHandler.parser('TAG1 CMD ABCDE').attributes).to.deep.equal([{ type: 'ATOM', value: 'ABCDE' }]); expect(imapHandler.parser('TAG1 CMD ABCDE DEFGH').attributes).to.deep.equal([{ type: 'ATOM', value: 'ABCDE' }, { type: 'ATOM', value: 'DEFGH' }]); expect(imapHandler.parser('TAG1 CMD %').attributes).to.deep.equal([{ type: 'ATOM', value: '%' }]); expect(imapHandler.parser('TAG1 CMD \\*').attributes).to.deep.equal([{ type: 'ATOM', value: '\\*' }]); expect(imapHandler.parser('12.82 STATUS [Gmail].Trash (UIDNEXT UNSEEN HIGHESTMODSEQ)').attributes).to.deep.equal([ // keep indentation { type: 'ATOM', value: '[Gmail].Trash' }, [{ type: 'ATOM', value: 'UIDNEXT' }, { type: 'ATOM', value: 'UNSEEN' }, { type: 'ATOM', value: 'HIGHESTMODSEQ' }] ]); }); it('should not succeed for ATOM', function () { expect(function () { imapHandler.parser('TAG1 CMD \\*a'); }).to.throw(Error); }); }); describe('get string', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD "ABCDE"').attributes).to.deep.equal([{ type: 'STRING', value: 'ABCDE' }]); expect(imapHandler.parser('TAG1 CMD "ABCDE" "DEFGH"').attributes).to.deep.equal([{ type: 'STRING', value: 'ABCDE' }, { type: 'STRING', value: 'DEFGH' }]); }); it('should not explode on invalid char', function () { expect(imapHandler.parser('* 1 FETCH (BODY[] "\xc2")').attributes).to.deep.equal([ // keep indentation { type: 'ATOM', value: 'FETCH' }, [{ type: 'ATOM', value: 'BODY', section: [] }, { type: 'STRING', value: '\xc2' }] ]); }); }); describe('get list', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD (1234)').attributes).to.deep.equal([ [{ type: 'ATOM', value: '1234' }] ]); expect(imapHandler.parser('TAG1 CMD (1234 TERE)').attributes).to.deep.equal([ [{ type: 'ATOM', value: '1234' }, { type: 'ATOM', value: 'TERE' }] ]); expect(imapHandler.parser('TAG1 CMD (1234)(TERE)').attributes).to.deep.equal([ [{ type: 'ATOM', value: '1234' }], [{ type: 'ATOM', value: 'TERE' }] ]); expect(imapHandler.parser('TAG1 CMD ( 1234)').attributes).to.deep.equal([ [{ type: 'ATOM', value: '1234' }] ]); // Trailing whitespace in a BODYSTRUCTURE atom list has been // observed on yahoo.co.jp's expect(imapHandler.parser('TAG1 CMD (1234 )').attributes).to.deep.equal([ [{ type: 'ATOM', value: '1234' }] ]); expect(imapHandler.parser('TAG1 CMD (1234) ').attributes).to.deep.equal([ [{ type: 'ATOM', value: '1234' }] ]); }); }); describe('nested list', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD (((TERE)) VANA)').attributes).to.deep.equal([ [ [ [{ type: 'ATOM', value: 'TERE' }] ], { type: 'ATOM', value: 'VANA' } ] ]); expect(imapHandler.parser('TAG1 CMD (( (TERE)) VANA)').attributes).to.deep.equal([ [ [ [{ type: 'ATOM', value: 'TERE' }] ], { type: 'ATOM', value: 'VANA' } ] ]); expect(imapHandler.parser('TAG1 CMD (((TERE) ) VANA)').attributes).to.deep.equal([ [ [ [{ type: 'ATOM', value: 'TERE' }] ], { type: 'ATOM', value: 'VANA' } ] ]); }); }); describe('get literal', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD {4}\r\nabcd').attributes).to.deep.equal([{ type: 'LITERAL', value: 'abcd' }]); expect(imapHandler.parser('TAG1 CMD {4}\r\nabcd {4}\r\nkere').attributes).to.deep.equal([{ type: 'LITERAL', value: 'abcd' }, { type: 'LITERAL', value: 'kere' }]); expect(imapHandler.parser('TAG1 CMD ({4}\r\nabcd {4}\r\nkere)').attributes).to.deep.equal([ [{ type: 'LITERAL', value: 'abcd' }, { type: 'LITERAL', value: 'kere' }] ]); }); it('should fail', function () { expect(function () { imapHandler.parser('TAG1 CMD {4}\r\nabcd{4} \r\nkere'); }).to.throw(Error); }); it('should allow zero length literal in the end of a list', function () { expect(imapHandler.parser('TAG1 CMD ({0}\r\n)').attributes).to.deep.equal([ [{ type: 'LITERAL', value: '' }] ]); }); }); describe('ATOM Section', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD BODY[]').attributes).to.deep.equal([{ type: 'ATOM', value: 'BODY', section: [] }]); expect(imapHandler.parser('TAG1 CMD BODY[(KERE)]').attributes).to.deep.equal([{ type: 'ATOM', value: 'BODY', section: [ [{ type: 'ATOM', value: 'KERE' }] ] }]); }); it('will not fail due to trailing whitespace', function () { // We intentionally have trailing whitespace in the section here // because we altered the parser to handle this when we made it // legal for lists and it makes sense to accordingly test it. // However, we have no recorded incidences of this happening in // reality (unlike for lists). expect(imapHandler.parser('TAG1 CMD BODY[HEADER.FIELDS (Subject From) ]').attributes).to.deep.equal([{ type: 'ATOM', value: 'BODY', section: [ // keep indentation { type: 'ATOM', value: 'HEADER.FIELDS' }, [{ type: 'ATOM', value: 'Subject' }, { type: 'ATOM', value: 'From' }] ] }]); }); it('should fail where default BODY and BODY.PEEK are allowed to have sections', function () {}); expect(function () { imapHandler.parser('TAG1 CMD KODY[]'); }).to.throw(Error); }); describe('Human readable', function () { it('should succeed', function () { expect(imapHandler.parser('* OK [CAPABILITY IDLE] Hello world!')).to.deep.equal({ command: 'OK', tag: '*', attributes: [{ section: [{ type: 'ATOM', value: 'CAPABILITY' }, { type: 'ATOM', value: 'IDLE' }], type: 'ATOM', value: '' }, { type: 'TEXT', value: 'Hello world!' }] }); expect(imapHandler.parser('* OK Hello world!')).to.deep.equal({ command: 'OK', tag: '*', attributes: [{ type: 'TEXT', value: 'Hello world!' }] }); expect(imapHandler.parser('* OK')).to.deep.equal({ command: 'OK', tag: '*' }); // USEATTR is from RFC6154; we are testing that just an ATOM // on its own will parse successfully here. (All of the // RFC5530 codes are also single atoms.) expect(imapHandler.parser('TAG1 OK [USEATTR] \\All not supported')).to.deep.equal({ tag: 'TAG1', command: 'OK', attributes: [{ type: 'ATOM', value: '', section: [{ type: 'ATOM', value: 'USEATTR' }] }, { type: 'TEXT', value: '\\All not supported' }] }); // RFC5267 defines the NOUPDATE error. Including for quote / // string coverage. expect(imapHandler.parser('* NO [NOUPDATE "B02"] Too many contexts')).to.deep.equal({ tag: '*', command: 'NO', attributes: [{ type: 'ATOM', value: '', section: [{ type: 'ATOM', value: 'NOUPDATE' }, { type: 'STRING', value: 'B02' }] }, { type: 'TEXT', value: 'Too many contexts' }] }); // RFC5464 defines the METADATA response code; adding this to // ensure the transition for when '2199' hits ']' is handled // safely. expect(imapHandler.parser('TAG1 OK [METADATA LONGENTRIES 2199] GETMETADATA complete')).to.deep.equal({ tag: 'TAG1', command: 'OK', attributes: [{ type: 'ATOM', value: '', section: [{ type: 'ATOM', value: 'METADATA' }, { type: 'ATOM', value: 'LONGENTRIES' }, { type: 'ATOM', value: '2199' }] }, { type: 'TEXT', value: 'GETMETADATA complete' }] }); // RFC4467 defines URLMECH. Included because of the example // third atom involves base64-encoding which is somewhat unusual expect(imapHandler.parser('TAG1 OK [URLMECH INTERNAL XSAMPLE=P34OKhO7VEkCbsiYY8rGEg==] done')).to.deep.equal({ tag: 'TAG1', command: 'OK', attributes: [{ type: 'ATOM', value: '', section: [{ type: 'ATOM', value: 'URLMECH' }, { type: 'ATOM', value: 'INTERNAL' }, { type: 'ATOM', value: 'XSAMPLE=P34OKhO7VEkCbsiYY8rGEg==' }] }, { type: 'TEXT', value: 'done' }] }); // RFC2221 defines REFERRAL where the argument is an imapurl // (defined by RFC2192 which is obsoleted by RFC5092) which // is significantly more complicated than the rest of the IMAP // grammar and which was based on the RFC2060 grammar where // resp_text_code included: // atom [SPACE 1*] // So this is just a test case of our explicit special-casing // of REFERRAL. expect(imapHandler.parser('TAG1 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/] Remote Server')).to.deep.equal({ tag: 'TAG1', command: 'NO', attributes: [{ type: 'ATOM', value: '', section: [{ type: 'ATOM', value: 'REFERRAL' }, { type: 'ATOM', value: 'IMAP://user;AUTH=*@SERVER2/' }] }, { type: 'TEXT', value: 'Remote Server' }] }); // PERMANENTFLAGS is from RFC3501. Its syntax is also very // similar to BADCHARSET, except BADCHARSET has astrings // inside the list. expect(imapHandler.parser('* OK [PERMANENTFLAGS (de:hacking $label kt-evalution [css3-page] \\*)] Flags permitted.')).to.deep.equal({ tag: '*', command: 'OK', attributes: [{ type: 'ATOM', value: '', section: [ // keep indentation { type: 'ATOM', value: 'PERMANENTFLAGS' }, [{ type: 'ATOM', value: 'de:hacking' }, { type: 'ATOM', value: '$label' }, { type: 'ATOM', value: 'kt-evalution' }, { type: 'ATOM', value: '[css3-page]' }, { type: 'ATOM', value: '\\*' }] ] }, { type: 'TEXT', value: 'Flags permitted.' }] }); // COPYUID is from RFC4315 and included the previously failing // parsing situation of a sequence terminated by ']' rather than // whitespace. expect(imapHandler.parser('TAG1 OK [COPYUID 4 1417051618:1417051620 1421730687:1421730689] COPY completed')).to.deep.equal({ tag: 'TAG1', command: 'OK', attributes: [{ type: 'ATOM', value: '', section: [{ type: 'ATOM', value: 'COPYUID' }, { type: 'ATOM', value: '4' }, { type: 'SEQUENCE', value: '1417051618:1417051620' }, { type: 'SEQUENCE', value: '1421730687:1421730689' }] }, { type: 'TEXT', value: 'COPY completed' }] }); // MODIFIED is from RFC4551 and is basically the same situation // as the COPYUID case, but in this case our example sequences // have commas in them. (Note that if there was no comma, the // '7,9' payload would end up an ATOM.) expect(imapHandler.parser('TAG1 OK [MODIFIED 7,9] Conditional STORE failed')).to.deep.equal({ tag: 'TAG1', command: 'OK', attributes: [{ type: 'ATOM', value: '', section: [{ type: 'ATOM', value: 'MODIFIED' }, { type: 'SEQUENCE', value: '7,9' }] }, { type: 'TEXT', value: 'Conditional STORE failed' }] }); }); }); describe('ATOM Partial', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD BODY[]<0>').attributes).to.deep.equal([{ type: 'ATOM', value: 'BODY', section: [], partial: [0] }]); expect(imapHandler.parser('TAG1 CMD BODY[]<12.45>').attributes).to.deep.equal([{ type: 'ATOM', value: 'BODY', section: [], partial: [12, 45] }]); expect(imapHandler.parser('TAG1 CMD BODY[HEADER.FIELDS (Subject From)]<12.45>').attributes).to.deep.equal([{ type: 'ATOM', value: 'BODY', section: [ // keep indentation { type: 'ATOM', value: 'HEADER.FIELDS' }, [{ type: 'ATOM', value: 'Subject' }, { type: 'ATOM', value: 'From' }] ], partial: [12, 45] }]); }); it('should fail', function () { expect(function () { imapHandler.parser('TAG1 CMD KODY<0.123>'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD BODY[]<01>'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD BODY[]<0.01>'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD BODY[]<0.1.>'); }).to.throw(Error); }); }); describe('SEQUENCE', function () { it('should succeed', function () { expect(imapHandler.parser('TAG1 CMD *:4,5:7 TEST').attributes).to.deep.equal([{ type: 'SEQUENCE', value: '*:4,5:7' }, { type: 'ATOM', value: 'TEST' }]); expect(imapHandler.parser('TAG1 CMD 1:* TEST').attributes).to.deep.equal([{ type: 'SEQUENCE', value: '1:*' }, { type: 'ATOM', value: 'TEST' }]); expect(imapHandler.parser('TAG1 CMD *:4 TEST').attributes).to.deep.equal([{ type: 'SEQUENCE', value: '*:4' }, { type: 'ATOM', value: 'TEST' }]); }); it('should fail', function () { expect(function () { imapHandler.parser('TAG1 CMD *:4,5:'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD *:4,5:TEST TEST'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD *:4,5: TEST'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD *4,5 TEST'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD *,5 TEST'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD 5,* TEST'); }).to.throw(Error); expect(function () { imapHandler.parser('TAG1 CMD 5, TEST'); }).to.throw(Error); }); }); describe('Escaped quotes', function () { it('should succeed', function () { expect(imapHandler.parser('* 331 FETCH (ENVELOPE ("=?ISO-8859-1?Q?\\"G=FCnter__Hammerl\\"?="))').attributes).to.deep.equal([ // keep indentation { type: 'ATOM', value: 'FETCH' }, [ // keep indentation { type: 'ATOM', value: 'ENVELOPE' }, [{ type: 'STRING', value: '=?ISO-8859-1?Q?"G=FCnter__Hammerl"?=' }] ] ]); }); }); describe('MimeTorture', function () { it('should parse mimetorture input', function () { let parsed; expect(function () { parsed = imapHandler.parser(mimetorture.input); }).to.not.throw(Error); expect(parsed).to.deep.equal(mimetorture.output); }); }); });