Skip to content

Commit 33a2bf5

Browse files
authored
Merge pull request #15 from arnt/support-utf8-accept-and-notify
Connection, Parser: Support NOTIFY and UTF8=ACCEPT
2 parents 58b477a + 8e14ce4 commit 33a2bf5

4 files changed

Lines changed: 125 additions & 21 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,5 @@ TODO
779779
Several things not yet implemented in no particular order:
780780
781781
* Support additional IMAP commands/extensions:
782-
* NOTIFY (via NOTIFY extension -- RFC5465)
783782
* STATUS addition to LIST (via LIST-STATUS extension -- RFC5819)
784783
* QRESYNC (RFC5162)

lib/Connection.js

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@ class Connection extends EventEmitter {
284284
serverSupports(cap) {
285285
return (this._caps && this._caps.indexOf(cap) > -1)
286286
}
287+
serverEnabled(cap) {
288+
return (this._enabled && this._enabled.indexOf(cap) > -1);
289+
}
290+
escapeBox(str) {
291+
return escape(this.serverEnabled("UTF8=ACCEPT") ? (''+str) : utf7.encode(''+str));
292+
}
287293
destroy() {
288294
this._queue = []
289295
this._curReq = undefined
@@ -310,7 +316,7 @@ class Connection extends EventEmitter {
310316
else
311317
options.mailbox = this._box.name
312318
}
313-
let cmd = 'APPEND "' + escape(utf7.encode('' + options.mailbox)) + '"'
319+
let cmd = 'APPEND "' + this.escapeBox(options.mailbox) + '"'
314320
if (options.flags) {
315321
if (!Array.isArray(options.flags))
316322
options.flags = [options.flags]
@@ -359,7 +365,7 @@ class Connection extends EventEmitter {
359365
cb = namespace
360366
namespace = ''
361367
}
362-
namespace = escape(utf7.encode('' + namespace))
368+
namespace = this.escapeBox(namespace)
363369
this._enqueue('LIST "' + namespace + '" "*"', cb)
364370
}
365371
id(identification, cb) {
@@ -392,7 +398,7 @@ class Connection extends EventEmitter {
392398
readOnly = false
393399
}
394400
name = '' + name
395-
const encname = escape(utf7.encode(name))
401+
const encname = this.escapeBox(name)
396402
let cmd = (readOnly ? 'EXAMINE' : 'SELECT'), self = this
397403
cmd += ' "' + encname + '"'
398404
if (this.serverSupports('CONDSTORE'))
@@ -445,13 +451,13 @@ class Connection extends EventEmitter {
445451
}
446452
}
447453
addBox(name, cb) {
448-
this._enqueue('CREATE "' + escape(utf7.encode('' + name)) + '"', cb)
454+
this._enqueue('CREATE "' + this.escapeBox(name) + '"', cb)
449455
}
450456
delBox(name, cb) {
451-
this._enqueue('DELETE "' + escape(utf7.encode('' + name)) + '"', cb)
457+
this._enqueue('DELETE "' + this.escapeBox(name) + '"', cb)
452458
}
453459
renameBox(oldname, newname, cb) {
454-
const encoldname = escape(utf7.encode('' + oldname)), encnewname = escape(utf7.encode('' + newname)), self = this
460+
const encoldname = this.escapeBox(oldname), encnewname = this.escapeBox(newname), self = this
455461
this._enqueue('RENAME "' + encoldname + '" "' + encnewname + '"', function (err) {
456462
if (err)
457463
return cb(err)
@@ -466,23 +472,23 @@ class Connection extends EventEmitter {
466472
})
467473
}
468474
subscribeBox(name, cb) {
469-
this._enqueue('SUBSCRIBE "' + escape(utf7.encode('' + name)) + '"', cb)
475+
this._enqueue('SUBSCRIBE "' + this.escapeBox(name) + '"', cb)
470476
}
471477
unsubscribeBox(name, cb) {
472-
this._enqueue('UNSUBSCRIBE "' + escape(utf7.encode('' + name)) + '"', cb)
478+
this._enqueue('UNSUBSCRIBE "' + this.escapeBox(name) + '"', cb)
473479
}
474480
getSubscribedBoxes(namespace, cb) {
475481
if (typeof namespace === 'function') {
476482
cb = namespace
477483
namespace = ''
478484
}
479-
namespace = escape(utf7.encode('' + namespace))
485+
namespace = this.escapeBox(namespace)
480486
this._enqueue('LSUB "' + namespace + '" "*"', cb)
481487
}
482488
status(boxName, cb) {
483489
if (this._box && this._box.name === boxName)
484490
throw new Error('Cannot call status on currently selected mailbox')
485-
boxName = escape(utf7.encode('' + boxName))
491+
boxName = this.escapeBox(boxName)
486492
let info = ['MESSAGES', 'RECENT', 'UNSEEN', 'UIDVALIDITY', 'UIDNEXT']
487493
if (this.serverSupports('CONDSTORE'))
488494
info.push('HIGHESTMODSEQ')
@@ -606,7 +612,7 @@ class Connection extends EventEmitter {
606612
+ (which === '' ? 'sequence number' : 'uid')
607613
+ 'list')
608614
}
609-
boxTo = escape(utf7.encode('' + boxTo))
615+
boxTo = this.escapeBox(boxTo)
610616
this._enqueue(which + 'COPY ' + uids.join(',') + ' "' + boxTo + '"', cb)
611617
}
612618
move(uids, boxTo, cb) {
@@ -625,7 +631,7 @@ class Connection extends EventEmitter {
625631
+ 'list')
626632
}
627633
uids = uids.join(',')
628-
boxTo = escape(utf7.encode('' + boxTo))
634+
boxTo = this.escapeBox(boxTo)
629635
this._enqueue(which + 'MOVE ' + uids + ' "' + boxTo + '"', cb)
630636
}
631637
else if (this._box.permFlags.indexOf('\\Deleted') === -1
@@ -809,7 +815,7 @@ class Connection extends EventEmitter {
809815
if (!Array.isArray(labels))
810816
labels = [labels]
811817
labels = labels.map(function (v) {
812-
return '"' + escape(utf7.encode('' + v)) + '"'
818+
return '"' + this.escapeBox(v) + '"'
813819
}).join(' ')
814820
uids = uids.join(',')
815821
this._enqueue(which + 'STORE ' + uids + ' ' + mode
@@ -920,23 +926,23 @@ class Connection extends EventEmitter {
920926
triplets += ' '
921927
triplets += l + ' ' + limits[l]
922928
}
923-
quotaRoot = escape(utf7.encode('' + quotaRoot))
929+
quotaRoot = this.escapeBox(quotaRoot)
924930
this._enqueue('SETQUOTA "' + quotaRoot + '" (' + triplets + ')', function (err, quotalist) {
925931
if (err)
926932
return cb(err)
927933
cb(err, quotalist ? quotalist[0] : limits)
928934
})
929935
}
930936
getQuota(quotaRoot, cb) {
931-
quotaRoot = escape(utf7.encode('' + quotaRoot))
937+
quotaRoot = this.escapeBox(quotaRoot)
932938
this._enqueue('GETQUOTA "' + quotaRoot + '"', function (err, quotalist) {
933939
if (err)
934940
return cb(err)
935941
cb(err, quotalist[0])
936942
})
937943
}
938944
getQuotaRoot(boxName, cb) {
939-
boxName = escape(utf7.encode('' + boxName))
945+
boxName = this.escapeBox(boxName)
940946
this._enqueue('GETQUOTAROOT "' + boxName + '"', function (err, quotalist) {
941947
if (err)
942948
return cb(err)
@@ -997,8 +1003,14 @@ class Connection extends EventEmitter {
9971003
this.namespaces = info.text
9981004
else if (type === 'id')
9991005
this._curReq.cbargs.push(info.text)
1000-
else if (type === 'capability')
1006+
else if (type === 'capability') {
10011007
this._caps = info.text.map(function (v) { return v.toUpperCase() })
1008+
if (this.serverSupports('ENABLE'))
1009+
this._enqueue('ENABLE UTF8=ACCEPT CONDSTORE', function() {});
1010+
if (this.serverSupports('NOTIFY'))
1011+
this._enqueue('NOTIFY SET (SELECTED-DELAYED (MESSAGENEW (UID) MESSAGEEXPUNGE))', function() {});
1012+
} else if (type === 'enabled')
1013+
this._enabled = info.text.map(function(v) { return v.toUpperCase(); });
10021014
else if (type === 'preauth')
10031015
this.state = 'authenticated'
10041016
else if (type === 'sort' || type === 'thread' || type === 'esearch')
@@ -1054,7 +1066,7 @@ class Connection extends EventEmitter {
10541066

10551067
}
10561068
else if (type === 'exists') {
1057-
if (!this._box && RE_OPENBOX.test(this._curReq.type))
1069+
if (!this._box && this._curReq && RE_OPENBOX.test(this._curReq.type))
10581070
this._createCurrentBox()
10591071
if (this._box) {
10601072
let prev = this._box.messages.total, now = info.num
@@ -1183,7 +1195,7 @@ class Connection extends EventEmitter {
11831195
this._curReq.cbargs.push(box)
11841196
}
11851197
else if (type === 'fetch') {
1186-
if (/^(?:UID )?FETCH/.test(this._curReq.fullcmd)) {
1198+
if (this._curReq && /^(?:UID )?FETCH/.test(this._curReq.fullcmd)) {
11871199
// FETCH response sent as result of FETCH request
11881200
const msg = this._curReq.fetchCache[info.num], keys = Object.keys(info.text), keyslen = keys.length
11891201
let toget, msgEmitter, j

lib/Parser.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const CH_LF = 10,
2020
RE_SEQNO = /^\* (\d+)/,
2121
RE_LISTCONTENT = /^\((.*)\)$/,
2222
RE_LITERAL = /\{(\d+)\}$/,
23-
RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|ID|LIST|XLIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT|THREAD|ESEARCH|QUOTA|QUOTAROOT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?:(?: \[([^\]]+)\])?(?: (.+))?)?$/i,
23+
RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|ID|LIST|XLIST|LSUB|SEARCH|STATUS|CAPABILITY|ENABLED|NAMESPACE|PREAUTH|SORT|THREAD|ESEARCH|QUOTA|QUOTAROOT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?:(?: \[([^\]]+)\])?(?: (.+))?)?$/i,
2424
RE_TAGGED = /^A(\d+) (OK|NO|BAD) ?(?:\[([^\]]+)\] )?(.*)$/i,
2525
RE_CONTINUE = /^\+(?: (?:\[([^\]]+)\] )?(.+))?$/i,
2626
RE_CRLF = /\r\n/g,
@@ -219,6 +219,7 @@ class Parser extends EventEmitter {
219219
if (type === 'flags'
220220
|| type === 'search'
221221
|| type === 'capability'
222+
|| type === 'enabled'
222223
|| type === 'sort') {
223224
if (m[5]) {
224225
if (type === 'search' && RE_SEARCH_MODSEQ.test(m[5])) {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
var assert = require('assert'),
2+
net = require('net'),
3+
Imap = require('../lib/Connection'),
4+
result;
5+
6+
var CRLF = '\r\n';
7+
8+
var RESPONSES = [
9+
['* CAPABILITY IMAP4rev1',
10+
'A0 OK Thats all she wrote!',
11+
''
12+
].join(CRLF),
13+
['* CAPABILITY IMAP4rev1 ENABLE NOTIFY',
14+
'A1 OK authenticated (Success)',
15+
''
16+
].join(CRLF),
17+
['* ENABLED',
18+
'A2 OK Success',
19+
''
20+
].join(CRLF),
21+
['A3 OK Success',
22+
''
23+
].join(CRLF),
24+
['* LIST (\\Noselect) "/" "/"',
25+
'A4 OK Success',
26+
''
27+
].join(CRLF),
28+
['* 941 EXISTS',
29+
'* OK [UIDNEXT 486028] next uid',
30+
'* 0 RECENT',
31+
'* 941 EXISTS',
32+
'* OK [UIDVALIDITY 5] uid validity',
33+
'* OK [UNSEEN 144] first unseen',
34+
'A5 OK [READ-ONLY] done',
35+
''
36+
].join(CRLF),
37+
['* 1 FETCH (UID 1 FLAGS () INTERNALDATE "01-Feb-2019 22:25:37 +0000")',
38+
'A6 OK Success',
39+
'* 2 FETCH (UID 2)',
40+
'* 2 OK unsolicited untagged response like one from gmail',
41+
''
42+
].join(CRLF),
43+
['* LIST (\\Noselect) "/" "/"',
44+
'* LIST () "/" "भारत"',
45+
'* LIST () "/" "&-"',
46+
'A7 OK Success',
47+
''
48+
].join(CRLF),
49+
['A8 OK Success',
50+
''
51+
].join(CRLF),
52+
];
53+
54+
var srv = net.createServer(function(sock) {
55+
sock.write('* OK asdf\r\n');
56+
var buf = '', lines;
57+
sock.on('data', function(data) {
58+
buf += data.toString('utf8');
59+
if (buf.indexOf(CRLF) > -1) {
60+
lines = buf.split(CRLF);
61+
buf = lines.pop();
62+
lines.forEach(function() {
63+
sock.write(RESPONSES.shift());
64+
});
65+
}
66+
});
67+
});
68+
srv.listen(0, '127.0.0.1', function() {
69+
var port = srv.address().port;
70+
var imap = new Imap({
71+
user: 'foo',
72+
password: 'bar',
73+
host: '127.0.0.1',
74+
port: port,
75+
keepalive: false
76+
});
77+
imap.on('ready', function() {
78+
imap.openBox('INBOX', true, function () {
79+
imap.fetch(1).on('end', function () {
80+
imap.getBoxes(function(err, boxes) {
81+
assert.deepEqual(Object.keys(boxes),
82+
[ '', 'भारत', '&' ]);
83+
srv.close()
84+
imap.end()
85+
});
86+
})
87+
})
88+
});
89+
imap.connect();
90+
});
91+
92+

0 commit comments

Comments
 (0)