diff --git a/README.md b/README.md index b13862a8..135d36eb 100644 --- a/README.md +++ b/README.md @@ -749,6 +749,5 @@ TODO Several things not yet implemented in no particular order: * Support additional IMAP commands/extensions: - * NOTIFY (via NOTIFY extension -- RFC5465) * STATUS addition to LIST (via LIST-STATUS extension -- RFC5819) * QRESYNC (RFC5162) diff --git a/lib/Connection.js b/lib/Connection.js index bd31e98b..94de7d5e 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -297,6 +297,14 @@ Connection.prototype.serverSupports = function(cap) { return (this._caps && this._caps.indexOf(cap) > -1); }; +Connection.prototype.serverEnabled = function(cap) { + return (this._enabled && this._enabled.indexOf(cap) > -1); +}; + +Connection.prototype.escapeBox = function(str) { + return escape(this.serverEnabled("UTF8=ACCEPT") ? (''+str) : utf7.encode(''+str)); +} + Connection.prototype.destroy = function() { this._queue = []; this._curReq = undefined; @@ -325,7 +333,7 @@ Connection.prototype.append = function(data, options, cb) { else options.mailbox = this._box.name; } - var cmd = 'APPEND "' + escape(utf7.encode(''+options.mailbox)) + '"'; + var cmd = 'APPEND "' + this.escapeBox(options.mailbox) + '"'; if (options.flags) { if (!Array.isArray(options.flags)) options.flags = [options.flags]; @@ -379,7 +387,7 @@ Connection.prototype.getBoxes = function(namespace, cb) { namespace = ''; } - namespace = escape(utf7.encode(''+namespace)); + namespace = this.escapeBox(namespace); this._enqueue('LIST "' + namespace + '" "*"', cb); }; @@ -417,7 +425,7 @@ Connection.prototype.openBox = function(name, readOnly, cb) { } name = ''+name; - var encname = escape(utf7.encode(name)), + var encname = this.escapeBox(name), cmd = (readOnly ? 'EXAMINE' : 'SELECT'), self = this; @@ -478,16 +486,16 @@ Connection.prototype.closeBox = function(shouldExpunge, cb) { }; Connection.prototype.addBox = function(name, cb) { - this._enqueue('CREATE "' + escape(utf7.encode(''+name)) + '"', cb); + this._enqueue('CREATE "' + this.escapeBox(name) + '"', cb); }; Connection.prototype.delBox = function(name, cb) { - this._enqueue('DELETE "' + escape(utf7.encode(''+name)) + '"', cb); + this._enqueue('DELETE "' + this.escapeBox(name) + '"', cb); }; Connection.prototype.renameBox = function(oldname, newname, cb) { - var encoldname = escape(utf7.encode(''+oldname)), - encnewname = escape(utf7.encode(''+newname)), + var encoldname = this.escapeBox(oldname), + encnewname = this.escapeBox(newname), self = this; this._enqueue('RENAME "' + encoldname + '" "' + encnewname + '"', @@ -507,11 +515,11 @@ Connection.prototype.renameBox = function(oldname, newname, cb) { }; Connection.prototype.subscribeBox = function(name, cb) { - this._enqueue('SUBSCRIBE "' + escape(utf7.encode(''+name)) + '"', cb); + this._enqueue('SUBSCRIBE "' + this.escapeBox(name) + '"', cb); }; Connection.prototype.unsubscribeBox = function(name, cb) { - this._enqueue('UNSUBSCRIBE "' + escape(utf7.encode(''+name)) + '"', cb); + this._enqueue('UNSUBSCRIBE "' + this.escapeBox(name) + '"', cb); }; Connection.prototype.getSubscribedBoxes = function(namespace, cb) { @@ -520,7 +528,7 @@ Connection.prototype.getSubscribedBoxes = function(namespace, cb) { namespace = ''; } - namespace = escape(utf7.encode(''+namespace)); + namespace = this.escapeBox(namespace); this._enqueue('LSUB "' + namespace + '" "*"', cb); }; @@ -529,7 +537,7 @@ Connection.prototype.status = function(boxName, cb) { if (this._box && this._box.name === boxName) throw new Error('Cannot call status on currently selected mailbox'); - boxName = escape(utf7.encode(''+boxName)); + boxName = this.escapeBox(boxName); var info = [ 'MESSAGES', 'RECENT', 'UNSEEN', 'UIDVALIDITY', 'UIDNEXT' ]; @@ -685,7 +693,7 @@ Connection.prototype._copy = function(which, uids, boxTo, cb) { + 'list'); } - boxTo = escape(utf7.encode(''+boxTo)); + boxTo = this.escapeBox(boxTo); this._enqueue(which + 'COPY ' + uids.join(',') + ' "' + boxTo + '"', cb); }; @@ -710,7 +718,7 @@ Connection.prototype._move = function(which, uids, boxTo, cb) { } uids = uids.join(','); - boxTo = escape(utf7.encode(''+boxTo)); + boxTo = escapebox(To); this._enqueue(which + 'MOVE ' + uids + ' "' + boxTo + '"', cb); } else if (this._box.permFlags.indexOf('\\Deleted') === -1 @@ -906,7 +914,7 @@ Connection.prototype._storeLabels = function(which, uids, labels, mode, cb) { if (!Array.isArray(labels)) labels = [labels]; labels = labels.map(function(v) { - return '"' + escape(utf7.encode(''+v)) + '"'; + return '"' + this.escapeBox(v) + '"'; }).join(' '); uids = uids.join(','); @@ -1023,7 +1031,7 @@ Connection.prototype.setQuota = function(quotaRoot, limits, cb) { triplets += l + ' ' + limits[l]; } - quotaRoot = escape(utf7.encode(''+quotaRoot)); + quotaRoot = this.escapeBox(quotaRoot); this._enqueue('SETQUOTA "' + quotaRoot + '" (' + triplets + ')', function(err, quotalist) { @@ -1036,7 +1044,7 @@ Connection.prototype.setQuota = function(quotaRoot, limits, cb) { }; Connection.prototype.getQuota = function(quotaRoot, cb) { - quotaRoot = escape(utf7.encode(''+quotaRoot)); + quotaRoot = this.escapeBox(quotaRoot); this._enqueue('GETQUOTA "' + quotaRoot + '"', function(err, quotalist) { if (err) @@ -1047,7 +1055,7 @@ Connection.prototype.getQuota = function(quotaRoot, cb) { }; Connection.prototype.getQuotaRoot = function(boxName, cb) { - boxName = escape(utf7.encode(''+boxName)); + boxName = this.escapeBox(boxName); this._enqueue('GETQUOTAROOT "' + boxName + '"', function(err, quotalist) { if (err) @@ -1241,8 +1249,14 @@ Connection.prototype._resUntagged = function(info) { this.namespaces = info.text; else if (type === 'id') this._curReq.cbargs.push(info.text); - else if (type === 'capability') + else if (type === 'capability') { this._caps = info.text.map(function(v) { return v.toUpperCase(); }); + if (this.serverSupports('ENABLE')) + this._enqueue('ENABLE UTF8=ACCEPT CONDSTORE', function() {}); + if (this.serverSupports('NOTIFY')) + this._enqueue('NOTIFY SET (SELECTED-DELAYED (MESSAGENEW (UID) MESSAGEEXPUNGE))', function() {}); + } else if (type === 'enabled') + this._enabled = info.text.map(function(v) { return v.toUpperCase(); }); else if (type === 'preauth') this.state = 'authenticated'; else if (type === 'sort' || type === 'thread' || type === 'esearch') diff --git a/lib/Parser.js b/lib/Parser.js index 1dd386ba..f20fb28c 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -17,7 +17,7 @@ var CH_LF = 10, RE_SEQNO = /^\* (\d+)/, RE_LISTCONTENT = /^\((.*)\)$/, RE_LITERAL = /\{(\d+)\}$/, - 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, + 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, RE_TAGGED = /^A(\d+) (OK|NO|BAD) ?(?:\[([^\]]+)\] )?(.*)$/i, RE_CONTINUE = /^\+(?: (?:\[([^\]]+)\] )?(.+))?$/i, RE_CRLF = /\r\n/g, @@ -222,6 +222,7 @@ Parser.prototype._resUntagged = function() { if (type === 'flags' || type === 'search' || type === 'capability' + || type === 'enabled' || type === 'sort') { if (m[5]) { if (type === 'search' && RE_SEARCH_MODSEQ.test(m[5])) { diff --git a/test/test-connection-notify-utf8accept.js b/test/test-connection-notify-utf8accept.js new file mode 100644 index 00000000..d93a40fa --- /dev/null +++ b/test/test-connection-notify-utf8accept.js @@ -0,0 +1,66 @@ +var assert = require('assert'), + net = require('net'), + Imap = require('../lib/Connection'), + result; + +var CRLF = '\r\n'; + +var RESPONSES = [ + ['* CAPABILITY IMAP4rev1', + 'A0 OK Thats all she wrote!', + '' + ].join(CRLF), + ['* CAPABILITY IMAP4rev1 ENABLE NOTIFY', + 'A1 OK authenticated (Success)', + '' + ].join(CRLF), + ['* ENABLED', + 'A2 OK Success', + '' + ].join(CRLF), + ['A3 OK Success', + '' + ].join(CRLF), + ['* LIST (\\Noselect) "/" "/"', + '* LIST () "/" "भारत"', + '* LIST () "/" "&-"', + 'A4 OK Success', + '' + ].join(CRLF), + ['A5 OK Success', + '' + ].join(CRLF) +]; + +var srv = net.createServer(function(sock) { + sock.write('* OK asdf\r\n'); + var buf = '', lines; + sock.on('data', function(data) { + buf += data.toString('utf8'); + if (buf.indexOf(CRLF) > -1) { + lines = buf.split(CRLF); + buf = lines.pop(); + lines.forEach(function() { + sock.write(RESPONSES.shift()); + }); + } + }); +}); +srv.listen(0, '127.0.0.1', function() { + var port = srv.address().port; + var imap = new Imap({ + user: 'foo', + password: 'bar', + host: '127.0.0.1', + port: port, + keepalive: false, + debug: function(info) { + console.log('Debug:', info); + } + }); + imap.on('ready', function() { + srv.close(); + imap.end(); + }); + imap.connect(); +});