@n8n/imap 0.7.0 → 0.8.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +754 -0
  3. package/deps/encoding/encoding-indexes.js +73 -0
  4. package/deps/encoding/encoding.js +2340 -0
  5. package/deps/test/utf7-imap.js +48 -0
  6. package/deps/test/utf7-rfc2152.js +78 -0
  7. package/deps/utf7.js +104 -0
  8. package/index.d.ts +396 -0
  9. package/lib/Connection.js +2131 -0
  10. package/lib/Parser.js +1026 -0
  11. package/package.json +18 -41
  12. package/test/test-connection-fetch-dup.js +92 -0
  13. package/test/test-connection-fetch-frag.js +92 -0
  14. package/test/test-connection-fetch-spillover.js +176 -0
  15. package/test/test-connection-fetch-stringbody.js +104 -0
  16. package/test/test-connection-idle-normal.js +144 -0
  17. package/test/test-connection-idle-order.js +138 -0
  18. package/test/test-parse-bodystructure.js +83 -0
  19. package/test/test-parse-envelope-addrs.js +117 -0
  20. package/test/test-parse-expr.js +92 -0
  21. package/test/test-parse-header.js +109 -0
  22. package/test/test-parser.js +543 -0
  23. package/test/test.js +4 -0
  24. package/LICENSE.md +0 -86
  25. package/LICENSE_EE.md +0 -27
  26. package/dist/ImapSimple.d.ts +0 -16
  27. package/dist/ImapSimple.js +0 -120
  28. package/dist/ImapSimple.js.map +0 -1
  29. package/dist/PartData.d.ts +0 -24
  30. package/dist/PartData.js +0 -99
  31. package/dist/PartData.js.map +0 -1
  32. package/dist/build.tsbuildinfo +0 -1
  33. package/dist/errors.d.ts +0 -12
  34. package/dist/errors.js +0 -30
  35. package/dist/errors.js.map +0 -1
  36. package/dist/helpers/getMessage.d.ts +0 -3
  37. package/dist/helpers/getMessage.js +0 -37
  38. package/dist/helpers/getMessage.js.map +0 -1
  39. package/dist/index.d.ts +0 -7
  40. package/dist/index.js +0 -85
  41. package/dist/index.js.map +0 -1
  42. package/dist/types.d.ts +0 -30
  43. package/dist/types.js +0 -3
  44. package/dist/types.js.map +0 -1
@@ -0,0 +1,2131 @@
1
+ var tls = require('tls'),
2
+ Socket = require('net').Socket,
3
+ EventEmitter = require('events').EventEmitter,
4
+ inherits = require('util').inherits,
5
+ inspect = require('util').inspect,
6
+ isDate = require('util').isDate,
7
+ utf7 = require('../deps/utf7').imap;
8
+
9
+ var Parser = require('./Parser').Parser,
10
+ parseExpr = require('./Parser').parseExpr,
11
+ parseHeader = require('./Parser').parseHeader;
12
+
13
+ var MAX_INT = 9007199254740992,
14
+ KEEPALIVE_INTERVAL = 10000,
15
+ MAX_IDLE_WAIT = 300000, // 5 minutes
16
+ MONTHS = ['Jan', 'Feb', 'Mar',
17
+ 'Apr', 'May', 'Jun',
18
+ 'Jul', 'Aug', 'Sep',
19
+ 'Oct', 'Nov', 'Dec'],
20
+ FETCH_ATTR_MAP = {
21
+ 'RFC822.SIZE': 'size',
22
+ 'BODY': 'struct',
23
+ 'BODYSTRUCTURE': 'struct',
24
+ 'ENVELOPE': 'envelope',
25
+ 'INTERNALDATE': 'date'
26
+ },
27
+ SPECIAL_USE_ATTRIBUTES = [
28
+ '\\All',
29
+ '\\Archive',
30
+ '\\Drafts',
31
+ '\\Flagged',
32
+ '\\Important',
33
+ '\\Junk',
34
+ '\\Sent',
35
+ '\\Trash'
36
+ ],
37
+ CRLF = '\r\n',
38
+ RE_CMD = /^([^ ]+)(?: |$)/,
39
+ RE_UIDCMD_HASRESULTS = /^UID (?:FETCH|SEARCH|SORT)/,
40
+ RE_IDLENOOPRES = /^(IDLE|NOOP) /,
41
+ RE_OPENBOX = /^EXAMINE|SELECT$/,
42
+ RE_BODYPART = /^BODY\[/,
43
+ RE_INVALID_KW_CHARS = /[\(\)\{\\\"\]\%\*\x00-\x20\x7F]/,
44
+ RE_NUM_RANGE = /^(?:[\d]+|\*):(?:[\d]+|\*)$/,
45
+ RE_BACKSLASH = /\\/g,
46
+ RE_DBLQUOTE = /"/g,
47
+ RE_ESCAPE = /\\\\/g,
48
+ RE_INTEGER = /^\d+$/;
49
+
50
+ function Connection(config) {
51
+ if (!(this instanceof Connection))
52
+ return new Connection(config);
53
+
54
+ EventEmitter.call(this);
55
+
56
+ config || (config = {});
57
+
58
+ this._config = {
59
+ localAddress: config.localAddress,
60
+ socket: config.socket,
61
+ socketTimeout: config.socketTimeout || 0,
62
+ host: config.host || 'localhost',
63
+ port: config.port || 143,
64
+ tls: config.tls,
65
+ tlsOptions: config.tlsOptions,
66
+ autotls: config.autotls,
67
+ user: config.user,
68
+ password: config.password,
69
+ xoauth: config.xoauth,
70
+ xoauth2: config.xoauth2,
71
+ connTimeout: config.connTimeout || 10000,
72
+ authTimeout: config.authTimeout || 5000,
73
+ keepalive: (config.keepalive === undefined || config.keepalive === null
74
+ ? true
75
+ : config.keepalive)
76
+ };
77
+
78
+ this._sock = config.socket || undefined;
79
+ this._tagcount = 0;
80
+ this._tmrConn = undefined;
81
+ this._tmrKeepalive = undefined;
82
+ this._tmrAuth = undefined;
83
+ this._queue = [];
84
+ this._box = undefined;
85
+ this._idle = { started: undefined, enabled: false };
86
+ this._parser = undefined;
87
+ this._curReq = undefined;
88
+ this.delimiter = undefined;
89
+ this.namespaces = undefined;
90
+ this.state = 'disconnected';
91
+ this.debug = config.debug;
92
+ }
93
+ inherits(Connection, EventEmitter);
94
+
95
+ Connection.prototype.connect = function() {
96
+ var config = this._config,
97
+ self = this,
98
+ socket,
99
+ parser,
100
+ tlsOptions;
101
+
102
+ socket = config.socket || new Socket();
103
+ socket.setKeepAlive(true);
104
+ this._sock = undefined;
105
+ this._tagcount = 0;
106
+ this._tmrConn = undefined;
107
+ this._tmrKeepalive = undefined;
108
+ this._tmrAuth = undefined;
109
+ this._queue = [];
110
+ this._box = undefined;
111
+ this._idle = { started: undefined, enabled: false };
112
+ this._parser = undefined;
113
+ this._curReq = undefined;
114
+ this.delimiter = undefined;
115
+ this.namespaces = undefined;
116
+ this.state = 'disconnected';
117
+
118
+ if (config.tls) {
119
+ tlsOptions = {};
120
+ // servername must be set to prevent issues with some imap server and openssl 1.1.1
121
+ tlsOptions.servername = config.host;
122
+ tlsOptions.host = config.host;
123
+ // Host name may be overridden the tlsOptions
124
+ for (var k in config.tlsOptions)
125
+ tlsOptions[k] = config.tlsOptions[k];
126
+ tlsOptions.socket = socket;
127
+ }
128
+
129
+ if (config.tls)
130
+ this._sock = tls.connect(tlsOptions, onconnect);
131
+ else {
132
+ socket.once('connect', onconnect);
133
+ this._sock = socket;
134
+ }
135
+
136
+ function onconnect() {
137
+ clearTimeout(self._tmrConn);
138
+ self.state = 'connected';
139
+ self.debug && self.debug('[connection] Connected to host');
140
+ self._tmrAuth = setTimeout(function() {
141
+ var err = new Error('Timed out while authenticating with server');
142
+ err.source = 'timeout-auth';
143
+ self.emit('error', err);
144
+ socket.destroy();
145
+ }, config.authTimeout);
146
+ }
147
+
148
+ this._onError = function(err) {
149
+ clearTimeout(self._tmrConn);
150
+ clearTimeout(self._tmrAuth);
151
+ self.debug && self.debug('[connection] Error: ' + err);
152
+ err.source = 'socket';
153
+ self.emit('error', err);
154
+ };
155
+ this._sock.on('error', this._onError);
156
+
157
+ this._onSocketTimeout = function() {
158
+ clearTimeout(self._tmrConn);
159
+ clearTimeout(self._tmrAuth);
160
+ clearTimeout(self._tmrKeepalive);
161
+ self.state = 'disconnected';
162
+ self.debug && self.debug('[connection] Socket timeout');
163
+
164
+ var err = new Error('Socket timed out while talking to server');
165
+ err.source = 'socket-timeout';
166
+ self.emit('error', err);
167
+ socket.destroy();
168
+ };
169
+ this._sock.on('timeout', this._onSocketTimeout);
170
+ socket.setTimeout(config.socketTimeout);
171
+
172
+ socket.once('close', function(had_err) {
173
+ clearTimeout(self._tmrConn);
174
+ clearTimeout(self._tmrAuth);
175
+ clearTimeout(self._tmrKeepalive);
176
+ self.state = 'disconnected';
177
+ self.debug && self.debug('[connection] Closed');
178
+ self.emit('close', had_err);
179
+ });
180
+
181
+ socket.once('end', function() {
182
+ clearTimeout(self._tmrConn);
183
+ clearTimeout(self._tmrAuth);
184
+ clearTimeout(self._tmrKeepalive);
185
+ self.state = 'disconnected';
186
+ self.debug && self.debug('[connection] Ended');
187
+ self.emit('end');
188
+ });
189
+
190
+ this._parser = parser = new Parser(this._sock, this.debug);
191
+
192
+ parser.on('untagged', function(info) {
193
+ self._resUntagged(info);
194
+ });
195
+ parser.on('tagged', function(info) {
196
+ self._resTagged(info);
197
+ });
198
+ parser.on('body', function(stream, info) {
199
+ var msg = self._curReq.fetchCache[info.seqno], toget;
200
+
201
+ if (msg === undefined) {
202
+ msg = self._curReq.fetchCache[info.seqno] = {
203
+ msgEmitter: new EventEmitter(),
204
+ toget: self._curReq.fetching.slice(0),
205
+ attrs: {},
206
+ ended: false
207
+ };
208
+
209
+ self._curReq.bodyEmitter.emit('message', msg.msgEmitter, info.seqno);
210
+ }
211
+
212
+ toget = msg.toget;
213
+
214
+ // here we compare the parsed version of the expression inside BODY[]
215
+ // because 'HEADER.FIELDS (TO FROM)' really is equivalent to
216
+ // 'HEADER.FIELDS ("TO" "FROM")' and some servers will actually send the
217
+ // quoted form even if the client did not use quotes
218
+ var thisbody = parseExpr(info.which);
219
+ for (var i = 0, len = toget.length; i < len; ++i) {
220
+ if (_deepEqual(thisbody, toget[i])) {
221
+ toget.splice(i, 1);
222
+ msg.msgEmitter.emit('body', stream, info);
223
+ return;
224
+ }
225
+ }
226
+ stream.resume(); // a body we didn't ask for?
227
+ });
228
+ parser.on('continue', function(info) {
229
+ var type = self._curReq.type;
230
+ if (type === 'IDLE') {
231
+ if (self._queue.length
232
+ && self._idle.started === 0
233
+ && self._curReq
234
+ && self._curReq.type === 'IDLE'
235
+ && self._sock
236
+ && self._sock.writable
237
+ && !self._idle.enabled) {
238
+ self.debug && self.debug('=> DONE');
239
+ self._sock.write('DONE' + CRLF);
240
+ return;
241
+ }
242
+ // now idling
243
+ self._idle.started = Date.now();
244
+ } else if (/^AUTHENTICATE XOAUTH/.test(self._curReq.fullcmd)) {
245
+ self._curReq.oauthError =
246
+ Buffer.from(info.text, 'base64').toString('utf8');
247
+ self.debug && self.debug('=> ' + inspect(CRLF));
248
+ self._sock.write(CRLF);
249
+ } else if (type === 'APPEND') {
250
+ self._sockWriteAppendData(self._curReq.appendData);
251
+ } else if (self._curReq.lines && self._curReq.lines.length) {
252
+ var line = self._curReq.lines.shift() + '\r\n';
253
+ self.debug && self.debug('=> ' + inspect(line));
254
+ self._sock.write(line, 'latin1');
255
+ }
256
+ });
257
+ parser.on('other', function(line) {
258
+ var m;
259
+ if (m = RE_IDLENOOPRES.exec(line)) {
260
+ // no longer idling
261
+ self._idle.enabled = false;
262
+ self._idle.started = undefined;
263
+ clearTimeout(self._tmrKeepalive);
264
+
265
+ self._curReq = undefined;
266
+
267
+ if (self._queue.length === 0
268
+ && self._config.keepalive
269
+ && self.state === 'authenticated'
270
+ && !self._idle.enabled) {
271
+ self._idle.enabled = true;
272
+ if (m[1] === 'NOOP')
273
+ self._doKeepaliveTimer();
274
+ else
275
+ self._doKeepaliveTimer(true);
276
+ }
277
+
278
+ self._processQueue();
279
+ }
280
+ });
281
+
282
+ this._tmrConn = setTimeout(function() {
283
+ var err = new Error('Timed out while connecting to server');
284
+ err.source = 'timeout';
285
+ self.emit('error', err);
286
+ socket.destroy();
287
+ }, config.connTimeout);
288
+
289
+ socket.connect({
290
+ port: config.port,
291
+ host: config.host,
292
+ localAddress: config.localAddress
293
+ });
294
+ };
295
+
296
+ Connection.prototype.serverSupports = function(cap) {
297
+ return (this._caps && this._caps.indexOf(cap) > -1);
298
+ };
299
+
300
+ Connection.prototype.destroy = function() {
301
+ this._queue = [];
302
+ this._curReq = undefined;
303
+ this._sock && this._sock.end();
304
+ };
305
+
306
+ Connection.prototype.end = function() {
307
+ var self = this;
308
+ this._enqueue('LOGOUT', function() {
309
+ self._queue = [];
310
+ self._curReq = undefined;
311
+ self._sock.end();
312
+ });
313
+ };
314
+
315
+ Connection.prototype.append = function(data, options, cb) {
316
+ var literal = this.serverSupports('LITERAL+');
317
+ if (typeof options === 'function') {
318
+ cb = options;
319
+ options = undefined;
320
+ }
321
+ options = options || {};
322
+ if (!options.mailbox) {
323
+ if (!this._box)
324
+ throw new Error('No mailbox specified or currently selected');
325
+ else
326
+ options.mailbox = this._box.name;
327
+ }
328
+ var cmd = 'APPEND "' + escape(utf7.encode(''+options.mailbox)) + '"';
329
+ if (options.flags) {
330
+ if (!Array.isArray(options.flags))
331
+ options.flags = [options.flags];
332
+ if (options.flags.length > 0) {
333
+ for (var i = 0, len = options.flags.length; i < len; ++i) {
334
+ if (options.flags[i][0] !== '$' && options.flags[i][0] !== '\\')
335
+ options.flags[i] = '\\' + options.flags[i];
336
+ }
337
+ cmd += ' (' + options.flags.join(' ') + ')';
338
+ }
339
+ }
340
+ if (options.date) {
341
+ if (!isDate(options.date))
342
+ throw new Error('`date` is not a Date object');
343
+ cmd += ' "';
344
+ cmd += options.date.getDate();
345
+ cmd += '-';
346
+ cmd += MONTHS[options.date.getMonth()];
347
+ cmd += '-';
348
+ cmd += options.date.getFullYear();
349
+ cmd += ' ';
350
+ cmd += ('0' + options.date.getHours()).slice(-2);
351
+ cmd += ':';
352
+ cmd += ('0' + options.date.getMinutes()).slice(-2);
353
+ cmd += ':';
354
+ cmd += ('0' + options.date.getSeconds()).slice(-2);
355
+ cmd += ((options.date.getTimezoneOffset() > 0) ? ' -' : ' +' );
356
+ cmd += ('0' + (-options.date.getTimezoneOffset() / 60)).slice(-2);
357
+ cmd += ('0' + (-options.date.getTimezoneOffset() % 60)).slice(-2);
358
+ cmd += '"';
359
+ }
360
+ cmd += ' {';
361
+ cmd += (Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data));
362
+ cmd += (literal ? '+' : '') + '}';
363
+
364
+ this._enqueue(cmd, cb);
365
+
366
+ if (literal)
367
+ this._queue[this._queue.length - 1].literalAppendData = data;
368
+ else
369
+ this._queue[this._queue.length - 1].appendData = data;
370
+ };
371
+
372
+ Connection.prototype.getSpecialUseBoxes = function(cb) {
373
+ this._enqueue('XLIST "" "*"', cb);
374
+ };
375
+
376
+ Connection.prototype.getBoxes = function(namespace, cb) {
377
+ if (typeof namespace === 'function') {
378
+ cb = namespace;
379
+ namespace = '';
380
+ }
381
+
382
+ namespace = escape(utf7.encode(''+namespace));
383
+
384
+ this._enqueue('LIST "' + namespace + '" "*"', cb);
385
+ };
386
+
387
+ Connection.prototype.id = function(identification, cb) {
388
+ if (!this.serverSupports('ID'))
389
+ throw new Error('Server does not support ID');
390
+ var cmd = 'ID';
391
+ if ((identification === null) || (Object.keys(identification).length === 0))
392
+ cmd += ' NIL';
393
+ else {
394
+ if (Object.keys(identification).length > 30)
395
+ throw new Error('Max allowed number of keys is 30');
396
+ var kv = [];
397
+ for (var k in identification) {
398
+ if (Buffer.byteLength(k) > 30)
399
+ throw new Error('Max allowed key length is 30');
400
+ if (Buffer.byteLength(identification[k]) > 1024)
401
+ throw new Error('Max allowed value length is 1024');
402
+ kv.push('"' + escape(k) + '"');
403
+ kv.push('"' + escape(identification[k]) + '"');
404
+ }
405
+ cmd += ' (' + kv.join(' ') + ')';
406
+ }
407
+ this._enqueue(cmd, cb);
408
+ };
409
+
410
+ Connection.prototype.openBox = function(name, readOnly, cb) {
411
+ if (this.state !== 'authenticated')
412
+ throw new Error('Not authenticated');
413
+
414
+ if (typeof readOnly === 'function') {
415
+ cb = readOnly;
416
+ readOnly = false;
417
+ }
418
+
419
+ name = ''+name;
420
+ var encname = escape(utf7.encode(name)),
421
+ cmd = (readOnly ? 'EXAMINE' : 'SELECT'),
422
+ self = this;
423
+
424
+ cmd += ' "' + encname + '"';
425
+
426
+ if (this.serverSupports('CONDSTORE'))
427
+ cmd += ' (CONDSTORE)';
428
+
429
+ this._enqueue(cmd, function(err) {
430
+ if (err) {
431
+ self._box = undefined;
432
+ cb(err);
433
+ } else {
434
+ self._box.name = name;
435
+ cb(err, self._box);
436
+ }
437
+ });
438
+ };
439
+
440
+ Connection.prototype.closeBox = function(shouldExpunge, cb) {
441
+ if (this._box === undefined)
442
+ throw new Error('No mailbox is currently selected');
443
+
444
+ var self = this;
445
+
446
+ if (typeof shouldExpunge === 'function') {
447
+ cb = shouldExpunge;
448
+ shouldExpunge = true;
449
+ }
450
+
451
+ if (shouldExpunge) {
452
+ this._enqueue('CLOSE', function(err) {
453
+ if (!err)
454
+ self._box = undefined;
455
+
456
+ cb(err);
457
+ });
458
+ } else {
459
+ if (this.serverSupports('UNSELECT')) {
460
+ // use UNSELECT if available, as it claims to be "cleaner" than the
461
+ // alternative "hack"
462
+ this._enqueue('UNSELECT', function(err) {
463
+ if (!err)
464
+ self._box = undefined;
465
+
466
+ cb(err);
467
+ });
468
+ } else {
469
+ // "HACK": close the box without expunging by attempting to SELECT a
470
+ // non-existent mailbox
471
+ var badbox = 'NODEJSIMAPCLOSINGBOX' + Date.now();
472
+ this._enqueue('SELECT "' + badbox + '"', function(err) {
473
+ self._box = undefined;
474
+ cb();
475
+ });
476
+ }
477
+ }
478
+ };
479
+
480
+ Connection.prototype.addBox = function(name, cb) {
481
+ this._enqueue('CREATE "' + escape(utf7.encode(''+name)) + '"', cb);
482
+ };
483
+
484
+ Connection.prototype.delBox = function(name, cb) {
485
+ this._enqueue('DELETE "' + escape(utf7.encode(''+name)) + '"', cb);
486
+ };
487
+
488
+ Connection.prototype.renameBox = function(oldname, newname, cb) {
489
+ var encoldname = escape(utf7.encode(''+oldname)),
490
+ encnewname = escape(utf7.encode(''+newname)),
491
+ self = this;
492
+
493
+ this._enqueue('RENAME "' + encoldname + '" "' + encnewname + '"',
494
+ function(err) {
495
+ if (err)
496
+ return cb(err);
497
+
498
+ if (self._box
499
+ && self._box.name === oldname
500
+ && oldname.toUpperCase() !== 'INBOX') {
501
+ self._box.name = newname;
502
+ cb(err, self._box);
503
+ } else
504
+ cb();
505
+ }
506
+ );
507
+ };
508
+
509
+ Connection.prototype.subscribeBox = function(name, cb) {
510
+ this._enqueue('SUBSCRIBE "' + escape(utf7.encode(''+name)) + '"', cb);
511
+ };
512
+
513
+ Connection.prototype.unsubscribeBox = function(name, cb) {
514
+ this._enqueue('UNSUBSCRIBE "' + escape(utf7.encode(''+name)) + '"', cb);
515
+ };
516
+
517
+ Connection.prototype.getSubscribedBoxes = function(namespace, cb) {
518
+ if (typeof namespace === 'function') {
519
+ cb = namespace;
520
+ namespace = '';
521
+ }
522
+
523
+ namespace = escape(utf7.encode(''+namespace));
524
+
525
+ this._enqueue('LSUB "' + namespace + '" "*"', cb);
526
+ };
527
+
528
+ Connection.prototype.status = function(boxName, cb) {
529
+ if (this._box && this._box.name === boxName)
530
+ throw new Error('Cannot call status on currently selected mailbox');
531
+
532
+ boxName = escape(utf7.encode(''+boxName));
533
+
534
+ var info = [ 'MESSAGES', 'RECENT', 'UNSEEN', 'UIDVALIDITY', 'UIDNEXT' ];
535
+
536
+ if (this.serverSupports('CONDSTORE'))
537
+ info.push('HIGHESTMODSEQ');
538
+
539
+ info = info.join(' ');
540
+
541
+ this._enqueue('STATUS "' + boxName + '" (' + info + ')', cb);
542
+ };
543
+
544
+ Connection.prototype.expunge = function(uids, cb) {
545
+ if (typeof uids === 'function') {
546
+ cb = uids;
547
+ uids = undefined;
548
+ }
549
+
550
+ if (uids !== undefined) {
551
+ if (!Array.isArray(uids))
552
+ uids = [uids];
553
+ validateUIDList(uids);
554
+
555
+ if (uids.length === 0)
556
+ throw new Error('Empty uid list');
557
+
558
+ uids = uids.join(',');
559
+
560
+ if (!this.serverSupports('UIDPLUS'))
561
+ throw new Error('Server does not support this feature (UIDPLUS)');
562
+
563
+ this._enqueue('UID EXPUNGE ' + uids, cb);
564
+ } else
565
+ this._enqueue('EXPUNGE', cb);
566
+ };
567
+
568
+ Connection.prototype.search = function(criteria, cb) {
569
+ this._search('UID ', criteria, cb);
570
+ };
571
+
572
+ Connection.prototype._search = function(which, criteria, cb) {
573
+ if (this._box === undefined)
574
+ throw new Error('No mailbox is currently selected');
575
+ else if (!Array.isArray(criteria))
576
+ throw new Error('Expected array for search criteria');
577
+
578
+ var cmd = which + 'SEARCH',
579
+ info = { hasUTF8: false /*output*/ },
580
+ query = buildSearchQuery(criteria, this._caps, info),
581
+ lines;
582
+ if (info.hasUTF8) {
583
+ cmd += ' CHARSET UTF-8';
584
+ lines = query.split(CRLF);
585
+ query = lines.shift();
586
+ }
587
+ cmd += query;
588
+ this._enqueue(cmd, cb);
589
+ if (info.hasUTF8) {
590
+ var req = this._queue[this._queue.length - 1];
591
+ req.lines = lines;
592
+ }
593
+ };
594
+
595
+ Connection.prototype.addFlags = function(uids, flags, cb) {
596
+ this._store('UID ', uids, { mode: '+', flags: flags }, cb);
597
+ };
598
+
599
+ Connection.prototype.delFlags = function(uids, flags, cb) {
600
+ this._store('UID ', uids, { mode: '-', flags: flags }, cb);
601
+ };
602
+
603
+ Connection.prototype.setFlags = function(uids, flags, cb) {
604
+ this._store('UID ', uids, { mode: '', flags: flags }, cb);
605
+ };
606
+
607
+ Connection.prototype.addKeywords = function(uids, keywords, cb) {
608
+ this._store('UID ', uids, { mode: '+', keywords: keywords }, cb);
609
+ };
610
+
611
+ Connection.prototype.delKeywords = function(uids, keywords, cb) {
612
+ this._store('UID ', uids, { mode: '-', keywords: keywords }, cb);
613
+ };
614
+
615
+ Connection.prototype.setKeywords = function(uids, keywords, cb) {
616
+ this._store('UID ', uids, { mode: '', keywords: keywords }, cb);
617
+ };
618
+
619
+ Connection.prototype._store = function(which, uids, cfg, cb) {
620
+ var mode = cfg.mode,
621
+ isFlags = (cfg.flags !== undefined),
622
+ items = (isFlags ? cfg.flags : cfg.keywords);
623
+ if (this._box === undefined)
624
+ throw new Error('No mailbox is currently selected');
625
+ else if (uids === undefined)
626
+ throw new Error('No messages specified');
627
+
628
+ if (!Array.isArray(uids))
629
+ uids = [uids];
630
+ validateUIDList(uids);
631
+
632
+ if (uids.length === 0) {
633
+ throw new Error('Empty '
634
+ + (which === '' ? 'sequence number' : 'uid')
635
+ + 'list');
636
+ }
637
+
638
+ if ((!Array.isArray(items) && typeof items !== 'string')
639
+ || (Array.isArray(items) && items.length === 0))
640
+ throw new Error((isFlags ? 'Flags' : 'Keywords')
641
+ + ' argument must be a string or a non-empty Array');
642
+ if (!Array.isArray(items))
643
+ items = [items];
644
+ for (var i = 0, len = items.length; i < len; ++i) {
645
+ if (isFlags) {
646
+ if (items[i][0] !== '\\')
647
+ items[i] = '\\' + items[i];
648
+ } else {
649
+ // keyword contains any char except control characters (%x00-1F and %x7F)
650
+ // and: '(', ')', '{', ' ', '%', '*', '\', '"', ']'
651
+ if (RE_INVALID_KW_CHARS.test(items[i])) {
652
+ throw new Error('The keyword "' + items[i]
653
+ + '" contains invalid characters');
654
+ }
655
+ }
656
+ }
657
+
658
+ items = items.join(' ');
659
+ uids = uids.join(',');
660
+
661
+ var modifiers = '';
662
+ if (cfg.modseq !== undefined && !this._box.nomodseq)
663
+ modifiers += 'UNCHANGEDSINCE ' + cfg.modseq + ' ';
664
+
665
+ this._enqueue(which + 'STORE ' + uids + ' '
666
+ + modifiers
667
+ + mode + 'FLAGS.SILENT (' + items + ')', cb);
668
+ };
669
+
670
+ Connection.prototype.copy = function(uids, boxTo, cb) {
671
+ this._copy('UID ', uids, boxTo, cb);
672
+ };
673
+
674
+ Connection.prototype._copy = function(which, uids, boxTo, cb) {
675
+ if (this._box === undefined)
676
+ throw new Error('No mailbox is currently selected');
677
+
678
+ if (!Array.isArray(uids))
679
+ uids = [uids];
680
+ validateUIDList(uids);
681
+
682
+ if (uids.length === 0) {
683
+ throw new Error('Empty '
684
+ + (which === '' ? 'sequence number' : 'uid')
685
+ + 'list');
686
+ }
687
+
688
+ boxTo = escape(utf7.encode(''+boxTo));
689
+
690
+ this._enqueue(which + 'COPY ' + uids.join(',') + ' "' + boxTo + '"', cb);
691
+ };
692
+
693
+ Connection.prototype.move = function(uids, boxTo, cb) {
694
+ this._move('UID ', uids, boxTo, cb);
695
+ };
696
+
697
+ Connection.prototype._move = function(which, uids, boxTo, cb) {
698
+ if (this._box === undefined)
699
+ throw new Error('No mailbox is currently selected');
700
+
701
+ if (this.serverSupports('MOVE')) {
702
+ if (!Array.isArray(uids))
703
+ uids = [uids];
704
+ validateUIDList(uids);
705
+
706
+ if (uids.length === 0) {
707
+ throw new Error('Empty '
708
+ + (which === '' ? 'sequence number' : 'uid')
709
+ + 'list');
710
+ }
711
+
712
+ uids = uids.join(',');
713
+ boxTo = escape(utf7.encode(''+boxTo));
714
+
715
+ this._enqueue(which + 'MOVE ' + uids + ' "' + boxTo + '"', cb);
716
+ } else if (this._box.permFlags.indexOf('\\Deleted') === -1
717
+ && this._box.flags.indexOf('\\Deleted') === -1) {
718
+ throw new Error('Cannot move message: '
719
+ + 'server does not allow deletion of messages');
720
+ } else {
721
+ var deletedUIDs, task = 0, self = this;
722
+ this._copy(which, uids, boxTo, function ccb(err, info) {
723
+ if (err)
724
+ return cb(err, info);
725
+
726
+ if (task === 0 && which && self.serverSupports('UIDPLUS')) {
727
+ // UIDPLUS gives us a 'UID EXPUNGE n' command to expunge a subset of
728
+ // messages with the \Deleted flag set. This allows us to skip some
729
+ // actions.
730
+ task = 2;
731
+ }
732
+ // Make sure we don't expunge any messages marked as Deleted except the
733
+ // one we are moving
734
+ if (task === 0) {
735
+ self.search(['DELETED'], function(e, result) {
736
+ ++task;
737
+ deletedUIDs = result;
738
+ ccb(e, info);
739
+ });
740
+ } else if (task === 1) {
741
+ if (deletedUIDs.length) {
742
+ self.delFlags(deletedUIDs, '\\Deleted', function(e) {
743
+ ++task;
744
+ ccb(e, info);
745
+ });
746
+ } else {
747
+ ++task;
748
+ ccb(err, info);
749
+ }
750
+ } else if (task === 2) {
751
+ var cbMarkDel = function(e) {
752
+ ++task;
753
+ ccb(e, info);
754
+ };
755
+ if (which)
756
+ self.addFlags(uids, '\\Deleted', cbMarkDel);
757
+ else
758
+ self.seq.addFlags(uids, '\\Deleted', cbMarkDel);
759
+ } else if (task === 3) {
760
+ if (which && self.serverSupports('UIDPLUS')) {
761
+ self.expunge(uids, function(e) {
762
+ cb(e, info);
763
+ });
764
+ } else {
765
+ self.expunge(function(e) {
766
+ ++task;
767
+ ccb(e, info);
768
+ });
769
+ }
770
+ } else if (task === 4) {
771
+ if (deletedUIDs.length) {
772
+ self.addFlags(deletedUIDs, '\\Deleted', function(e) {
773
+ cb(e, info);
774
+ });
775
+ } else
776
+ cb(err, info);
777
+ }
778
+ });
779
+ }
780
+ };
781
+
782
+ Connection.prototype.fetch = function(uids, options) {
783
+ return this._fetch('UID ', uids, options);
784
+ };
785
+
786
+ Connection.prototype._fetch = function(which, uids, options) {
787
+ if (uids === undefined
788
+ || uids === null
789
+ || (Array.isArray(uids) && uids.length === 0))
790
+ throw new Error('Nothing to fetch');
791
+
792
+ if (!Array.isArray(uids))
793
+ uids = [uids];
794
+ validateUIDList(uids);
795
+
796
+ if (uids.length === 0) {
797
+ throw new Error('Empty '
798
+ + (which === '' ? 'sequence number' : 'uid')
799
+ + 'list');
800
+ }
801
+
802
+ uids = uids.join(',');
803
+
804
+ var cmd = which + 'FETCH ' + uids + ' (',
805
+ fetching = [],
806
+ i, len, key;
807
+
808
+ if (this.serverSupports('X-GM-EXT-1')) {
809
+ fetching.push('X-GM-THRID');
810
+ fetching.push('X-GM-MSGID');
811
+ fetching.push('X-GM-LABELS');
812
+ }
813
+ if (this.serverSupports('CONDSTORE') && !this._box.nomodseq)
814
+ fetching.push('MODSEQ');
815
+
816
+ fetching.push('UID');
817
+ fetching.push('FLAGS');
818
+ fetching.push('INTERNALDATE');
819
+
820
+ var modifiers;
821
+
822
+ if (options) {
823
+ modifiers = options.modifiers;
824
+ if (options.envelope)
825
+ fetching.push('ENVELOPE');
826
+ if (options.struct)
827
+ fetching.push('BODYSTRUCTURE');
828
+ if (options.size)
829
+ fetching.push('RFC822.SIZE');
830
+ if (Array.isArray(options.extensions)) {
831
+ options.extensions.forEach(function (extension) {
832
+ fetching.push(extension.toUpperCase());
833
+ });
834
+ }
835
+ cmd += fetching.join(' ');
836
+ if (options.bodies !== undefined) {
837
+ var bodies = options.bodies,
838
+ prefix = (options.markSeen ? '' : '.PEEK');
839
+ if (!Array.isArray(bodies))
840
+ bodies = [bodies];
841
+ for (i = 0, len = bodies.length; i < len; ++i) {
842
+ fetching.push(parseExpr(''+bodies[i]));
843
+ cmd += ' BODY' + prefix + '[' + bodies[i] + ']';
844
+ }
845
+ }
846
+ } else
847
+ cmd += fetching.join(' ');
848
+
849
+ cmd += ')';
850
+
851
+ var modkeys = (typeof modifiers === 'object' ? Object.keys(modifiers) : []),
852
+ modstr = ' (';
853
+ for (i = 0, len = modkeys.length, key; i < len; ++i) {
854
+ key = modkeys[i].toUpperCase();
855
+ if (key === 'CHANGEDSINCE' && this.serverSupports('CONDSTORE')
856
+ && !this._box.nomodseq)
857
+ modstr += key + ' ' + modifiers[modkeys[i]] + ' ';
858
+ }
859
+ if (modstr.length > 2) {
860
+ cmd += modstr.substring(0, modstr.length - 1);
861
+ cmd += ')';
862
+ }
863
+
864
+ this._enqueue(cmd);
865
+ var req = this._queue[this._queue.length - 1];
866
+ req.fetchCache = {};
867
+ req.fetching = fetching;
868
+ return (req.bodyEmitter = new EventEmitter());
869
+ };
870
+
871
+ // Extension methods ===========================================================
872
+ Connection.prototype.setLabels = function(uids, labels, cb) {
873
+ this._storeLabels('UID ', uids, labels, '', cb);
874
+ };
875
+
876
+ Connection.prototype.addLabels = function(uids, labels, cb) {
877
+ this._storeLabels('UID ', uids, labels, '+', cb);
878
+ };
879
+
880
+ Connection.prototype.delLabels = function(uids, labels, cb) {
881
+ this._storeLabels('UID ', uids, labels, '-', cb);
882
+ };
883
+
884
+ Connection.prototype._storeLabels = function(which, uids, labels, mode, cb) {
885
+ if (!this.serverSupports('X-GM-EXT-1'))
886
+ throw new Error('Server must support X-GM-EXT-1 capability');
887
+ else if (this._box === undefined)
888
+ throw new Error('No mailbox is currently selected');
889
+ else if (uids === undefined)
890
+ throw new Error('No messages specified');
891
+
892
+ if (!Array.isArray(uids))
893
+ uids = [uids];
894
+ validateUIDList(uids);
895
+
896
+ if (uids.length === 0) {
897
+ throw new Error('Empty '
898
+ + (which === '' ? 'sequence number' : 'uid')
899
+ + 'list');
900
+ }
901
+
902
+ if ((!Array.isArray(labels) && typeof labels !== 'string')
903
+ || (Array.isArray(labels) && labels.length === 0))
904
+ throw new Error('labels argument must be a string or a non-empty Array');
905
+
906
+ if (!Array.isArray(labels))
907
+ labels = [labels];
908
+ labels = labels.map(function(v) {
909
+ return '"' + escape(utf7.encode(''+v)) + '"';
910
+ }).join(' ');
911
+
912
+ uids = uids.join(',');
913
+
914
+ this._enqueue(which + 'STORE ' + uids + ' ' + mode
915
+ + 'X-GM-LABELS.SILENT (' + labels + ')', cb);
916
+ };
917
+
918
+ Connection.prototype.sort = function(sorts, criteria, cb) {
919
+ this._sort('UID ', sorts, criteria, cb);
920
+ };
921
+
922
+ Connection.prototype._sort = function(which, sorts, criteria, cb) {
923
+ if (this._box === undefined)
924
+ throw new Error('No mailbox is currently selected');
925
+ else if (!Array.isArray(sorts) || !sorts.length)
926
+ throw new Error('Expected array with at least one sort criteria');
927
+ else if (!Array.isArray(criteria))
928
+ throw new Error('Expected array for search criteria');
929
+ else if (!this.serverSupports('SORT'))
930
+ throw new Error('Sort is not supported on the server');
931
+
932
+ sorts = sorts.map(function(c) {
933
+ if (typeof c !== 'string')
934
+ throw new Error('Unexpected sort criteria data type. '
935
+ + 'Expected string. Got: ' + typeof criteria);
936
+
937
+ var modifier = '';
938
+ if (c[0] === '-') {
939
+ modifier = 'REVERSE ';
940
+ c = c.substring(1);
941
+ }
942
+ switch (c.toUpperCase()) {
943
+ case 'ARRIVAL':
944
+ case 'CC':
945
+ case 'DATE':
946
+ case 'FROM':
947
+ case 'SIZE':
948
+ case 'SUBJECT':
949
+ case 'TO':
950
+ break;
951
+ default:
952
+ throw new Error('Unexpected sort criteria: ' + c);
953
+ }
954
+
955
+ return modifier + c;
956
+ });
957
+
958
+ sorts = sorts.join(' ');
959
+
960
+ var info = { hasUTF8: false /*output*/ },
961
+ query = buildSearchQuery(criteria, this._caps, info),
962
+ charset = 'US-ASCII',
963
+ lines;
964
+ if (info.hasUTF8) {
965
+ charset = 'UTF-8';
966
+ lines = query.split(CRLF);
967
+ query = lines.shift();
968
+ }
969
+
970
+ this._enqueue(which + 'SORT (' + sorts + ') ' + charset + query, cb);
971
+ if (info.hasUTF8) {
972
+ var req = this._queue[this._queue.length - 1];
973
+ req.lines = lines;
974
+ }
975
+ };
976
+
977
+ Connection.prototype.esearch = function(criteria, options, cb) {
978
+ this._esearch('UID ', criteria, options, cb);
979
+ };
980
+
981
+ Connection.prototype._esearch = function(which, criteria, options, cb) {
982
+ if (this._box === undefined)
983
+ throw new Error('No mailbox is currently selected');
984
+ else if (!Array.isArray(criteria))
985
+ throw new Error('Expected array for search options');
986
+
987
+ var info = { hasUTF8: false /*output*/ },
988
+ query = buildSearchQuery(criteria, this._caps, info),
989
+ charset = '',
990
+ lines;
991
+ if (info.hasUTF8) {
992
+ charset = ' CHARSET UTF-8';
993
+ lines = query.split(CRLF);
994
+ query = lines.shift();
995
+ }
996
+
997
+ if (typeof options === 'function') {
998
+ cb = options;
999
+ options = '';
1000
+ } else if (!options)
1001
+ options = '';
1002
+
1003
+ if (Array.isArray(options))
1004
+ options = options.join(' ');
1005
+
1006
+ this._enqueue(which + 'SEARCH RETURN (' + options + ')' + charset + query, cb);
1007
+ if (info.hasUTF8) {
1008
+ var req = this._queue[this._queue.length - 1];
1009
+ req.lines = lines;
1010
+ }
1011
+ };
1012
+
1013
+ Connection.prototype.setQuota = function(quotaRoot, limits, cb) {
1014
+ if (typeof limits === 'function') {
1015
+ cb = limits;
1016
+ limits = {};
1017
+ }
1018
+
1019
+ var triplets = '';
1020
+ for (var l in limits) {
1021
+ if (triplets)
1022
+ triplets += ' ';
1023
+ triplets += l + ' ' + limits[l];
1024
+ }
1025
+
1026
+ quotaRoot = escape(utf7.encode(''+quotaRoot));
1027
+
1028
+ this._enqueue('SETQUOTA "' + quotaRoot + '" (' + triplets + ')',
1029
+ function(err, quotalist) {
1030
+ if (err)
1031
+ return cb(err);
1032
+
1033
+ cb(err, quotalist ? quotalist[0] : limits);
1034
+ }
1035
+ );
1036
+ };
1037
+
1038
+ Connection.prototype.getQuota = function(quotaRoot, cb) {
1039
+ quotaRoot = escape(utf7.encode(''+quotaRoot));
1040
+
1041
+ this._enqueue('GETQUOTA "' + quotaRoot + '"', function(err, quotalist) {
1042
+ if (err)
1043
+ return cb(err);
1044
+
1045
+ cb(err, quotalist[0]);
1046
+ });
1047
+ };
1048
+
1049
+ Connection.prototype.getQuotaRoot = function(boxName, cb) {
1050
+ boxName = escape(utf7.encode(''+boxName));
1051
+
1052
+ this._enqueue('GETQUOTAROOT "' + boxName + '"', function(err, quotalist) {
1053
+ if (err)
1054
+ return cb(err);
1055
+
1056
+ var quotas = {};
1057
+ if (quotalist) {
1058
+ for (var i = 0, len = quotalist.length; i < len; ++i)
1059
+ quotas[quotalist[i].root] = quotalist[i].resources;
1060
+ }
1061
+
1062
+ cb(err, quotas);
1063
+ });
1064
+ };
1065
+
1066
+ Connection.prototype.thread = function(algorithm, criteria, cb) {
1067
+ this._thread('UID ', algorithm, criteria, cb);
1068
+ };
1069
+
1070
+ Connection.prototype._thread = function(which, algorithm, criteria, cb) {
1071
+ algorithm = algorithm.toUpperCase();
1072
+
1073
+ if (!this.serverSupports('THREAD=' + algorithm))
1074
+ throw new Error('Server does not support that threading algorithm');
1075
+
1076
+ var info = { hasUTF8: false /*output*/ },
1077
+ query = buildSearchQuery(criteria, this._caps, info),
1078
+ charset = 'US-ASCII',
1079
+ lines;
1080
+ if (info.hasUTF8) {
1081
+ charset = 'UTF-8';
1082
+ lines = query.split(CRLF);
1083
+ query = lines.shift();
1084
+ }
1085
+
1086
+ this._enqueue(which + 'THREAD ' + algorithm + ' ' + charset + query, cb);
1087
+ if (info.hasUTF8) {
1088
+ var req = this._queue[this._queue.length - 1];
1089
+ req.lines = lines;
1090
+ }
1091
+ };
1092
+
1093
+ Connection.prototype.addFlagsSince = function(uids, flags, modseq, cb) {
1094
+ this._store('UID ',
1095
+ uids,
1096
+ { mode: '+', flags: flags, modseq: modseq },
1097
+ cb);
1098
+ };
1099
+
1100
+ Connection.prototype.delFlagsSince = function(uids, flags, modseq, cb) {
1101
+ this._store('UID ',
1102
+ uids,
1103
+ { mode: '-', flags: flags, modseq: modseq },
1104
+ cb);
1105
+ };
1106
+
1107
+ Connection.prototype.setFlagsSince = function(uids, flags, modseq, cb) {
1108
+ this._store('UID ',
1109
+ uids,
1110
+ { mode: '', flags: flags, modseq: modseq },
1111
+ cb);
1112
+ };
1113
+
1114
+ Connection.prototype.addKeywordsSince = function(uids, keywords, modseq, cb) {
1115
+ this._store('UID ',
1116
+ uids,
1117
+ { mode: '+', keywords: keywords, modseq: modseq },
1118
+ cb);
1119
+ };
1120
+
1121
+ Connection.prototype.delKeywordsSince = function(uids, keywords, modseq, cb) {
1122
+ this._store('UID ',
1123
+ uids,
1124
+ { mode: '-', keywords: keywords, modseq: modseq },
1125
+ cb);
1126
+ };
1127
+
1128
+ Connection.prototype.setKeywordsSince = function(uids, keywords, modseq, cb) {
1129
+ this._store('UID ',
1130
+ uids,
1131
+ { mode: '', keywords: keywords, modseq: modseq },
1132
+ cb);
1133
+ };
1134
+ // END Extension methods =======================================================
1135
+
1136
+ // Namespace for seqno-based commands
1137
+ Object.defineProperty(Connection.prototype, 'seq', { get: function() {
1138
+ var self = this;
1139
+ return {
1140
+ delKeywords: function(seqnos, keywords, cb) {
1141
+ self._store('', seqnos, { mode: '-', keywords: keywords }, cb);
1142
+ },
1143
+ addKeywords: function(seqnos, keywords, cb) {
1144
+ self._store('', seqnos, { mode: '+', keywords: keywords }, cb);
1145
+ },
1146
+ setKeywords: function(seqnos, keywords, cb) {
1147
+ self._store('', seqnos, { mode: '', keywords: keywords }, cb);
1148
+ },
1149
+
1150
+ delFlags: function(seqnos, flags, cb) {
1151
+ self._store('', seqnos, { mode: '-', flags: flags }, cb);
1152
+ },
1153
+ addFlags: function(seqnos, flags, cb) {
1154
+ self._store('', seqnos, { mode: '+', flags: flags }, cb);
1155
+ },
1156
+ setFlags: function(seqnos, flags, cb) {
1157
+ self._store('', seqnos, { mode: '', flags: flags }, cb);
1158
+ },
1159
+
1160
+ move: function(seqnos, boxTo, cb) {
1161
+ self._move('', seqnos, boxTo, cb);
1162
+ },
1163
+ copy: function(seqnos, boxTo, cb) {
1164
+ self._copy('', seqnos, boxTo, cb);
1165
+ },
1166
+ fetch: function(seqnos, options) {
1167
+ return self._fetch('', seqnos, options);
1168
+ },
1169
+ search: function(options, cb) {
1170
+ self._search('', options, cb);
1171
+ },
1172
+
1173
+ // Extensions ==============================================================
1174
+ delLabels: function(seqnos, labels, cb) {
1175
+ self._storeLabels('', seqnos, labels, '-', cb);
1176
+ },
1177
+ addLabels: function(seqnos, labels, cb) {
1178
+ self._storeLabels('', seqnos, labels, '+', cb);
1179
+ },
1180
+ setLabels: function(seqnos, labels, cb) {
1181
+ self._storeLabels('', seqnos, labels, '', cb);
1182
+ },
1183
+
1184
+ esearch: function(criteria, options, cb) {
1185
+ self._esearch('', criteria, options, cb);
1186
+ },
1187
+
1188
+ sort: function(sorts, options, cb) {
1189
+ self._sort('', sorts, options, cb);
1190
+ },
1191
+ thread: function(algorithm, criteria, cb) {
1192
+ self._thread('', algorithm, criteria, cb);
1193
+ },
1194
+
1195
+ delKeywordsSince: function(seqnos, keywords, modseq, cb) {
1196
+ self._store('',
1197
+ seqnos,
1198
+ { mode: '-', keywords: keywords, modseq: modseq },
1199
+ cb);
1200
+ },
1201
+ addKeywordsSince: function(seqnos, keywords, modseq, cb) {
1202
+ self._store('',
1203
+ seqnos,
1204
+ { mode: '+', keywords: keywords, modseq: modseq },
1205
+ cb);
1206
+ },
1207
+ setKeywordsSince: function(seqnos, keywords, modseq, cb) {
1208
+ self._store('',
1209
+ seqnos,
1210
+ { mode: '', keywords: keywords, modseq: modseq },
1211
+ cb);
1212
+ },
1213
+
1214
+ delFlagsSince: function(seqnos, flags, modseq, cb) {
1215
+ self._store('',
1216
+ seqnos,
1217
+ { mode: '-', flags: flags, modseq: modseq },
1218
+ cb);
1219
+ },
1220
+ addFlagsSince: function(seqnos, flags, modseq, cb) {
1221
+ self._store('',
1222
+ seqnos,
1223
+ { mode: '+', flags: flags, modseq: modseq },
1224
+ cb);
1225
+ },
1226
+ setFlagsSince: function(seqnos, flags, modseq, cb) {
1227
+ self._store('',
1228
+ seqnos,
1229
+ { mode: '', flags: flags, modseq: modseq },
1230
+ cb);
1231
+ }
1232
+ };
1233
+ }});
1234
+
1235
+ Connection.prototype._resUntagged = function(info) {
1236
+ var type = info.type, i, len, box, attrs, key;
1237
+
1238
+ if (type === 'bye')
1239
+ this._sock.end();
1240
+ else if (type === 'namespace')
1241
+ this.namespaces = info.text;
1242
+ else if (type === 'id')
1243
+ this._curReq.cbargs.push(info.text);
1244
+ else if (type === 'capability')
1245
+ this._caps = info.text.map(function(v) { return v.toUpperCase(); });
1246
+ else if (type === 'preauth')
1247
+ this.state = 'authenticated';
1248
+ else if (type === 'sort' || type === 'thread' || type === 'esearch')
1249
+ this._curReq.cbargs.push(info.text);
1250
+ else if (type === 'search') {
1251
+ if (info.text.results !== undefined) {
1252
+ // CONDSTORE-modified search results
1253
+ this._curReq.cbargs.push(info.text.results);
1254
+ this._curReq.cbargs.push(info.text.modseq);
1255
+ } else
1256
+ this._curReq.cbargs.push(info.text);
1257
+ } else if (type === 'quota') {
1258
+ var cbargs = this._curReq.cbargs;
1259
+ if (!cbargs.length)
1260
+ cbargs.push([]);
1261
+ cbargs[0].push(info.text);
1262
+ } else if (type === 'recent') {
1263
+ if (!this._box && RE_OPENBOX.test(this._curReq.type))
1264
+ this._createCurrentBox();
1265
+ if (this._box)
1266
+ this._box.messages.new = info.num;
1267
+ } else if (type === 'flags') {
1268
+ if (!this._box && RE_OPENBOX.test(this._curReq.type))
1269
+ this._createCurrentBox();
1270
+ if (this._box)
1271
+ this._box.flags = info.text;
1272
+ } else if (type === 'bad' || type === 'no') {
1273
+ if (this.state === 'connected' && !this._curReq) {
1274
+ clearTimeout(this._tmrConn);
1275
+ clearTimeout(this._tmrAuth);
1276
+ var err = new Error('Received negative welcome: ' + info.text);
1277
+ err.source = 'protocol';
1278
+ this.emit('error', err);
1279
+ this._sock.end();
1280
+ }
1281
+ } else if (type === 'exists') {
1282
+ if (!this._box && RE_OPENBOX.test(this._curReq.type))
1283
+ this._createCurrentBox();
1284
+ if (this._box) {
1285
+ var prev = this._box.messages.total,
1286
+ now = info.num;
1287
+ this._box.messages.total = now;
1288
+ if (now > prev && this.state === 'authenticated') {
1289
+ this._box.messages.new = now - prev;
1290
+ this.emit('mail', this._box.messages.new);
1291
+ }
1292
+ }
1293
+ } else if (type === 'expunge') {
1294
+ if (this._box) {
1295
+ if (this._box.messages.total > 0)
1296
+ --this._box.messages.total;
1297
+ this.emit('expunge', info.num);
1298
+ }
1299
+ } else if (type === 'ok') {
1300
+ if (this.state === 'connected' && !this._curReq)
1301
+ this._login();
1302
+ else if (typeof info.textCode === 'string'
1303
+ && info.textCode.toUpperCase() === 'ALERT')
1304
+ this.emit('alert', info.text);
1305
+ else if (this._curReq
1306
+ && info.textCode
1307
+ && (RE_OPENBOX.test(this._curReq.type))) {
1308
+ // we're opening a mailbox
1309
+
1310
+ if (!this._box)
1311
+ this._createCurrentBox();
1312
+
1313
+ if (info.textCode.key)
1314
+ key = info.textCode.key.toUpperCase();
1315
+ else
1316
+ key = info.textCode;
1317
+
1318
+ if (key === 'UIDVALIDITY')
1319
+ this._box.uidvalidity = info.textCode.val;
1320
+ else if (key === 'UIDNEXT')
1321
+ this._box.uidnext = info.textCode.val;
1322
+ else if (key === 'HIGHESTMODSEQ')
1323
+ this._box.highestmodseq = ''+info.textCode.val;
1324
+ else if (key === 'PERMANENTFLAGS') {
1325
+ var idx, permFlags, keywords;
1326
+ this._box.permFlags = permFlags = info.textCode.val;
1327
+ if ((idx = this._box.permFlags.indexOf('\\*')) > -1) {
1328
+ this._box.newKeywords = true;
1329
+ permFlags.splice(idx, 1);
1330
+ }
1331
+ this._box.keywords = keywords = permFlags.filter(function(f) {
1332
+ return (f[0] !== '\\');
1333
+ });
1334
+ for (i = 0, len = keywords.length; i < len; ++i)
1335
+ permFlags.splice(permFlags.indexOf(keywords[i]), 1);
1336
+ } else if (key === 'UIDNOTSTICKY')
1337
+ this._box.persistentUIDs = false;
1338
+ else if (key === 'NOMODSEQ')
1339
+ this._box.nomodseq = true;
1340
+ } else if (typeof info.textCode === 'string'
1341
+ && info.textCode.toUpperCase() === 'UIDVALIDITY')
1342
+ this.emit('uidvalidity', info.text);
1343
+ } else if (type === 'list' || type === 'lsub' || type === 'xlist') {
1344
+ if (this.delimiter === undefined)
1345
+ this.delimiter = info.text.delimiter;
1346
+ else {
1347
+ if (this._curReq.cbargs.length === 0)
1348
+ this._curReq.cbargs.push({});
1349
+
1350
+ box = {
1351
+ attribs: info.text.flags,
1352
+ delimiter: info.text.delimiter,
1353
+ children: null,
1354
+ parent: null
1355
+ };
1356
+
1357
+ for (i = 0, len = SPECIAL_USE_ATTRIBUTES.length; i < len; ++i)
1358
+ if (box.attribs.indexOf(SPECIAL_USE_ATTRIBUTES[i]) > -1)
1359
+ box.special_use_attrib = SPECIAL_USE_ATTRIBUTES[i];
1360
+
1361
+ var name = info.text.name,
1362
+ curChildren = this._curReq.cbargs[0];
1363
+
1364
+ if (box.delimiter) {
1365
+ var path = name.split(box.delimiter),
1366
+ parent = null;
1367
+ name = path.pop();
1368
+ for (i = 0, len = path.length; i < len; ++i) {
1369
+ if (!curChildren[path[i]])
1370
+ curChildren[path[i]] = {};
1371
+ if (!curChildren[path[i]].children)
1372
+ curChildren[path[i]].children = {};
1373
+ parent = curChildren[path[i]];
1374
+ curChildren = curChildren[path[i]].children;
1375
+ }
1376
+ box.parent = parent;
1377
+ }
1378
+ if (curChildren[name])
1379
+ box.children = curChildren[name].children;
1380
+ curChildren[name] = box;
1381
+ }
1382
+ } else if (type === 'status') {
1383
+ box = {
1384
+ name: info.text.name,
1385
+ uidnext: 0,
1386
+ uidvalidity: 0,
1387
+ messages: {
1388
+ total: 0,
1389
+ new: 0,
1390
+ unseen: 0
1391
+ }
1392
+ };
1393
+ attrs = info.text.attrs;
1394
+
1395
+ if (attrs) {
1396
+ if (attrs.recent !== undefined)
1397
+ box.messages.new = attrs.recent;
1398
+ if (attrs.unseen !== undefined)
1399
+ box.messages.unseen = attrs.unseen;
1400
+ if (attrs.messages !== undefined)
1401
+ box.messages.total = attrs.messages;
1402
+ if (attrs.uidnext !== undefined)
1403
+ box.uidnext = attrs.uidnext;
1404
+ if (attrs.uidvalidity !== undefined)
1405
+ box.uidvalidity = attrs.uidvalidity;
1406
+ if (attrs.highestmodseq !== undefined) // CONDSTORE
1407
+ box.highestmodseq = ''+attrs.highestmodseq;
1408
+ }
1409
+ this._curReq.cbargs.push(box);
1410
+ } else if (type === 'fetch') {
1411
+ if (/^(?:UID )?FETCH/.test(this._curReq.fullcmd)) {
1412
+ // FETCH response sent as result of FETCH request
1413
+ var msg = this._curReq.fetchCache[info.num],
1414
+ keys = Object.keys(info.text),
1415
+ keyslen = keys.length,
1416
+ toget, msgEmitter, j;
1417
+
1418
+ if (msg === undefined) {
1419
+ // simple case -- no bodies were streamed
1420
+ toget = this._curReq.fetching.slice(0);
1421
+ if (toget.length === 0)
1422
+ return;
1423
+
1424
+ msgEmitter = new EventEmitter();
1425
+ attrs = {};
1426
+
1427
+ this._curReq.bodyEmitter.emit('message', msgEmitter, info.num);
1428
+ } else {
1429
+ toget = msg.toget;
1430
+ msgEmitter = msg.msgEmitter;
1431
+ attrs = msg.attrs;
1432
+ }
1433
+
1434
+ i = toget.length;
1435
+ if (i === 0) {
1436
+ if (msg && !msg.ended) {
1437
+ msg.ended = true;
1438
+ process.nextTick(function() {
1439
+ msgEmitter.emit('end');
1440
+ });
1441
+ }
1442
+ return;
1443
+ }
1444
+
1445
+ if (keyslen > 0) {
1446
+ while (--i >= 0) {
1447
+ j = keyslen;
1448
+ while (--j >= 0) {
1449
+ if (keys[j].toUpperCase() === toget[i]) {
1450
+ if (!RE_BODYPART.test(toget[i])) {
1451
+ if (toget[i] === 'X-GM-LABELS') {
1452
+ var labels = info.text[keys[j]];
1453
+ for (var k = 0, lenk = labels.length; k < lenk; ++k)
1454
+ labels[k] = (''+labels[k]).replace(RE_ESCAPE, '\\');
1455
+ }
1456
+ key = FETCH_ATTR_MAP[toget[i]];
1457
+ if (!key)
1458
+ key = toget[i].toLowerCase();
1459
+ attrs[key] = info.text[keys[j]];
1460
+ }
1461
+ toget.splice(i, 1);
1462
+ break;
1463
+ }
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ if (toget.length === 0) {
1469
+ if (msg)
1470
+ msg.ended = true;
1471
+ process.nextTick(function() {
1472
+ msgEmitter.emit('attributes', attrs);
1473
+ msgEmitter.emit('end');
1474
+ });
1475
+ } else if (msg === undefined) {
1476
+ this._curReq.fetchCache[info.num] = {
1477
+ msgEmitter: msgEmitter,
1478
+ toget: toget,
1479
+ attrs: attrs,
1480
+ ended: false
1481
+ };
1482
+ }
1483
+ } else {
1484
+ // FETCH response sent as result of STORE request or sent unilaterally,
1485
+ // treat them as the same for now for simplicity
1486
+ this.emit('update', info.num, info.text);
1487
+ }
1488
+ }
1489
+ };
1490
+
1491
+ Connection.prototype._resTagged = function(info) {
1492
+ var req = this._curReq, err;
1493
+
1494
+ if (!req)
1495
+ return;
1496
+
1497
+ this._curReq = undefined;
1498
+
1499
+ if (info.type === 'no' || info.type === 'bad') {
1500
+ var errtext;
1501
+ if (info.text)
1502
+ errtext = info.text;
1503
+ else
1504
+ errtext = req.oauthError;
1505
+ err = new Error(errtext);
1506
+ err.type = info.type;
1507
+ err.textCode = info.textCode;
1508
+ err.source = 'protocol';
1509
+ } else if (this._box) {
1510
+ if (req.type === 'EXAMINE' || req.type === 'SELECT') {
1511
+ this._box.readOnly = (typeof info.textCode === 'string'
1512
+ && info.textCode.toUpperCase() === 'READ-ONLY');
1513
+ }
1514
+
1515
+ // According to RFC 3501, UID commands do not give errors for
1516
+ // non-existant user-supplied UIDs, so give the callback empty results
1517
+ // if we unexpectedly received no untagged responses.
1518
+ if (RE_UIDCMD_HASRESULTS.test(req.fullcmd) && req.cbargs.length === 0)
1519
+ req.cbargs.push([]);
1520
+ }
1521
+
1522
+ if (req.bodyEmitter) {
1523
+ var bodyEmitter = req.bodyEmitter;
1524
+ if (err)
1525
+ bodyEmitter.emit('error', err);
1526
+ process.nextTick(function() {
1527
+ bodyEmitter.emit('end');
1528
+ });
1529
+ } else {
1530
+ req.cbargs.unshift(err);
1531
+ if (info.textCode && info.textCode.key) {
1532
+ var key = info.textCode.key.toUpperCase();
1533
+ if (key === 'APPENDUID') // [uidvalidity, newUID]
1534
+ req.cbargs.push(info.textCode.val[1]);
1535
+ else if (key === 'COPYUID') // [uidvalidity, sourceUIDs, destUIDs]
1536
+ req.cbargs.push(info.textCode.val[2]);
1537
+ }
1538
+ req.cb && req.cb.apply(this, req.cbargs);
1539
+ }
1540
+
1541
+ if (this._queue.length === 0
1542
+ && this._config.keepalive
1543
+ && this.state === 'authenticated'
1544
+ && !this._idle.enabled) {
1545
+ this._idle.enabled = true;
1546
+ this._doKeepaliveTimer(true);
1547
+ }
1548
+
1549
+ this._processQueue();
1550
+ };
1551
+
1552
+ Connection.prototype._createCurrentBox = function() {
1553
+ this._box = {
1554
+ name: '',
1555
+ flags: [],
1556
+ readOnly: false,
1557
+ uidvalidity: 0,
1558
+ uidnext: 0,
1559
+ permFlags: [],
1560
+ keywords: [],
1561
+ newKeywords: false,
1562
+ persistentUIDs: true,
1563
+ nomodseq: false,
1564
+ messages: {
1565
+ total: 0,
1566
+ new: 0
1567
+ }
1568
+ };
1569
+ };
1570
+
1571
+ Connection.prototype._doKeepaliveTimer = function(immediate) {
1572
+ var self = this,
1573
+ interval = this._config.keepalive.interval || KEEPALIVE_INTERVAL,
1574
+ idleWait = this._config.keepalive.idleInterval || MAX_IDLE_WAIT,
1575
+ forceNoop = this._config.keepalive.forceNoop || false,
1576
+ timerfn = function() {
1577
+ if (self._idle.enabled) {
1578
+ // unlike NOOP, IDLE is only a valid command after authenticating
1579
+ if (!self.serverSupports('IDLE')
1580
+ || self.state !== 'authenticated'
1581
+ || forceNoop)
1582
+ self._enqueue('NOOP', true);
1583
+ else {
1584
+ if (self._idle.started === undefined) {
1585
+ self._idle.started = 0;
1586
+ self._enqueue('IDLE', true);
1587
+ } else if (self._idle.started > 0) {
1588
+ var timeDiff = Date.now() - self._idle.started;
1589
+ if (timeDiff >= idleWait) {
1590
+ self._idle.enabled = false;
1591
+ self.debug && self.debug('=> DONE');
1592
+ self._sock.write('DONE' + CRLF);
1593
+ return;
1594
+ }
1595
+ }
1596
+ self._tmrKeepalive = setTimeout(timerfn, interval);
1597
+ }
1598
+ }
1599
+ };
1600
+
1601
+ if (immediate)
1602
+ timerfn();
1603
+ else
1604
+ this._tmrKeepalive = setTimeout(timerfn, interval);
1605
+ };
1606
+
1607
+ Connection.prototype._login = function() {
1608
+ var self = this, checkedNS = false;
1609
+
1610
+ var reentry = function(err) {
1611
+ clearTimeout(self._tmrAuth);
1612
+ if (err) {
1613
+ self.emit('error', err);
1614
+ return self._sock.end();
1615
+ }
1616
+
1617
+ // 2. Get the list of available namespaces (RFC2342)
1618
+ if (!checkedNS && self.serverSupports('NAMESPACE')) {
1619
+ checkedNS = true;
1620
+ return self._enqueue('NAMESPACE', reentry);
1621
+ }
1622
+
1623
+ // 3. Get the top-level mailbox hierarchy delimiter used by the server
1624
+ self._enqueue('LIST "" ""', function() {
1625
+ self.state = 'authenticated';
1626
+ self.emit('ready');
1627
+ });
1628
+ };
1629
+
1630
+ // 1. Get the supported capabilities
1631
+ self._enqueue('CAPABILITY', function() {
1632
+ // No need to attempt the login sequence if we're on a PREAUTH connection.
1633
+ if (self.state === 'connected') {
1634
+ var err,
1635
+ checkCaps = function(error) {
1636
+ if (error) {
1637
+ error.source = 'authentication';
1638
+ return reentry(error);
1639
+ }
1640
+
1641
+ if (self._caps === undefined) {
1642
+ // Fetch server capabilities if they were not automatically
1643
+ // provided after authentication
1644
+ return self._enqueue('CAPABILITY', reentry);
1645
+ } else
1646
+ reentry();
1647
+ };
1648
+
1649
+ if (self.serverSupports('STARTTLS')
1650
+ && (self._config.autotls === 'always'
1651
+ || (self._config.autotls === 'required'
1652
+ && self.serverSupports('LOGINDISABLED')))) {
1653
+ self._starttls();
1654
+ return;
1655
+ }
1656
+
1657
+ if (self.serverSupports('LOGINDISABLED')) {
1658
+ err = new Error('Logging in is disabled on this server');
1659
+ err.source = 'authentication';
1660
+ return reentry(err);
1661
+ }
1662
+
1663
+ var cmd;
1664
+ if (self.serverSupports('AUTH=XOAUTH') && self._config.xoauth) {
1665
+ self._caps = undefined;
1666
+ cmd = 'AUTHENTICATE XOAUTH';
1667
+ // are there any servers that support XOAUTH/XOAUTH2 and not SASL-IR?
1668
+ //if (self.serverSupports('SASL-IR'))
1669
+ cmd += ' ' + escape(self._config.xoauth);
1670
+ self._enqueue(cmd, checkCaps);
1671
+ } else if (self.serverSupports('AUTH=XOAUTH2') && self._config.xoauth2) {
1672
+ self._caps = undefined;
1673
+ cmd = 'AUTHENTICATE XOAUTH2';
1674
+ //if (self.serverSupports('SASL-IR'))
1675
+ cmd += ' ' + escape(self._config.xoauth2);
1676
+ self._enqueue(cmd, checkCaps);
1677
+ } else if (self._config.user && self._config.password) {
1678
+ self._caps = undefined;
1679
+ self._enqueue('LOGIN "' + escape(self._config.user) + '" "'
1680
+ + escape(self._config.password) + '"', checkCaps);
1681
+ } else {
1682
+ err = new Error('No supported authentication method(s) available. '
1683
+ + 'Unable to login.');
1684
+ err.source = 'authentication';
1685
+ return reentry(err);
1686
+ }
1687
+ } else
1688
+ reentry();
1689
+ });
1690
+ };
1691
+
1692
+ Connection.prototype._starttls = function() {
1693
+ var self = this;
1694
+ this._enqueue('STARTTLS', function(err) {
1695
+ if (err) {
1696
+ self.emit('error', err);
1697
+ return self._sock.end();
1698
+ }
1699
+
1700
+ self._caps = undefined;
1701
+ self._sock.removeAllListeners('error');
1702
+
1703
+ var tlsOptions = {};
1704
+
1705
+ tlsOptions.host = this._config.host;
1706
+ // Host name may be overridden the tlsOptions
1707
+ for (var k in this._config.tlsOptions)
1708
+ tlsOptions[k] = this._config.tlsOptions[k];
1709
+ tlsOptions.socket = self._sock;
1710
+
1711
+ self._sock = tls.connect(tlsOptions, function() {
1712
+ self._login();
1713
+ });
1714
+
1715
+ self._sock.on('error', self._onError);
1716
+ self._sock.on('timeout', self._onSocketTimeout);
1717
+ self._sock.setTimeout(self._config.socketTimeout);
1718
+
1719
+ self._parser.setStream(self._sock);
1720
+ });
1721
+ };
1722
+
1723
+ Connection.prototype._processQueue = function() {
1724
+ if (this._curReq || !this._queue.length || !this._sock || !this._sock.writable)
1725
+ return;
1726
+
1727
+ this._curReq = this._queue.shift();
1728
+
1729
+ if (this._tagcount === MAX_INT)
1730
+ this._tagcount = 0;
1731
+
1732
+ var prefix;
1733
+
1734
+ if (this._curReq.type === 'IDLE' || this._curReq.type === 'NOOP')
1735
+ prefix = this._curReq.type;
1736
+ else
1737
+ prefix = 'A' + (this._tagcount++);
1738
+
1739
+ var out = prefix + ' ' + this._curReq.fullcmd;
1740
+ this.debug && this.debug('=> ' + inspect(out));
1741
+ this._sock.write(out + CRLF, 'utf8');
1742
+
1743
+ if (this._curReq.literalAppendData) {
1744
+ // LITERAL+: we are appending a mesage, and not waiting for a reply
1745
+ this._sockWriteAppendData(this._curReq.literalAppendData);
1746
+ }
1747
+ };
1748
+
1749
+ Connection.prototype._sockWriteAppendData = function(appendData)
1750
+ {
1751
+ var val = appendData;
1752
+ if (Buffer.isBuffer(appendData))
1753
+ val = val.toString('utf8');
1754
+
1755
+ this.debug && this.debug('=> ' + inspect(val));
1756
+ this._sock.write(val);
1757
+ this._sock.write(CRLF);
1758
+ };
1759
+
1760
+ Connection.prototype._enqueue = function(fullcmd, promote, cb) {
1761
+ if (typeof promote === 'function') {
1762
+ cb = promote;
1763
+ promote = false;
1764
+ }
1765
+
1766
+ var info = {
1767
+ type: fullcmd.match(RE_CMD)[1],
1768
+ fullcmd: fullcmd,
1769
+ cb: cb,
1770
+ cbargs: []
1771
+ },
1772
+ self = this;
1773
+
1774
+ if (promote)
1775
+ this._queue.unshift(info);
1776
+ else
1777
+ this._queue.push(info);
1778
+
1779
+ if (!this._curReq
1780
+ && this.state !== 'disconnected'
1781
+ && this.state !== 'upgrading') {
1782
+ // defer until next tick for requests like APPEND and FETCH where access to
1783
+ // the request object is needed immediately after enqueueing
1784
+ process.nextTick(function() { self._processQueue(); });
1785
+ } else if (this._curReq
1786
+ && this._curReq.type === 'IDLE'
1787
+ && this._sock
1788
+ && this._sock.writable
1789
+ && this._idle.enabled) {
1790
+ this._idle.enabled = false;
1791
+ clearTimeout(this._tmrKeepalive);
1792
+ if (this._idle.started > 0) {
1793
+ // we've seen the continuation for our IDLE
1794
+ this.debug && this.debug('=> DONE');
1795
+ this._sock.write('DONE' + CRLF);
1796
+ }
1797
+ }
1798
+ };
1799
+
1800
+ Connection.parseHeader = parseHeader; // from Parser.js
1801
+
1802
+ module.exports = Connection;
1803
+
1804
+ // utilities -------------------------------------------------------------------
1805
+
1806
+ function escape(str) {
1807
+ return str.replace(RE_BACKSLASH, '\\\\').replace(RE_DBLQUOTE, '\\"');
1808
+ }
1809
+
1810
+ function validateUIDList(uids, noThrow) {
1811
+ for (var i = 0, len = uids.length, intval; i < len; ++i) {
1812
+ if (typeof uids[i] === 'string') {
1813
+ if (uids[i] === '*' || uids[i] === '*:*') {
1814
+ if (len > 1)
1815
+ uids = ['*'];
1816
+ break;
1817
+ } else if (RE_NUM_RANGE.test(uids[i]))
1818
+ continue;
1819
+ }
1820
+ intval = parseInt(''+uids[i], 10);
1821
+ if (isNaN(intval)) {
1822
+ var err = new Error('UID/seqno must be an integer, "*", or a range: '
1823
+ + uids[i]);
1824
+ if (noThrow)
1825
+ return err;
1826
+ else
1827
+ throw err;
1828
+ } else if (intval <= 0) {
1829
+ var err = new Error('UID/seqno must be greater than zero');
1830
+ if (noThrow)
1831
+ return err;
1832
+ else
1833
+ throw err;
1834
+ } else if (typeof uids[i] !== 'number') {
1835
+ uids[i] = intval;
1836
+ }
1837
+ }
1838
+ }
1839
+
1840
+ function hasNonASCII(str) {
1841
+ for (var i = 0, len = str.length; i < len; ++i) {
1842
+ if (str.charCodeAt(i) > 0x7F)
1843
+ return true;
1844
+ }
1845
+ return false;
1846
+ }
1847
+
1848
+ function buildString(str) {
1849
+ if (typeof str !== 'string')
1850
+ str = ''+str;
1851
+
1852
+ if (hasNonASCII(str)) {
1853
+ var buf = Buffer.from(str, 'utf8');
1854
+ return '{' + buf.length + '}\r\n' + buf.toString('latin1');
1855
+ } else
1856
+ return '"' + escape(str) + '"';
1857
+ }
1858
+
1859
+ function buildSearchQuery(options, extensions, info, isOrChild) {
1860
+ var searchargs = '', err, val;
1861
+ for (var i = 0, len = options.length; i < len; ++i) {
1862
+ var criteria = (isOrChild ? options : options[i]),
1863
+ args = null,
1864
+ modifier = (isOrChild ? '' : ' ');
1865
+ if (typeof criteria === 'string')
1866
+ criteria = criteria.toUpperCase();
1867
+ else if (Array.isArray(criteria)) {
1868
+ if (criteria.length > 1)
1869
+ args = criteria.slice(1);
1870
+ if (criteria.length > 0)
1871
+ criteria = criteria[0].toUpperCase();
1872
+ } else
1873
+ throw new Error('Unexpected search option data type. '
1874
+ + 'Expected string or array. Got: ' + typeof criteria);
1875
+ if (criteria === 'OR') {
1876
+ if (args.length !== 2)
1877
+ throw new Error('OR must have exactly two arguments');
1878
+ if (isOrChild)
1879
+ searchargs += 'OR (';
1880
+ else
1881
+ searchargs += ' OR (';
1882
+ searchargs += buildSearchQuery(args[0], extensions, info, true);
1883
+ searchargs += ') (';
1884
+ searchargs += buildSearchQuery(args[1], extensions, info, true);
1885
+ searchargs += ')';
1886
+ } else {
1887
+ if (criteria[0] === '!') {
1888
+ modifier += 'NOT ';
1889
+ criteria = criteria.substr(1);
1890
+ }
1891
+ switch(criteria) {
1892
+ // -- Standard criteria --
1893
+ case 'ALL':
1894
+ case 'ANSWERED':
1895
+ case 'DELETED':
1896
+ case 'DRAFT':
1897
+ case 'FLAGGED':
1898
+ case 'NEW':
1899
+ case 'SEEN':
1900
+ case 'RECENT':
1901
+ case 'OLD':
1902
+ case 'UNANSWERED':
1903
+ case 'UNDELETED':
1904
+ case 'UNDRAFT':
1905
+ case 'UNFLAGGED':
1906
+ case 'UNSEEN':
1907
+ searchargs += modifier + criteria;
1908
+ break;
1909
+ case 'BCC':
1910
+ case 'BODY':
1911
+ case 'CC':
1912
+ case 'FROM':
1913
+ case 'SUBJECT':
1914
+ case 'TEXT':
1915
+ case 'TO':
1916
+ if (!args || args.length !== 1)
1917
+ throw new Error('Incorrect number of arguments for search option: '
1918
+ + criteria);
1919
+ val = buildString(args[0]);
1920
+ if (info && val[0] === '{')
1921
+ info.hasUTF8 = true;
1922
+ searchargs += modifier + criteria + ' ' + val;
1923
+ break;
1924
+ case 'BEFORE':
1925
+ case 'ON':
1926
+ case 'SENTBEFORE':
1927
+ case 'SENTON':
1928
+ case 'SENTSINCE':
1929
+ case 'SINCE':
1930
+ if (!args || args.length !== 1)
1931
+ throw new Error('Incorrect number of arguments for search option: '
1932
+ + criteria);
1933
+ else if (!(args[0] instanceof Date)) {
1934
+ if ((args[0] = new Date(args[0])).toString() === 'Invalid Date')
1935
+ throw new Error('Search option argument must be a Date object'
1936
+ + ' or a parseable date string');
1937
+ }
1938
+ searchargs += modifier + criteria + ' ' + args[0].getDate() + '-'
1939
+ + MONTHS[args[0].getMonth()] + '-'
1940
+ + args[0].getFullYear();
1941
+ break;
1942
+ case 'KEYWORD':
1943
+ case 'UNKEYWORD':
1944
+ if (!args || args.length !== 1)
1945
+ throw new Error('Incorrect number of arguments for search option: '
1946
+ + criteria);
1947
+ searchargs += modifier + criteria + ' ' + args[0];
1948
+ break;
1949
+ case 'LARGER':
1950
+ case 'SMALLER':
1951
+ if (!args || args.length !== 1)
1952
+ throw new Error('Incorrect number of arguments for search option: '
1953
+ + criteria);
1954
+ var num = parseInt(args[0], 10);
1955
+ if (isNaN(num))
1956
+ throw new Error('Search option argument must be a number');
1957
+ searchargs += modifier + criteria + ' ' + args[0];
1958
+ break;
1959
+ case 'HEADER':
1960
+ if (!args || args.length !== 2)
1961
+ throw new Error('Incorrect number of arguments for search option: '
1962
+ + criteria);
1963
+ val = buildString(args[1]);
1964
+ if (info && val[0] === '{')
1965
+ info.hasUTF8 = true;
1966
+ searchargs += modifier + criteria + ' "' + escape(''+args[0])
1967
+ + '" ' + val;
1968
+ break;
1969
+ case 'UID':
1970
+ if (!args)
1971
+ throw new Error('Incorrect number of arguments for search option: '
1972
+ + criteria);
1973
+ validateUIDList(args);
1974
+ if (args.length === 0)
1975
+ throw new Error('Empty uid list');
1976
+ searchargs += modifier + criteria + ' ' + args.join(',');
1977
+ break;
1978
+ // Extensions ==========================================================
1979
+ case 'X-GM-MSGID': // Gmail unique message ID
1980
+ case 'X-GM-THRID': // Gmail thread ID
1981
+ if (extensions.indexOf('X-GM-EXT-1') === -1)
1982
+ throw new Error('IMAP extension not available for: ' + criteria);
1983
+ if (!args || args.length !== 1)
1984
+ throw new Error('Incorrect number of arguments for search option: '
1985
+ + criteria);
1986
+ else {
1987
+ val = ''+args[0];
1988
+ if (!(RE_INTEGER.test(args[0])))
1989
+ throw new Error('Invalid value');
1990
+ }
1991
+ searchargs += modifier + criteria + ' ' + val;
1992
+ break;
1993
+ case 'X-GM-RAW': // Gmail search syntax
1994
+ if (extensions.indexOf('X-GM-EXT-1') === -1)
1995
+ throw new Error('IMAP extension not available for: ' + criteria);
1996
+ if (!args || args.length !== 1)
1997
+ throw new Error('Incorrect number of arguments for search option: '
1998
+ + criteria);
1999
+ val = buildString(args[0]);
2000
+ if (info && val[0] === '{')
2001
+ info.hasUTF8 = true;
2002
+ searchargs += modifier + criteria + ' ' + val;
2003
+ break;
2004
+ case 'X-GM-LABELS': // Gmail labels
2005
+ if (extensions.indexOf('X-GM-EXT-1') === -1)
2006
+ throw new Error('IMAP extension not available for: ' + criteria);
2007
+ if (!args || args.length !== 1)
2008
+ throw new Error('Incorrect number of arguments for search option: '
2009
+ + criteria);
2010
+ searchargs += modifier + criteria + ' ' + args[0];
2011
+ break;
2012
+ case 'MODSEQ':
2013
+ if (extensions.indexOf('CONDSTORE') === -1)
2014
+ throw new Error('IMAP extension not available for: ' + criteria);
2015
+ if (!args || args.length !== 1)
2016
+ throw new Error('Incorrect number of arguments for search option: '
2017
+ + criteria);
2018
+ searchargs += modifier + criteria + ' ' + args[0];
2019
+ break;
2020
+ default:
2021
+ // last hope it's a seqno set
2022
+ // http://tools.ietf.org/html/rfc3501#section-6.4.4
2023
+ var seqnos = (args ? [criteria].concat(args) : [criteria]);
2024
+ if (!validateUIDList(seqnos, true)) {
2025
+ if (seqnos.length === 0)
2026
+ throw new Error('Empty sequence number list');
2027
+ searchargs += modifier + seqnos.join(',');
2028
+ } else
2029
+ throw new Error('Unexpected search option: ' + criteria);
2030
+ }
2031
+ }
2032
+ if (isOrChild)
2033
+ break;
2034
+ }
2035
+ return searchargs;
2036
+ }
2037
+
2038
+ // Pulled from assert.deepEqual:
2039
+ var pSlice = Array.prototype.slice;
2040
+ function _deepEqual(actual, expected) {
2041
+ // 7.1. All identical values are equivalent, as determined by ===.
2042
+ if (actual === expected) {
2043
+ return true;
2044
+
2045
+ } else if (Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) {
2046
+ if (actual.length !== expected.length) return false;
2047
+
2048
+ for (var i = 0; i < actual.length; i++) {
2049
+ if (actual[i] !== expected[i]) return false;
2050
+ }
2051
+
2052
+ return true;
2053
+
2054
+ // 7.2. If the expected value is a Date object, the actual value is
2055
+ // equivalent if it is also a Date object that refers to the same time.
2056
+ } else if (actual instanceof Date && expected instanceof Date) {
2057
+ return actual.getTime() === expected.getTime();
2058
+
2059
+ // 7.3 If the expected value is a RegExp object, the actual value is
2060
+ // equivalent if it is also a RegExp object with the same source and
2061
+ // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
2062
+ } else if (actual instanceof RegExp && expected instanceof RegExp) {
2063
+ return actual.source === expected.source &&
2064
+ actual.global === expected.global &&
2065
+ actual.multiline === expected.multiline &&
2066
+ actual.lastIndex === expected.lastIndex &&
2067
+ actual.ignoreCase === expected.ignoreCase;
2068
+
2069
+ // 7.4. Other pairs that do not both pass typeof value == 'object',
2070
+ // equivalence is determined by ==.
2071
+ } else if (typeof actual !== 'object' && typeof expected !== 'object') {
2072
+ return actual == expected;
2073
+
2074
+ // 7.5 For all other Object pairs, including Array objects, equivalence is
2075
+ // determined by having the same number of owned properties (as verified
2076
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
2077
+ // (although not necessarily the same order), equivalent values for every
2078
+ // corresponding key, and an identical 'prototype' property. Note: this
2079
+ // accounts for both named and indexed properties on Arrays.
2080
+ } else {
2081
+ return objEquiv(actual, expected);
2082
+ }
2083
+ }
2084
+ function isUndefinedOrNull(value) {
2085
+ return value === null || value === undefined;
2086
+ }
2087
+ function isArguments(object) {
2088
+ return Object.prototype.toString.call(object) === '[object Arguments]';
2089
+ }
2090
+ function objEquiv(a, b) {
2091
+ var ka, kb, key, i;
2092
+ if (isUndefinedOrNull(a) || isUndefinedOrNull(b))
2093
+ return false;
2094
+ // an identical 'prototype' property.
2095
+ if (a.prototype !== b.prototype) return false;
2096
+ //~~~I've managed to break Object.keys through screwy arguments passing.
2097
+ // Converting to array solves the problem.
2098
+ if (isArguments(a)) {
2099
+ if (!isArguments(b)) {
2100
+ return false;
2101
+ }
2102
+ a = pSlice.call(a);
2103
+ b = pSlice.call(b);
2104
+ return _deepEqual(a, b);
2105
+ }
2106
+ try {
2107
+ ka = Object.keys(a);
2108
+ kb = Object.keys(b);
2109
+ } catch (e) {//happens when one is a string literal and the other isn't
2110
+ return false;
2111
+ }
2112
+ // having the same number of owned properties (keys incorporates
2113
+ // hasOwnProperty)
2114
+ if (ka.length !== kb.length)
2115
+ return false;
2116
+ //the same set of keys (although not necessarily the same order),
2117
+ ka.sort();
2118
+ kb.sort();
2119
+ //~~~cheap key test
2120
+ for (i = ka.length - 1; i >= 0; i--) {
2121
+ if (ka[i] != kb[i])
2122
+ return false;
2123
+ }
2124
+ //equivalent values for every corresponding key, and
2125
+ //~~~possibly expensive deep test
2126
+ for (i = ka.length - 1; i >= 0; i--) {
2127
+ key = ka[i];
2128
+ if (!_deepEqual(a[key], b[key])) return false;
2129
+ }
2130
+ return true;
2131
+ }