"use strict" ;
const Promise = require ( "bluebird" ) ;
const defaultValue = require ( "default-value" ) ;
const asExpression = require ( "as-expression" ) ;
const matchValue = require ( "match-value" ) ;
const unreachable = require ( "@joepie91/unreachable" ) ;
const { validateArguments } = require ( "@validatem/core" ) ;
const required = require ( "@validatem/required" ) ;
const anyProperty = require ( "@validatem/any-property" ) ;
const isString = require ( "@validatem/is-string" ) ;
const isFunction = require ( "@validatem/is-function" ) ;
const { command , unsafeRaw , already7Bit } = require ( "./util/command" ) ;
const pInterval = require ( "./util/p-interval" ) ;
const createFetchTaskTracker = require ( "./util/fetch-task" ) ;
const createBoxTreeBuilder = require ( "./util/box-tree-builder" ) ;
const ensureArray = require ( "./util/ensure-array" ) ;
var tls = require ( 'tls' ) ,
Socket = require ( 'net' ) . Socket ,
EventEmitter = require ( 'events' ) . EventEmitter ,
inherits = require ( 'util' ) . inherits ,
inspect = require ( 'util' ) . inspect ,
isDate = require ( 'util' ) . isDate ,
utf7 = require ( 'utf7' ) . imap ;
var Parser = require ( './Parser' ) . Parser ,
parseExpr = require ( './Parser' ) . parseExpr ,
parseHeader = require ( './Parser' ) . parseHeader ;
var MAX _INT = 9007199254740992 ,
KEEPALIVE _INTERVAL = 10000 ,
MAX _IDLE _WAIT = 300000 , // 5 minutes
MONTHS = [ 'Jan' , 'Feb' , 'Mar' ,
'Apr' , 'May' , 'Jun' ,
'Jul' , 'Aug' , 'Sep' ,
'Oct' , 'Nov' , 'Dec' ] ,
FETCH _ATTR _MAP = {
'RFC822.SIZE' : 'size' ,
'BODY' : 'struct' ,
'BODYSTRUCTURE' : 'struct' ,
'ENVELOPE' : 'envelope' ,
'INTERNALDATE' : 'date'
} ,
SPECIAL _USE _ATTRIBUTES = [
'\\All' ,
'\\Archive' ,
'\\Drafts' ,
'\\Flagged' ,
'\\Important' ,
'\\Junk' ,
'\\Sent' ,
'\\Trash'
] ,
CRLF = '\r\n' ,
RE _CMD = /^([^ ]+)(?: |$)/ ,
RE _UIDCMD _HASRESULTS = /^UID (?:FETCH|SEARCH|SORT)/ ,
RE _IDLENOOPRES = /^(IDLE|NOOP) / ,
RE _OPENBOX = /^EXAMINE|SELECT$/ ,
RE _BODYPART = /^BODY\[/ ,
RE _INVALID _KW _CHARS = /[\(\)\{\\\"\]\%\*\x00-\x20\x7F]/ ,
RE _NUM _RANGE = /^(?:[\d]+|\*):(?:[\d]+|\*)$/ ,
RE _BACKSLASH = /\\/g ,
RE _DBLQUOTE = /"/g ,
RE _ESCAPE = /\\\\/g ,
RE _INTEGER = /^\d+$/ ;
function Connection ( config ) {
if ( ! ( this instanceof Connection ) )
return new Connection ( config ) ;
EventEmitter . call ( this ) ;
config || ( config = { } ) ;
this . _config = {
localAddress : config . localAddress ,
socket : config . socket ,
socketTimeout : config . socketTimeout || 0 ,
host : config . host || 'localhost' ,
port : config . port || 143 ,
tls : config . tls ,
tlsOptions : config . tlsOptions ,
autotls : config . autotls ,
user : config . user ,
password : config . password ,
xoauth : config . xoauth ,
xoauth2 : config . xoauth2 ,
connTimeout : config . connTimeout || 10000 ,
authTimeout : config . authTimeout || 5000 ,
keepalive : ( config . keepalive === undefined || config . keepalive === null
? true
: config . keepalive )
} ;
this . _sock = config . socket || undefined ;
this . _tagcount = 0 ;
this . _connectionTimeout = undefined ;
this . _authenticationTimeout = undefined ;
this . _queue = [ ] ;
this . _box = undefined ;
this . _idle = { started : undefined , enabled : false } ;
this . _parser = undefined ;
this . _curReq = undefined ;
this . delimiter = undefined ;
this . namespaces = undefined ;
this . state = 'disconnected' ;
this . debug = config . debug ;
}
inherits ( Connection , EventEmitter ) ;
Connection . prototype . connect = function ( ) {
var config = this . _config ,
self = this ,
socket ,
parser ,
tlsOptions ;
socket = config . socket || new Socket ( ) ;
socket . setKeepAlive ( true ) ;
this . _sock = undefined ;
this . _tagcount = 0 ;
this . _connectionTimeout = undefined ;
this . _authenticationTimeout = undefined ;
this . _queue = [ ] ;
this . _box = undefined ;
this . _idle = { started : undefined , enabled : false } ;
this . _parser = undefined ;
this . _curReq = undefined ;
this . delimiter = undefined ;
this . namespaces = undefined ;
this . state = 'disconnected' ;
this . _cancelKeepaliveTimer = function ( ) { } ;
if ( config . tls ) {
tlsOptions = { } ;
tlsOptions . host = config . host ;
// Host name may be overridden the tlsOptions
for ( var k in config . tlsOptions )
tlsOptions [ k ] = config . tlsOptions [ k ] ;
tlsOptions . socket = socket ;
}
if ( config . tls )
this . _sock = tls . connect ( tlsOptions , onconnect ) ;
else {
socket . once ( 'connect' , onconnect ) ;
this . _sock = socket ;
}
function onconnect ( ) {
clearTimeout ( self . _connectionTimeout ) ;
self . state = 'connected' ;
self . debug && self . debug ( '[connection] Connected to host' ) ;
self . _authenticationTimeout = setTimeout ( function ( ) {
var err = new Error ( 'Timed out while authenticating with server' ) ;
err . source = 'timeout-auth' ;
self . emit ( 'error' , err ) ;
socket . destroy ( ) ;
} , config . authTimeout ) ;
}
this . _onError = function ( err ) {
clearTimeout ( self . _connectionTimeout ) ;
clearTimeout ( self . _authenticationTimeout ) ;
self . debug && self . debug ( '[connection] Error: ' + err ) ;
err . source = 'socket' ;
self . emit ( 'error' , err ) ;
} ;
this . _sock . on ( 'error' , this . _onError ) ;
this . _teardownConnection = function ( ) {
clearTimeout ( self . _connectionTimeout ) ;
clearTimeout ( self . _authenticationTimeout ) ;
self . _cancelKeepaliveTimer ( ) ;
self . state = 'disconnected' ;
} ;
this . _onSocketTimeout = function ( ) {
self . _teardownConnection ( ) ;
self . debug && self . debug ( '[connection] Socket timeout' ) ;
var err = new Error ( 'Socket timed out while talking to server' ) ;
err . source = 'socket-timeout' ;
self . emit ( 'error' , err ) ;
socket . destroy ( ) ;
} ;
this . _sock . on ( 'timeout' , this . _onSocketTimeout ) ;
socket . setTimeout ( config . socketTimeout ) ;
socket . once ( 'close' , function ( had _err ) {
self . _teardownConnection ( ) ;
self . debug && self . debug ( '[connection] Closed' ) ;
self . emit ( 'close' , had _err ) ;
} ) ;
socket . once ( 'end' , function ( ) {
self . _teardownConnection ( ) ;
self . debug && self . debug ( '[connection] Ended' ) ;
self . emit ( 'end' ) ;
} ) ;
this . _parser = parser = new Parser ( this . _sock , this . debug ) ;
parser . on ( 'untagged' , function ( info ) {
self . _resUntagged ( info ) ;
} ) ;
parser . on ( 'tagged' , function ( info ) {
self . _resTagged ( info ) ;
} ) ;
parser . on ( 'body' , function ( stream , info ) {
var task = self . _curReq . fetchCache . get ( info . seqno ) , remainingKeys ;
if ( task == null ) {
task = self . _curReq . fetchCache . create ( info . seqno , self . _curReq . fetching . slice ( ) ) ;
self . _curReq . bodyEmitter . emit ( 'message' , task . emitter , info . seqno ) ;
}
remainingKeys = task . getRemainingKeys ( ) ;
// FIXME: Refactor below
// here we compare the parsed version of the expression inside BODY[]
// because 'HEADER.FIELDS (TO FROM)' really is equivalent to
// 'HEADER.FIELDS ("TO" "FROM")' and some servers will actually send the
// quoted form even if the client did not use quotes
var thisbody = parseExpr ( info . which ) ;
for ( var i = 0 , len = remainingKeys . length ; i < len ; ++ i ) {
if ( _deepEqual ( thisbody , remainingKeys [ i ] ) ) {
remainingKeys . splice ( i , 1 ) ;
task . emitter . emit ( 'body' , stream , info ) ;
return ;
}
}
stream . resume ( ) ; // a body we didn't ask for?
} ) ;
parser . on ( 'continue' , function ( info ) {
var type = self . _curReq . type ;
if ( type === 'IDLE' ) {
if ( self . _queue . length
&& self . _idle . started === 0
&& self . _curReq
&& self . _curReq . type === 'IDLE'
&& self . _sock
&& self . _sock . writable
&& ! self . _idle . enabled ) {
self . debug && self . debug ( '=> DONE' ) ;
self . _sock . write ( 'DONE' + CRLF ) ;
return ;
}
// now idling
self . _idle . started = Date . now ( ) ;
} else if ( /^AUTHENTICATE XOAUTH/ . test ( self . _curReq . fullcmd ) ) {
self . _curReq . oauthError = Buffer . from ( info . text , 'base64' ) . toString ( 'utf8' ) ;
self . debug && self . debug ( '=> ' + inspect ( CRLF ) ) ;
self . _sock . write ( CRLF ) ;
} else if ( type === 'APPEND' ) {
self . _sockWriteAppendData ( self . _curReq . appendData ) ;
} else if ( self . _curReq . lines && self . _curReq . lines . length ) {
var line = self . _curReq . lines . shift ( ) + '\r\n' ;
self . debug && self . debug ( '=> ' + inspect ( line ) ) ;
self . _sock . write ( line , 'binary' ) ;
}
} ) ;
parser . on ( 'other' , function ( line ) {
var m ;
if ( m = RE _IDLENOOPRES . exec ( line ) ) {
// no longer idling
self . _idle . enabled = false ;
self . _idle . started = undefined ;
self . _cancelKeepaliveTimer ( ) ;
self . _curReq = undefined ;
if ( self . _queue . length === 0
&& self . _config . keepalive
&& self . state === 'authenticated'
&& ! self . _idle . enabled ) {
self . _idle . enabled = true ;
if ( m [ 1 ] === 'NOOP' )
self . _doKeepaliveTimer ( ) ;
else
self . _doKeepaliveTimer ( true ) ;
}
self . _processQueue ( ) ;
}
} ) ;
this . _connectionTimeout = setTimeout ( function ( ) {
var err = new Error ( 'Timed out while connecting to server' ) ;
err . source = 'timeout' ;
self . emit ( 'error' , err ) ;
socket . destroy ( ) ;
} , config . connTimeout ) ;
socket . connect ( {
port : config . port ,
host : config . host ,
localAddress : config . localAddress
} ) ;
} ;
Connection . prototype . serverSupports = function ( cap ) {
return ( this . _caps && this . _caps . indexOf ( cap ) > - 1 ) ;
} ;
Connection . prototype . destroy = function ( ) {
this . _queue = [ ] ;
this . _curReq = undefined ;
if ( this . _sock != null ) {
this . _sock . end ( ) ;
}
} ;
Connection . prototype . end = function ( ) {
return Promise . try ( ( ) => {
return this . _enqueue2 ( command ` LOGOUT ` ) ;
} ) . then ( ( ) => {
return this . destroy ( ) ;
} ) ;
} ;
Connection . prototype . append = function ( data , options , cb ) {
var literal = this . serverSupports ( 'LITERAL+' ) ;
if ( typeof options === 'function' ) {
cb = options ;
options = undefined ;
}
options = options || { } ;
if ( ! options . mailbox ) {
if ( ! this . _box )
throw new Error ( 'No mailbox specified or currently selected' ) ;
else
options . mailbox = this . _box . name ;
}
var cmd = 'APPEND "' + escape ( utf7 . encode ( '' + options . mailbox ) ) + '"' ;
if ( options . flags ) {
if ( ! Array . isArray ( options . flags ) )
options . flags = [ options . flags ] ;
if ( options . flags . length > 0 ) {
for ( var i = 0 , len = options . flags . length ; i < len ; ++ i ) {
if ( options . flags [ i ] [ 0 ] !== '$' && options . flags [ i ] [ 0 ] !== '\\' )
options . flags [ i ] = '\\' + options . flags [ i ] ;
}
cmd += ' (' + options . flags . join ( ' ' ) + ')' ;
}
}
if ( options . date ) {
if ( ! isDate ( options . date ) )
throw new Error ( '`date` is not a Date object' ) ;
cmd += ' "' ;
cmd += options . date . getDate ( ) ;
cmd += '-' ;
cmd += MONTHS [ options . date . getMonth ( ) ] ;
cmd += '-' ;
cmd += options . date . getFullYear ( ) ;
cmd += ' ' ;
cmd += ( '0' + options . date . getHours ( ) ) . slice ( - 2 ) ;
cmd += ':' ;
cmd += ( '0' + options . date . getMinutes ( ) ) . slice ( - 2 ) ;
cmd += ':' ;
cmd += ( '0' + options . date . getSeconds ( ) ) . slice ( - 2 ) ;
cmd += ( ( options . date . getTimezoneOffset ( ) > 0 ) ? ' -' : ' +' ) ;
cmd += ( '0' + ( - options . date . getTimezoneOffset ( ) / 60 ) ) . slice ( - 2 ) ;
cmd += ( '0' + ( - options . date . getTimezoneOffset ( ) % 60 ) ) . slice ( - 2 ) ;
cmd += '"' ;
}
cmd += ' {' ;
cmd += ( Buffer . isBuffer ( data ) ? data . length : Buffer . byteLength ( data ) ) ;
cmd += ( literal ? '+' : '' ) + '}' ;
this . _enqueue ( cmd , cb ) ;
if ( literal )
this . _queue [ this . _queue . length - 1 ] . literalAppendData = data ;
else
this . _queue [ this . _queue . length - 1 ] . appendData = data ;
} ;
Connection . prototype . getSpecialUseBoxes = function ( cb ) {
this . _enqueue ( 'XLIST "" "*"' , cb ) ;
} ;
Connection . prototype . getBoxes = function ( namespace , cb ) {
if ( typeof namespace === 'function' ) {
cb = namespace ;
namespace = '' ;
}
namespace = escape ( utf7 . encode ( '' + namespace ) ) ;
this . _enqueue ( 'LIST "' + namespace + '" "*"' , cb ) ;
} ;
Connection . prototype . id = function ( identification , cb ) {
if ( ! this . serverSupports ( 'ID' ) )
throw new Error ( 'Server does not support ID' ) ;
var cmd = 'ID' ;
if ( ( identification === null ) || ( Object . keys ( identification ) . length === 0 ) )
cmd += ' NIL' ;
else {
if ( Object . keys ( identification ) . length > 30 )
throw new Error ( 'Max allowed number of keys is 30' ) ;
var kv = [ ] ;
for ( var k in identification ) {
if ( Buffer . byteLength ( k ) > 30 )
throw new Error ( 'Max allowed key length is 30' ) ;
if ( Buffer . byteLength ( identification [ k ] ) > 1024 )
throw new Error ( 'Max allowed value length is 1024' ) ;
kv . push ( '"' + escape ( k ) + '"' ) ;
kv . push ( '"' + escape ( identification [ k ] ) + '"' ) ;
}
cmd += ' (' + kv . join ( ' ' ) + ')' ;
}
this . _enqueue ( cmd , cb ) ;
} ;
Connection . prototype . openBox = function ( name , readOnly , cb ) {
if ( this . state !== 'authenticated' )
throw new Error ( 'Not authenticated' ) ;
if ( typeof readOnly === 'function' ) {
cb = readOnly ;
readOnly = false ;
}
name = '' + name ;
var encname = escape ( utf7 . encode ( name ) ) ,
cmd = ( readOnly ? 'EXAMINE' : 'SELECT' ) ,
self = this ;
cmd += ' "' + encname + '"' ;
if ( this . serverSupports ( 'CONDSTORE' ) )
cmd += ' (CONDSTORE)' ;
this . _enqueue ( cmd , function ( err ) {
if ( err ) {
self . _box = undefined ;
cb ( err ) ;
} else {
self . _box . name = name ;
cb ( err , self . _box ) ;
}
} ) ;
} ;
Connection . prototype . closeBox = function ( shouldExpunge , cb ) {
if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
var self = this ;
if ( typeof shouldExpunge === 'function' ) {
cb = shouldExpunge ;
shouldExpunge = true ;
}
if ( shouldExpunge ) {
this . _enqueue ( 'CLOSE' , function ( err ) {
if ( ! err )
self . _box = undefined ;
cb ( err ) ;
} ) ;
} else {
if ( this . serverSupports ( 'UNSELECT' ) ) {
// use UNSELECT if available, as it claims to be "cleaner" than the
// alternative "hack"
this . _enqueue ( 'UNSELECT' , function ( err ) {
if ( ! err )
self . _box = undefined ;
cb ( err ) ;
} ) ;
} else {
// "HACK": close the box without expunging by attempting to SELECT a
// non-existent mailbox
var badbox = 'NODEJSIMAPCLOSINGBOX' + Date . now ( ) ;
this . _enqueue ( 'SELECT "' + badbox + '"' , function ( err ) {
self . _box = undefined ;
cb ( ) ;
} ) ;
}
}
} ;
Connection . prototype . addBox = function ( name , cb ) {
this . _enqueue ( 'CREATE "' + escape ( utf7 . encode ( '' + name ) ) + '"' , cb ) ;
} ;
Connection . prototype . delBox = function ( name , cb ) {
this . _enqueue ( 'DELETE "' + escape ( utf7 . encode ( '' + name ) ) + '"' , cb ) ;
} ;
Connection . prototype . renameBox = function ( oldname , newname , cb ) {
var encoldname = escape ( utf7 . encode ( '' + oldname ) ) ,
encnewname = escape ( utf7 . encode ( '' + newname ) ) ,
self = this ;
this . _enqueue ( 'RENAME "' + encoldname + '" "' + encnewname + '"' ,
function ( err ) {
if ( err )
return cb ( err ) ;
if ( self . _box
&& self . _box . name === oldname
&& oldname . toUpperCase ( ) !== 'INBOX' ) {
self . _box . name = newname ;
cb ( err , self . _box ) ;
} else
cb ( ) ;
}
) ;
} ;
Connection . prototype . subscribeBox = function ( name , cb ) {
this . _enqueue ( 'SUBSCRIBE "' + escape ( utf7 . encode ( '' + name ) ) + '"' , cb ) ;
} ;
Connection . prototype . unsubscribeBox = function ( name , cb ) {
this . _enqueue ( 'UNSUBSCRIBE "' + escape ( utf7 . encode ( '' + name ) ) + '"' , cb ) ;
} ;
Connection . prototype . getSubscribedBoxes = function ( namespace , cb ) {
if ( typeof namespace === 'function' ) {
cb = namespace ;
namespace = '' ;
}
namespace = escape ( utf7 . encode ( '' + namespace ) ) ;
this . _enqueue ( 'LSUB "' + namespace + '" "*"' , cb ) ;
} ;
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 ) ) ;
var info = [ 'MESSAGES' , 'RECENT' , 'UNSEEN' , 'UIDVALIDITY' , 'UIDNEXT' ] ;
if ( this . serverSupports ( 'CONDSTORE' ) )
info . push ( 'HIGHESTMODSEQ' ) ;
info = info . join ( ' ' ) ;
this . _enqueue ( 'STATUS "' + boxName + '" (' + info + ')' , cb ) ;
} ;
Connection . prototype . expunge = function ( uids , cb ) {
if ( typeof uids === 'function' ) {
cb = uids ;
uids = undefined ;
}
if ( uids !== undefined ) {
if ( ! Array . isArray ( uids ) )
uids = [ uids ] ;
validateUIDList ( uids ) ;
if ( uids . length === 0 )
throw new Error ( 'Empty uid list' ) ;
uids = uids . join ( ',' ) ;
if ( ! this . serverSupports ( 'UIDPLUS' ) )
throw new Error ( 'Server does not support this feature (UIDPLUS)' ) ;
this . _enqueue ( 'UID EXPUNGE ' + uids , cb ) ;
} else
this . _enqueue ( 'EXPUNGE' , cb ) ;
} ;
Connection . prototype . search = function ( criteria , cb ) {
this . _search ( 'UID ' , criteria , cb ) ;
} ;
Connection . prototype . _search = function ( which , criteria , cb ) {
if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
else if ( ! Array . isArray ( criteria ) )
throw new Error ( 'Expected array for search criteria' ) ;
var cmd = which + 'SEARCH' ,
info = { hasUTF8 : false /*output*/ } ,
query = buildSearchQuery ( criteria , this . _caps , info ) ,
lines ;
if ( info . hasUTF8 ) {
cmd += ' CHARSET UTF-8' ;
lines = query . split ( CRLF ) ;
query = lines . shift ( ) ;
}
cmd += query ;
this . _enqueue ( cmd , cb ) ;
if ( info . hasUTF8 ) {
var req = this . _queue [ this . _queue . length - 1 ] ;
req . lines = lines ;
}
} ;
Connection . prototype . addFlags = function ( uids , flags , cb ) {
this . _store ( 'UID ' , uids , { mode : '+' , flags : flags } , cb ) ;
} ;
Connection . prototype . delFlags = function ( uids , flags , cb ) {
this . _store ( 'UID ' , uids , { mode : '-' , flags : flags } , cb ) ;
} ;
Connection . prototype . setFlags = function ( uids , flags , cb ) {
this . _store ( 'UID ' , uids , { mode : '' , flags : flags } , cb ) ;
} ;
Connection . prototype . addKeywords = function ( uids , keywords , cb ) {
this . _store ( 'UID ' , uids , { mode : '+' , keywords : keywords } , cb ) ;
} ;
Connection . prototype . delKeywords = function ( uids , keywords , cb ) {
this . _store ( 'UID ' , uids , { mode : '-' , keywords : keywords } , cb ) ;
} ;
Connection . prototype . setKeywords = function ( uids , keywords , cb ) {
this . _store ( 'UID ' , uids , { mode : '' , keywords : keywords } , cb ) ;
} ;
Connection . prototype . _store = function ( which , uids , cfg , cb ) {
var mode = cfg . mode ,
isFlags = ( cfg . flags !== undefined ) ,
items = ( isFlags ? cfg . flags : cfg . keywords ) ;
if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
else if ( uids === undefined )
throw new Error ( 'No messages specified' ) ;
if ( ! Array . isArray ( uids ) )
uids = [ uids ] ;
validateUIDList ( uids ) ;
if ( uids . length === 0 ) {
throw new Error ( 'Empty '
+ ( which === '' ? 'sequence number' : 'uid' )
+ 'list' ) ;
}
if ( ( ! Array . isArray ( items ) && typeof items !== 'string' )
|| ( Array . isArray ( items ) && items . length === 0 ) )
throw new Error ( ( isFlags ? 'Flags' : 'Keywords' )
+ ' argument must be a string or a non-empty Array' ) ;
if ( ! Array . isArray ( items ) )
items = [ items ] ;
for ( var i = 0 , len = items . length ; i < len ; ++ i ) {
if ( isFlags ) {
if ( items [ i ] [ 0 ] !== '\\' )
items [ i ] = '\\' + items [ i ] ;
} else {
// keyword contains any char except control characters (%x00-1F and %x7F)
// and: '(', ')', '{', ' ', '%', '*', '\', '"', ']'
if ( RE _INVALID _KW _CHARS . test ( items [ i ] ) ) {
throw new Error ( 'The keyword "' + items [ i ]
+ '" contains invalid characters' ) ;
}
}
}
items = items . join ( ' ' ) ;
uids = uids . join ( ',' ) ;
var modifiers = '' ;
if ( cfg . modseq !== undefined && ! this . _box . nomodseq )
modifiers += 'UNCHANGEDSINCE ' + cfg . modseq + ' ' ;
this . _enqueue ( which + 'STORE ' + uids + ' '
+ modifiers
+ mode + 'FLAGS.SILENT (' + items + ')' , cb ) ;
} ;
Connection . prototype . copy = function ( uids , boxTo , cb ) {
this . _copy ( 'UID ' , uids , boxTo , cb ) ;
} ;
Connection . prototype . _copy = function ( which , uids , boxTo , cb ) {
if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
if ( ! Array . isArray ( uids ) )
uids = [ uids ] ;
validateUIDList ( uids ) ;
if ( uids . length === 0 ) {
throw new Error ( 'Empty '
+ ( which === '' ? 'sequence number' : 'uid' )
+ 'list' ) ;
}
boxTo = escape ( utf7 . encode ( '' + boxTo ) ) ;
this . _enqueue ( which + 'COPY ' + uids . join ( ',' ) + ' "' + boxTo + '"' , cb ) ;
} ;
Connection . prototype . move = function ( uids , boxTo , cb ) {
this . _move ( 'UID ' , uids , boxTo , cb ) ;
} ;
Connection . prototype . _move = function ( which , uids , boxTo , cb ) {
if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
if ( this . serverSupports ( 'MOVE' ) ) {
if ( ! Array . isArray ( uids ) )
uids = [ uids ] ;
validateUIDList ( uids ) ;
if ( uids . length === 0 ) {
throw new Error ( 'Empty '
+ ( which === '' ? 'sequence number' : 'uid' )
+ 'list' ) ;
}
uids = uids . join ( ',' ) ;
boxTo = escape ( utf7 . encode ( '' + boxTo ) ) ;
this . _enqueue ( which + 'MOVE ' + uids + ' "' + boxTo + '"' , cb ) ;
} else if ( this . _box . permFlags . indexOf ( '\\Deleted' ) === - 1
&& this . _box . flags . indexOf ( '\\Deleted' ) === - 1 ) {
throw new Error ( 'Cannot move message: '
+ 'server does not allow deletion of messages' ) ;
} else {
var deletedUIDs , task = 0 , self = this ;
this . _copy ( which , uids , boxTo , function ccb ( err , info ) {
if ( err )
return cb ( err , info ) ;
if ( task === 0 && which && self . serverSupports ( 'UIDPLUS' ) ) {
// UIDPLUS gives us a 'UID EXPUNGE n' command to expunge a subset of
// messages with the \Deleted flag set. This allows us to skip some
// actions.
task = 2 ;
}
// Make sure we don't expunge any messages marked as Deleted except the
// one we are moving
if ( task === 0 ) {
self . search ( [ 'DELETED' ] , function ( e , result ) {
++ task ;
deletedUIDs = result ;
ccb ( e , info ) ;
} ) ;
} else if ( task === 1 ) {
if ( deletedUIDs . length ) {
self . delFlags ( deletedUIDs , '\\Deleted' , function ( e ) {
++ task ;
ccb ( e , info ) ;
} ) ;
} else {
++ task ;
ccb ( err , info ) ;
}
} else if ( task === 2 ) {
var cbMarkDel = function ( e ) {
++ task ;
ccb ( e , info ) ;
} ;
if ( which )
self . addFlags ( uids , '\\Deleted' , cbMarkDel ) ;
else
self . seq . addFlags ( uids , '\\Deleted' , cbMarkDel ) ;
} else if ( task === 3 ) {
if ( which && self . serverSupports ( 'UIDPLUS' ) ) {
self . expunge ( uids , function ( e ) {
cb ( e , info ) ;
} ) ;
} else {
self . expunge ( function ( e ) {
++ task ;
ccb ( e , info ) ;
} ) ;
}
} else if ( task === 4 ) {
if ( deletedUIDs . length ) {
self . addFlags ( deletedUIDs , '\\Deleted' , function ( e ) {
cb ( e , info ) ;
} ) ;
} else
cb ( err , info ) ;
}
} ) ;
}
} ;
Connection . prototype . fetch = function ( uids , options ) {
return this . _fetch ( 'UID ' , uids , options ) ;
} ;
Connection . prototype . _fetch = function ( which , uids , options ) {
if ( uids === undefined
|| uids === null
|| ( Array . isArray ( uids ) && uids . length === 0 ) )
throw new Error ( 'Nothing to fetch' ) ;
if ( ! Array . isArray ( uids ) )
uids = [ uids ] ;
validateUIDList ( uids ) ;
if ( uids . length === 0 ) {
throw new Error ( 'Empty '
+ ( which === '' ? 'sequence number' : 'uid' )
+ 'list' ) ;
}
uids = uids . join ( ',' ) ;
var cmd = which + 'FETCH ' + uids + ' (' ,
fetching = [ ] ,
i , len , key ;
if ( this . serverSupports ( 'X-GM-EXT-1' ) ) {
fetching . push ( 'X-GM-THRID' ) ;
fetching . push ( 'X-GM-MSGID' ) ;
fetching . push ( 'X-GM-LABELS' ) ;
}
if ( this . serverSupports ( 'CONDSTORE' ) && ! this . _box . nomodseq )
fetching . push ( 'MODSEQ' ) ;
fetching . push ( 'UID' ) ;
fetching . push ( 'FLAGS' ) ;
fetching . push ( 'INTERNALDATE' ) ;
var modifiers ;
if ( options ) {
modifiers = options . modifiers ;
if ( options . envelope )
fetching . push ( 'ENVELOPE' ) ;
if ( options . struct )
fetching . push ( 'BODYSTRUCTURE' ) ;
if ( options . size )
fetching . push ( 'RFC822.SIZE' ) ;
if ( Array . isArray ( options . extensions ) ) {
options . extensions . forEach ( function ( extension ) {
fetching . push ( extension . toUpperCase ( ) ) ;
} ) ;
}
cmd += fetching . join ( ' ' ) ;
if ( options . bodies !== undefined ) {
var bodies = options . bodies ,
prefix = ( options . markSeen ? '' : '.PEEK' ) ;
if ( ! Array . isArray ( bodies ) )
bodies = [ bodies ] ;
for ( i = 0 , len = bodies . length ; i < len ; ++ i ) {
fetching . push ( parseExpr ( '' + bodies [ i ] ) ) ;
cmd += ' BODY' + prefix + '[' + bodies [ i ] + ']' ;
}
}
} else
cmd += fetching . join ( ' ' ) ;
cmd += ')' ;
var modkeys = ( typeof modifiers === 'object' ? Object . keys ( modifiers ) : [ ] ) ,
modstr = ' (' ;
for ( i = 0 , len = modkeys . length , key ; i < len ; ++ i ) {
key = modkeys [ i ] . toUpperCase ( ) ;
if ( key === 'CHANGEDSINCE' && this . serverSupports ( 'CONDSTORE' )
&& ! this . _box . nomodseq )
modstr += key + ' ' + modifiers [ modkeys [ i ] ] + ' ' ;
}
if ( modstr . length > 2 ) {
cmd += modstr . substring ( 0 , modstr . length - 1 ) ;
cmd += ')' ;
}
this . _enqueue ( cmd ) ;
var req = this . _queue [ this . _queue . length - 1 ] ;
req . fetchCache = createFetchTaskTracker ( ) ;
req . fetching = fetching ;
return ( req . bodyEmitter = new EventEmitter ( ) ) ;
} ;
// Extension methods ===========================================================
Connection . prototype . setLabels = function ( uids , labels , cb ) {
this . _storeLabels ( 'UID ' , uids , labels , '' , cb ) ;
} ;
Connection . prototype . addLabels = function ( uids , labels , cb ) {
this . _storeLabels ( 'UID ' , uids , labels , '+' , cb ) ;
} ;
Connection . prototype . delLabels = function ( uids , labels , cb ) {
this . _storeLabels ( 'UID ' , uids , labels , '-' , cb ) ;
} ;
Connection . prototype . _storeLabels = function ( which , uids , labels , mode , cb ) {
if ( ! this . serverSupports ( 'X-GM-EXT-1' ) )
throw new Error ( 'Server must support X-GM-EXT-1 capability' ) ;
else if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
else if ( uids === undefined )
throw new Error ( 'No messages specified' ) ;
if ( ! Array . isArray ( uids ) )
uids = [ uids ] ;
validateUIDList ( uids ) ;
if ( uids . length === 0 ) {
throw new Error ( 'Empty '
+ ( which === '' ? 'sequence number' : 'uid' )
+ 'list' ) ;
}
if ( ( ! Array . isArray ( labels ) && typeof labels !== 'string' )
|| ( Array . isArray ( labels ) && labels . length === 0 ) )
throw new Error ( 'labels argument must be a string or a non-empty Array' ) ;
if ( ! Array . isArray ( labels ) )
labels = [ labels ] ;
labels = labels . map ( function ( v ) {
return '"' + escape ( utf7 . encode ( '' + v ) ) + '"' ;
} ) . join ( ' ' ) ;
uids = uids . join ( ',' ) ;
this . _enqueue ( which + 'STORE ' + uids + ' ' + mode
+ 'X-GM-LABELS.SILENT (' + labels + ')' , cb ) ;
} ;
Connection . prototype . sort = function ( sorts , criteria , cb ) {
this . _sort ( 'UID ' , sorts , criteria , cb ) ;
} ;
Connection . prototype . _sort = function ( which , sorts , criteria , cb ) {
if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
else if ( ! Array . isArray ( sorts ) || ! sorts . length )
throw new Error ( 'Expected array with at least one sort criteria' ) ;
else if ( ! Array . isArray ( criteria ) )
throw new Error ( 'Expected array for search criteria' ) ;
else if ( ! this . serverSupports ( 'SORT' ) )
throw new Error ( 'Sort is not supported on the server' ) ;
sorts = sorts . map ( function ( c ) {
if ( typeof c !== 'string' )
throw new Error ( 'Unexpected sort criteria data type. '
+ 'Expected string. Got: ' + typeof criteria ) ;
var modifier = '' ;
if ( c [ 0 ] === '-' ) {
modifier = 'REVERSE ' ;
c = c . substring ( 1 ) ;
}
switch ( c . toUpperCase ( ) ) {
case 'ARRIVAL' :
case 'CC' :
case 'DATE' :
case 'FROM' :
case 'SIZE' :
case 'SUBJECT' :
case 'TO' :
break ;
default :
throw new Error ( 'Unexpected sort criteria: ' + c ) ;
}
return modifier + c ;
} ) ;
sorts = sorts . join ( ' ' ) ;
var info = { hasUTF8 : false /*output*/ } ,
query = buildSearchQuery ( criteria , this . _caps , info ) ,
charset = 'US-ASCII' ,
lines ;
if ( info . hasUTF8 ) {
charset = 'UTF-8' ;
lines = query . split ( CRLF ) ;
query = lines . shift ( ) ;
}
this . _enqueue ( which + 'SORT (' + sorts + ') ' + charset + query , cb ) ;
if ( info . hasUTF8 ) {
var req = this . _queue [ this . _queue . length - 1 ] ;
req . lines = lines ;
}
} ;
Connection . prototype . esearch = function ( criteria , options , cb ) {
this . _esearch ( 'UID ' , criteria , options , cb ) ;
} ;
Connection . prototype . _esearch = function ( which , criteria , options , cb ) {
if ( this . _box === undefined )
throw new Error ( 'No mailbox is currently selected' ) ;
else if ( ! Array . isArray ( criteria ) )
throw new Error ( 'Expected array for search options' ) ;
var info = { hasUTF8 : false /*output*/ } ,
query = buildSearchQuery ( criteria , this . _caps , info ) ,
charset = '' ,
lines ;
if ( info . hasUTF8 ) {
charset = ' CHARSET UTF-8' ;
lines = query . split ( CRLF ) ;
query = lines . shift ( ) ;
}
if ( typeof options === 'function' ) {
cb = options ;
options = '' ;
} else if ( ! options )
options = '' ;
if ( Array . isArray ( options ) )
options = options . join ( ' ' ) ;
this . _enqueue ( which + 'SEARCH RETURN (' + options + ')' + charset + query , cb ) ;
if ( info . hasUTF8 ) {
var req = this . _queue [ this . _queue . length - 1 ] ;
req . lines = lines ;
}
} ;
Connection . prototype . setQuota = function ( quotaRoot , limits , cb ) {
if ( typeof limits === 'function' ) {
cb = limits ;
limits = { } ;
}
var triplets = '' ;
for ( var l in limits ) {
if ( triplets )
triplets += ' ' ;
triplets += l + ' ' + limits [ l ] ;
}
quotaRoot = escape ( utf7 . encode ( '' + quotaRoot ) ) ;
this . _enqueue ( 'SETQUOTA "' + quotaRoot + '" (' + triplets + ')' ,
function ( err , quotalist ) {
if ( err )
return cb ( err ) ;
cb ( err , quotalist ? quotalist [ 0 ] : limits ) ;
}
) ;
} ;
Connection . prototype . getQuota = function ( quotaRoot , cb ) {
quotaRoot = escape ( utf7 . encode ( '' + quotaRoot ) ) ;
this . _enqueue ( 'GETQUOTA "' + quotaRoot + '"' , function ( err , quotalist ) {
if ( err )
return cb ( err ) ;
cb ( err , quotalist [ 0 ] ) ;
} ) ;
} ;
Connection . prototype . getQuotaRoot = function ( boxName , cb ) {
boxName = escape ( utf7 . encode ( '' + boxName ) ) ;
this . _enqueue ( 'GETQUOTAROOT "' + boxName + '"' , function ( err , quotalist ) {
if ( err )
return cb ( err ) ;
var quotas = { } ;
if ( quotalist ) {
for ( var i = 0 , len = quotalist . length ; i < len ; ++ i )
quotas [ quotalist [ i ] . root ] = quotalist [ i ] . resources ;
}
cb ( err , quotas ) ;
} ) ;
} ;
Connection . prototype . thread = function ( algorithm , criteria , cb ) {
this . _thread ( 'UID ' , algorithm , criteria , cb ) ;
} ;
Connection . prototype . _thread = function ( which , algorithm , criteria , cb ) {
algorithm = algorithm . toUpperCase ( ) ;
if ( ! this . serverSupports ( 'THREAD=' + algorithm ) )
throw new Error ( 'Server does not support that threading algorithm' ) ;
var info = { hasUTF8 : false /*output*/ } ,
query = buildSearchQuery ( criteria , this . _caps , info ) ,
charset = 'US-ASCII' ,
lines ;
if ( info . hasUTF8 ) {
charset = 'UTF-8' ;
lines = query . split ( CRLF ) ;
query = lines . shift ( ) ;
}
this . _enqueue ( which + 'THREAD ' + algorithm + ' ' + charset + query , cb ) ;
if ( info . hasUTF8 ) {
var req = this . _queue [ this . _queue . length - 1 ] ;
req . lines = lines ;
}
} ;
Connection . prototype . addFlagsSince = function ( uids , flags , modseq , cb ) {
this . _store ( 'UID ' ,
uids ,
{ mode : '+' , flags : flags , modseq : modseq } ,
cb ) ;
} ;
Connection . prototype . delFlagsSince = function ( uids , flags , modseq , cb ) {
this . _store ( 'UID ' ,
uids ,
{ mode : '-' , flags : flags , modseq : modseq } ,
cb ) ;
} ;
Connection . prototype . setFlagsSince = function ( uids , flags , modseq , cb ) {
this . _store ( 'UID ' ,
uids ,
{ mode : '' , flags : flags , modseq : modseq } ,
cb ) ;
} ;
Connection . prototype . addKeywordsSince = function ( uids , keywords , modseq , cb ) {
this . _store ( 'UID ' ,
uids ,
{ mode : '+' , keywords : keywords , modseq : modseq } ,
cb ) ;
} ;
Connection . prototype . delKeywordsSince = function ( uids , keywords , modseq , cb ) {
this . _store ( 'UID ' ,
uids ,
{ mode : '-' , keywords : keywords , modseq : modseq } ,
cb ) ;
} ;
Connection . prototype . setKeywordsSince = function ( uids , keywords , modseq , cb ) {
this . _store ( 'UID ' ,
uids ,
{ mode : '' , keywords : keywords , modseq : modseq } ,
cb ) ;
} ;
// END Extension methods =======================================================
// Namespace for seqno-based commands
Object . defineProperty ( Connection . prototype , 'seq' , { get : function ( ) {
var self = this ;
return {
delKeywords : function ( seqnos , keywords , cb ) {
self . _store ( '' , seqnos , { mode : '-' , keywords : keywords } , cb ) ;
} ,
addKeywords : function ( seqnos , keywords , cb ) {
self . _store ( '' , seqnos , { mode : '+' , keywords : keywords } , cb ) ;
} ,
setKeywords : function ( seqnos , keywords , cb ) {
self . _store ( '' , seqnos , { mode : '' , keywords : keywords } , cb ) ;
} ,
delFlags : function ( seqnos , flags , cb ) {
self . _store ( '' , seqnos , { mode : '-' , flags : flags } , cb ) ;
} ,
addFlags : function ( seqnos , flags , cb ) {
self . _store ( '' , seqnos , { mode : '+' , flags : flags } , cb ) ;
} ,
setFlags : function ( seqnos , flags , cb ) {
self . _store ( '' , seqnos , { mode : '' , flags : flags } , cb ) ;
} ,
move : function ( seqnos , boxTo , cb ) {
self . _move ( '' , seqnos , boxTo , cb ) ;
} ,
copy : function ( seqnos , boxTo , cb ) {
self . _copy ( '' , seqnos , boxTo , cb ) ;
} ,
fetch : function ( seqnos , options ) {
return self . _fetch ( '' , seqnos , options ) ;
} ,
search : function ( options , cb ) {
self . _search ( '' , options , cb ) ;
} ,
// Extensions ==============================================================
delLabels : function ( seqnos , labels , cb ) {
self . _storeLabels ( '' , seqnos , labels , '-' , cb ) ;
} ,
addLabels : function ( seqnos , labels , cb ) {
self . _storeLabels ( '' , seqnos , labels , '+' , cb ) ;
} ,
setLabels : function ( seqnos , labels , cb ) {
self . _storeLabels ( '' , seqnos , labels , '' , cb ) ;
} ,
esearch : function ( criteria , options , cb ) {
self . _esearch ( '' , criteria , options , cb ) ;
} ,
sort : function ( sorts , options , cb ) {
self . _sort ( '' , sorts , options , cb ) ;
} ,
thread : function ( algorithm , criteria , cb ) {
self . _thread ( '' , algorithm , criteria , cb ) ;
} ,
delKeywordsSince : function ( seqnos , keywords , modseq , cb ) {
self . _store ( '' ,
seqnos ,
{ mode : '-' , keywords : keywords , modseq : modseq } ,
cb ) ;
} ,
addKeywordsSince : function ( seqnos , keywords , modseq , cb ) {
self . _store ( '' ,
seqnos ,
{ mode : '+' , keywords : keywords , modseq : modseq } ,
cb ) ;
} ,
setKeywordsSince : function ( seqnos , keywords , modseq , cb ) {
self . _store ( '' ,
seqnos ,
{ mode : '' , keywords : keywords , modseq : modseq } ,
cb ) ;
} ,
delFlagsSince : function ( seqnos , flags , modseq , cb ) {
self . _store ( '' ,
seqnos ,
{ mode : '-' , flags : flags , modseq : modseq } ,
cb ) ;
} ,
addFlagsSince : function ( seqnos , flags , modseq , cb ) {
self . _store ( '' ,
seqnos ,
{ mode : '+' , flags : flags , modseq : modseq } ,
cb ) ;
} ,
setFlagsSince : function ( seqnos , flags , modseq , cb ) {
self . _store ( '' ,
seqnos ,
{ mode : '' , flags : flags , modseq : modseq } ,
cb ) ;
}
} ;
} } ) ;
function parseTypes ( typeString ) {
if ( typeString === NoRequest || typeString === AnyRequest ) {
return [ typeString ] ;
} else {
return typeString
. split ( /\s*,\s*/ )
. map ( ( type ) => type . toUpperCase ( ) ) ;
}
}
function createCommandHandlers ( _rules ) {
let [ rules ] = validateArguments ( arguments , {
rules : anyProperty ( {
key : [ required ] ,
value : [ required , {
untagged : anyProperty ( {
key : [ isString ] ,
value : [ required , isFunction ]
} ) ,
tagged : [ isFunction ]
} ]
} )
} ) ;
let untaggedHandlers = new Map ( ) ;
let taggedHandlers = new Map ( ) ;
function setUntaggedHandler ( requestType , responseType , handler ) {
if ( ! untaggedHandlers . has ( requestType ) ) {
untaggedHandlers . set ( requestType , new Map ( ) ) ;
}
let handlers = untaggedHandlers . get ( requestType ) ;
if ( ! handlers . has ( responseType ) ) {
handlers . set ( responseType , handler ) ;
} else {
throw new Error ( ` Duplicate handler definition for untagged type ' ${ requestType } ' -> ' ${ responseType } ' ` ) ;
}
}
function setTaggedHandler ( requestType , handler ) {
if ( ! taggedHandlers . has ( requestType ) ) {
taggedHandlers . set ( requestType , handler ) ;
} else {
throw new Error ( ` Duplicate handler definition for tagged type ` ) ;
}
}
function getRequestType ( request ) {
return ( request != null )
? request . type . toUpperCase ( )
: NoRequest ;
}
function getTaggedHandler ( request ) {
return taggedHandlers . get ( getRequestType ( request ) ) ;
}
function getUntaggedHandler ( request , responseType ) {
let typeHandlers = untaggedHandlers . get ( getRequestType ( request ) ) ;
let anyRequestHandlers = untaggedHandlers . get ( AnyRequest ) ;
// console.log({
// requestType: getRequestType(request),
// responseType: responseType,
// anyHandler: anyRequestHandlers.get(responseType)
// });
if ( typeHandlers != null && typeHandlers . has ( responseType ) ) {
return typeHandlers . get ( responseType ) ;
} else if ( anyRequestHandlers != null && anyRequestHandlers . has ( responseType ) ) {
return anyRequestHandlers . get ( responseType ) ;
}
}
for ( let [ typeString , options ] of allEntries ( rules ) ) {
for ( let type of parseTypes ( typeString ) ) {
if ( options . untagged != null ) {
for ( let [ responseTypeString , handler ] of allEntries ( options . untagged ) ) {
for ( let responseType of parseTypes ( responseTypeString ) ) {
setUntaggedHandler ( type , responseType , handler ) ;
}
}
}
if ( options . tagged != null ) {
setTaggedHandler ( type , options . tagged ) ;
}
}
}
// REFACTOR: Eventually remove `.call(this` hackery
// REFACTOR: Remove all the toUpperCase stuff and normalize this in the core instead, so that we can just *assume* it to be upper-case here
return {
canHandleUntagged : function ( request , data ) {
return ( getUntaggedHandler ( request , data . type . toUpperCase ( ) ) != null ) ;
} ,
canHandleTagged : function ( request , _data ) {
return ( getTaggedHandler ( request ) != null ) ;
} ,
handleUntagged : function ( request , data ) {
let handler = getUntaggedHandler ( request , data . type . toUpperCase ( ) ) ;
return handler . call ( this , request , data ) ;
} ,
handleTagged : function ( request , data ) {
let handler = getTaggedHandler ( request ) ;
return handler . call ( this , request , data ) ;
}
} ;
}
function allEntries ( object ) {
// The equivalent of Object.entries but also including Symbol keys
return Reflect . ownKeys ( object ) . map ( ( key ) => {
return [ key , object [ key ] ] ;
} ) ;
}
let NoRequest = Symbol ( "NoRequest" ) ;
let AnyRequest = Symbol ( "AnyRequest" ) ;
// FIXME: Strip "UID" prefix from command names before matching, as these only affect the output format, but not the type of response
// Exception: EXPUNGE is allowed during UID commands, but not during their non-UID equivalents: https://datatracker.ietf.org/doc/html/rfc3501#page-73
let commandHandlers = createCommandHandlers ( {
[ AnyRequest ] : {
untagged : {
"BYE" : function ( _request , _ ) {
this . _sock . end ( ) ;
} ,
"CAPABILITY" : function ( _request , { payload } ) {
this . _caps = payload . map ( ( v ) => v . toUpperCase ( ) ) ;
} ,
"NAMESPACE" : function ( _request , { payload } ) {
this . namespaces = payload ;
} ,
"PREAUTH" : function ( _request , _ ) {
this . state = 'authenticated' ;
} ,
"EXPUNGE" : function ( _request , { sequenceNumber } ) {
if ( this . _box != null ) {
if ( this . _box . messages . total > 0 ) {
this . _box . messages . total -= 1 ;
}
this . emit ( 'expunge' , sequenceNumber ) ;
}
} ,
}
} ,
"STATUS" : {
untagged : {
"STATUS" : function ( request , { payload } ) {
// REFACTOR: Improve this?
let attrs = defaultValue ( payload . attrs , { } ) ;
let box = {
name : payload . name ,
uidnext : defaultValue ( attrs . uidnext , 0 ) ,
uidvalidity : defaultValue ( attrs . uidvalidity , 0 ) ,
messages : {
total : defaultValue ( attrs . messages , 0 ) ,
new : defaultValue ( attrs . recent , 0 ) ,
unseen : defaultValue ( attrs . unseen , 0 )
} ,
// CONDSTORE
highestmodseq : ( attrs . highestmodseq != null )
? String ( attrs . highestmodseq )
: undefined
} ;
request . legacyArgs . push ( box ) ;
Object . assign ( request . responseData , box ) ;
}
}
} ,
"LIST, XLIST, LSUB" : {
untagged : {
"LIST, XLIST, LSUB" : function ( request , { payload } ) {
if ( request . delimiter === undefined ) {
request . delimiter = payload . delimiter ;
} else {
if ( request . boxBuilder == null ) {
request . boxBuilder = createBoxTreeBuilder ( ) ;
}
request . boxBuilder . add ( payload ) ;
}
}
} ,
tagged : function ( request , _ ) {
// FIXME: Check request types for correctness
let boxTree = ( request . boxBuilder != null )
? request . boxBuilder . done ( )
: { } ; // No response items were received
request . legacyArgs . push ( boxTree ) ;
request . responseData . boxTree = boxTree ;
}
} ,
"ID" : {
// https://datatracker.ietf.org/doc/html/rfc2971
// Used for communicating server/client name, version, etc.
untagged : {
"ID" : function ( request , { payload } ) {
request . responseData . serverVersion = payload ;
request . legacyArgs . push ( payload ) ;
}
}
} ,
"ESEARCH" : {
// https://datatracker.ietf.org/doc/html/rfc4731 / https://datatracker.ietf.org/doc/html/rfc7377
untagged : {
"ESEARCH" : function ( request , { payload } ) {
Object . assign ( request . responseData , payload ) ; // Protocol-defined attributes. TODO: Improve the key names for this? Or is there extensibility?
request . legacyArgs . push ( payload ) ;
}
}
} ,
"SORT" : {
// https://datatracker.ietf.org/doc/html/rfc5256
untagged : {
"SORT" : function ( request , { payload } ) {
request . responseData . UIDs = payload ;
request . legacyArgs . push ( payload ) ;
}
}
} ,
"THREAD" : {
// https://datatracker.ietf.org/doc/html/rfc5256
untagged : {
"THREAD" : function ( request , { payload } ) {
request . responseData . threads = payload ; // FIXME: Work out the exact format
request . legacyArgs . push ( payload ) ;
}
}
} ,
"SEARCH" : {
untagged : {
"SEARCH" : function ( request , { payload } ) {
if ( payload . results !== undefined ) {
// CONDSTORE-modified search results
request . legacyArgs . push ( payload . results ) ;
request . legacyArgs . push ( payload . modseq ) ;
} else {
request . legacyArgs . push ( payload ) ;
}
}
}
} ,
"GETQUOTA, GETQUOTAROOT" : {
// https://datatracker.ietf.org/doc/html/rfc2087
untagged : {
"QUOTA" : function ( request , { payload } ) {
ensureArray ( request . responseData , "quota" ) ;
request . responseData . quota . push ( payload ) ;
ensureArray ( request . legacyArgs , 0 ) ;
request . legacyArgs [ 0 ] . push ( payload ) ;
} ,
"QUOTAROOT" : function ( _request , _ ) {
throw new Error ( ` Not implemented ` ) ;
}
}
} ,
"SELECT, EXAMINE" : {
untagged : {
"RECENT" : function ( _request , { sequenceNumber } ) {
this . _ensureBox ( ) ;
// FIXME: This conditional is always true?
if ( this . _box ) {
this . _box . messages . new = sequenceNumber ;
}
} ,
"FLAGS" : function ( _request , { payload } ) {
this . _ensureBox ( ) ;
// FIXME: This conditional is always true?
if ( this . _box ) {
this . _box . flags = payload ;
}
} ,
"EXISTS" : function ( _request , { sequenceNumber } ) {
this . _ensureBox ( ) ;
// FIXME: This conditional is always true?
if ( this . _box ) {
var prev = this . _box . messages . total , now = sequenceNumber ;
this . _box . messages . total = now ;
if ( now > prev && this . state === 'authenticated' ) {
this . _box . messages . new = now - prev ;
this . emit ( 'mail' , this . _box . messages . new ) ;
}
}
} ,
}
} ,
} ) ;
Connection . prototype . _ensureBox = function ( ) {
if ( ! this . _box ) {
if ( RE _OPENBOX . test ( this . _curReq . type ) ) {
this . _resetCurrentBox ( ) ;
} else {
throw new Error ( ` Received a box-related response while not processing a box-related request ` ) ;
}
}
} ;
// type: type,
// num: num, -- sequence number of the affected nessage, used for FETCH and EXPUNGE only (message-data) and maybe RECENT and EXISTS (mailbox-data)?
// textCode: textCode,
// text: val
// NOTE: responseData is meant to contain machine-readable data, payload is meant to contain human-readable data, but in practice payload is also often machine-parsed
Connection . prototype . _resUntagged = function ( { type , num : sequenceNumber , textCode : responseData , text : payload } ) {
// console.log("resUntagged", { type, num: sequenceNumber, payload, textCode: responseData });
var i , len , box , destinationKey ;
let response = { type , sequenceNumber , payload } ;
if ( commandHandlers . canHandleUntagged ( this . _curReq , response ) ) {
// FIXME: Include other fields
commandHandlers . handleUntagged . call ( this , this . _curReq , response ) ;
} else if ( type === 'ok' ) {
if ( this . state === 'connected' && ! this . _curReq ) {
this . _login ( ) ;
} else if ( typeof responseData === 'string' && responseData . toUpperCase ( ) === 'ALERT' ) {
this . emit ( 'alert' , payload ) ;
} else if ( this . _curReq && responseData && ( RE _OPENBOX . test ( this . _curReq . type ) ) ) {
// we're opening a mailbox
if ( ! this . _box ) {
this . _resetCurrentBox ( ) ;
}
let destinationKey = ( responseData . key != null )
? responseData . key . toUpperCase ( )
: responseData ;
if ( destinationKey === 'UIDVALIDITY' ) {
this . _box . uidvalidity = responseData . val ;
} else if ( destinationKey === 'UIDNEXT' ) {
this . _box . uidnext = responseData . val ;
} else if ( destinationKey === 'HIGHESTMODSEQ' ) {
this . _box . highestmodseq = '' + responseData . val ;
} else if ( destinationKey === 'PERMANENTFLAGS' ) {
var idx , permFlags , keywords ;
this . _box . permFlags = permFlags = responseData . val ;
if ( ( idx = this . _box . permFlags . indexOf ( '\\*' ) ) > - 1 ) {
this . _box . newKeywords = true ;
permFlags . splice ( idx , 1 ) ;
}
this . _box . keywords = keywords = permFlags . filter ( ( f ) => f [ 0 ] !== '\\' ) ;
for ( i = 0 , len = keywords . length ; i < len ; ++ i ) {
permFlags . splice ( permFlags . indexOf ( keywords [ i ] ) , 1 ) ;
}
} else if ( destinationKey === 'UIDNOTSTICKY' ) {
this . _box . persistentUIDs = false ;
} else if ( destinationKey === 'NOMODSEQ' ) {
this . _box . nomodseq = true ;
}
} else if ( typeof responseData === 'string' && responseData . toUpperCase ( ) === 'UIDVALIDITY' ) {
this . emit ( 'uidvalidity' , payload ) ;
}
} else if ( type === 'bad' || type === 'no' ) {
if ( this . state === 'connected' && ! this . _curReq ) {
clearTimeout ( this . _connectionTimeout ) ;
clearTimeout ( this . _authenticationTimeout ) ;
var err = new Error ( 'Received negative welcome: ' + payload ) ;
err . source = 'protocol' ;
this . emit ( 'error' , err ) ;
this . _sock . end ( ) ;
}
} else if ( type === 'fetch' ) {
if ( /^(?:UID )?FETCH/ . test ( this . _curReq . fullcmd ) ) {
// FETCH response sent as result of FETCH request
let task = this . _curReq . fetchCache . get ( sequenceNumber ) ;
// FIXME: Refactor, probably make the task itself an event emitter
if ( task == null ) {
task = this . _curReq . fetchCache . create ( sequenceNumber , this . _curReq . fetching . slice ( ) ) ;
this . _curReq . bodyEmitter . emit ( 'message' , task . emitter , sequenceNumber ) ;
}
task . processFetchResponse ( payload ) ;
} else {
// FETCH response sent as result of STORE request or sent unilaterally,
// treat them as the same for now for simplicity
this . emit ( 'update' , sequenceNumber , payload ) ;
}
}
} ;
Connection . prototype . _resTagged = function ( { type , tagnum , text : payload , textCode : responseCode } ) {
// console.log("resTagged", { type, tagnum, payload, textCode: responseCode });
// REFACTOR: textCode: either just the key, or a {key, val} object
var request = this . _curReq ;
if ( request != null ) {
var err ;
this . _curReq = undefined ;
if ( type === 'no' || type === 'bad' ) {
// TODO: Can text be an empty string?
let errorText = defaultValue ( payload , request . oauthError ) ;
err = Object . assign ( new Error ( errorText ) , {
type : type ,
text : responseCode ,
source : "protocol"
} ) ;
} else if ( this . _box != null ) {
if ( request . type === 'EXAMINE' || request . type === 'SELECT' ) {
this . _box . readOnly = (
typeof responseCode === 'string'
&& responseCode . toUpperCase ( ) === 'READ-ONLY'
) ;
}
// According to RFC 3501, UID commands do not give errors for
// non-existant user-supplied UIDs, so give the callback empty results
// if we unexpectedly received no untagged responses.
if ( RE _UIDCMD _HASRESULTS . test ( request . fullcmd ) && request . cbargs . length === 0 ) {
request . cbargs . push ( [ ] ) ;
}
}
if ( request . bodyEmitter != null ) {
var bodyEmitter = request . bodyEmitter ;
if ( err ) {
bodyEmitter . emit ( 'error' , err ) ;
}
process . nextTick ( function ( ) {
bodyEmitter . emit ( 'end' ) ;
} ) ;
} else {
let extraArguments = asExpression ( ( ) => {
if ( responseCode != null && responseCode . key != null ) {
return matchValue ( responseCode . key . toUpperCase ( ) , {
// [uidvalidity, newUID]
APPENDUID : [ responseCode . val [ 1 ] ] ,
// [uidvalidity, sourceUIDs, destUIDs]
COPYUID : [ responseCode . val [ 2 ] ] ,
_ : [ ]
} ) ;
} else {
return [ ] ;
}
} ) ;
if ( responseCode != null && responseCode . key != null ) {
// FIXME: This eventually should replace the extraArguments array stuff
matchValue ( responseCode . key . toUpperCase ( ) , {
APPENDUID : ( ) => {
request . responseData . newUID = responseCode . val [ 1 ] ;
} ,
COPYUID : ( ) => {
// FIXME: Parsing? Looks like it will be multiple items
request . responseData . destinationUIDs = responseCode . val [ 2 ] ;
}
} ) ;
}
let response = { type , payload } ;
if ( commandHandlers . canHandleTagged ( request , response ) ) {
// FIXME: Add other fields with a sensible name
commandHandlers . handleTagged . call ( this , request , response ) ;
}
// console.dir({ done: request.cbargs }, { depth: null, colors: true });
if ( request . cb2 != null ) {
request . cb . apply ( this , request . responseData ) ;
} else if ( request . cb != null ) {
request . cb . apply ( this , [
err ,
... request . cbargs ,
... extraArguments
] ) ;
}
}
if ( this . _queue . length === 0
&& this . _config . keepalive
&& this . state === 'authenticated'
&& ! this . _idle . enabled ) {
this . _idle . enabled = true ;
this . _doKeepaliveTimer ( true ) ;
}
this . _processQueue ( ) ;
} else {
// TODO: Configurable logger
console . warn ( ` ignoring response because there is no pending request ` ) ;
}
} ;
Connection . prototype . _resetCurrentBox = function ( ) {
this . _box = {
name : '' ,
flags : [ ] ,
readOnly : false ,
uidvalidity : 0 ,
uidnext : 0 ,
permFlags : [ ] ,
keywords : [ ] ,
newKeywords : false ,
persistentUIDs : true ,
nomodseq : false ,
messages : {
total : 0 ,
new : 0
}
} ;
} ;
// TODO: Refactor this better
Connection . prototype . _sendKeepalive = function ( ) {
let idleWait = this . _config . keepalive . idleInterval || MAX _IDLE _WAIT ;
let forceNoop = this . _config . keepalive . forceNoop || false ;
if ( this . _idle . enabled ) {
// unlike NOOP, IDLE is only a valid command after authenticating
if ( ! this . serverSupports ( 'IDLE' ) || this . state !== 'authenticated' || forceNoop ) {
// Ignore return
this . _enqueue2 ( command ` NOOP ` , { insertInFront : true } ) ;
this . _cancelKeepaliveTimer ( ) ;
} else {
if ( this . _idle . started === undefined ) {
this . _idle . started = 0 ;
// Ignore return
this . _enqueue2 ( command ` IDLE ` , { insertInFront : true } ) ;
} else if ( this . _idle . started > 0 ) {
var timeDiff = Date . now ( ) - this . _idle . started ;
if ( timeDiff >= idleWait ) {
this . _idle . enabled = false ;
this . debug && this . debug ( '=> DONE' ) ;
// ????
this . _sock . write ( 'DONE' + CRLF ) ;
return ;
}
}
}
}
} ;
// TODO: Get rid of
Connection . prototype . _doKeepaliveTimer = function ( immediate ) {
let interval = this . _config . keepalive . interval || KEEPALIVE _INTERVAL ;
if ( immediate ) {
return this . _sendKeepalive ( ) ;
} else {
this . _cancelKeepaliveTimer = pInterval ( interval , ( ) => {
this . _sendKeepalive ( ) ;
} ) ;
}
} ;
Connection . prototype . _login = function ( ) {
var self = this , checkedNS = false ;
var reentry = function ( err ) {
clearTimeout ( self . _authenticationTimeout ) ;
if ( err ) {
self . emit ( 'error' , err ) ;
return self . _sock . end ( ) ;
}
// 2. Get the list of available namespaces (RFC2342)
if ( ! checkedNS && self . serverSupports ( 'NAMESPACE' ) ) {
checkedNS = true ;
return self . _enqueue ( 'NAMESPACE' , reentry ) ;
}
// 3. Get the top-level mailbox hierarchy delimiter used by the server
self . _enqueue ( 'LIST "" ""' , function ( ) {
self . state = 'authenticated' ;
self . emit ( 'ready' ) ;
} ) ;
} ;
// 1. Get the supported capabilities
self . _enqueue ( 'CAPABILITY' , function ( ) {
// No need to attempt the login sequence if we're on a PREAUTH connection.
if ( self . state === 'connected' ) {
var err ,
checkCaps = function ( error ) {
if ( error ) {
error . source = 'authentication' ;
return reentry ( error ) ;
}
if ( self . _caps === undefined ) {
// Fetch server capabilities if they were not automatically
// provided after authentication
return self . _enqueue ( 'CAPABILITY' , reentry ) ;
} else
reentry ( ) ;
} ;
if ( self . serverSupports ( 'STARTTLS' )
&& ( self . _config . autotls === 'always'
|| ( self . _config . autotls === 'required'
&& self . serverSupports ( 'LOGINDISABLED' ) ) ) ) {
self . _starttls ( ) ;
return ;
}
if ( self . serverSupports ( 'LOGINDISABLED' ) ) {
err = new Error ( 'Logging in is disabled on this server' ) ;
err . source = 'authentication' ;
return reentry ( err ) ;
}
var cmd ;
if ( self . serverSupports ( 'AUTH=XOAUTH' ) && self . _config . xoauth ) {
self . _caps = undefined ;
cmd = 'AUTHENTICATE XOAUTH' ;
// are there any servers that support XOAUTH/XOAUTH2 and not SASL-IR?
//if (self.serverSupports('SASL-IR'))
cmd += ' ' + escape ( self . _config . xoauth ) ;
self . _enqueue ( cmd , checkCaps ) ;
} else if ( self . serverSupports ( 'AUTH=XOAUTH2' ) && self . _config . xoauth2 ) {
self . _caps = undefined ;
cmd = 'AUTHENTICATE XOAUTH2' ;
//if (self.serverSupports('SASL-IR'))
cmd += ' ' + escape ( self . _config . xoauth2 ) ;
self . _enqueue ( cmd , checkCaps ) ;
} else if ( self . _config . user && self . _config . password ) {
self . _caps = undefined ;
self . _enqueue ( 'LOGIN "' + escape ( self . _config . user ) + '" "'
+ escape ( self . _config . password ) + '"' , checkCaps ) ;
} else {
err = new Error ( 'No supported authentication method(s) available. '
+ 'Unable to login.' ) ;
err . source = 'authentication' ;
return reentry ( err ) ;
}
} else
reentry ( ) ;
} ) ;
} ;
Connection . prototype . _starttls = function ( ) {
var self = this ;
this . _enqueue ( 'STARTTLS' , function ( err ) {
if ( err ) {
self . emit ( 'error' , err ) ;
return self . _sock . end ( ) ;
}
self . _caps = undefined ;
self . _sock . removeAllListeners ( 'error' ) ;
var tlsOptions = { } ;
tlsOptions . host = this . _config . host ;
// Host name may be overridden the tlsOptions
for ( var k in this . _config . tlsOptions )
tlsOptions [ k ] = this . _config . tlsOptions [ k ] ;
tlsOptions . socket = self . _sock ;
self . _sock = tls . connect ( tlsOptions , function ( ) {
self . _login ( ) ;
} ) ;
self . _sock . on ( 'error' , self . _onError ) ;
self . _sock . on ( 'timeout' , self . _onSocketTimeout ) ;
self . _sock . setTimeout ( self . _config . socketTimeout ) ;
self . _parser . setStream ( self . _sock ) ;
} ) ;
} ;
Connection . prototype . _processQueue = function ( ) {
if ( this . _curReq || ! this . _queue . length || ! this . _sock || ! this . _sock . writable )
return ;
this . _curReq = this . _queue . shift ( ) ;
if ( this . _tagcount === MAX _INT )
this . _tagcount = 0 ;
var prefix ;
if ( this . _curReq . type === 'IDLE' || this . _curReq . type === 'NOOP' )
prefix = this . _curReq . type ;
else
prefix = 'A' + ( this . _tagcount ++ ) ;
var out = prefix + ' ' + this . _curReq . fullcmd ;
this . debug && this . debug ( '=> ' + inspect ( out ) ) ;
this . _sock . write ( out + CRLF , 'utf8' ) ;
if ( this . _curReq . literalAppendData ) {
// LITERAL+: we are appending a mesage, and not waiting for a reply
this . _sockWriteAppendData ( this . _curReq . literalAppendData ) ;
}
} ;
Connection . prototype . _sockWriteAppendData = function ( appendData )
{
var val = appendData ;
if ( Buffer . isBuffer ( appendData ) )
val = val . toString ( 'utf8' ) ;
this . debug && this . debug ( '=> ' + inspect ( val ) ) ;
this . _sock . write ( val ) ;
this . _sock . write ( CRLF ) ;
} ;
Connection . prototype . _enqueue = function ( fullcmd , promote , cb , newAPI ) {
// TODO: Remove variability
if ( typeof promote === 'function' ) {
cb = promote ;
promote = false ;
}
var request = {
type : fullcmd . match ( RE _CMD ) [ 1 ] ,
fullcmd : fullcmd ,
cb : ( newAPI ) ? null : cb ,
cb2 : ( newAPI ) ? cb : null ,
cbargs : [ ] ,
responseData : { }
} ;
// Alias
request . legacyArgs = request . cbargs ;
var self = this ;
if ( promote ) {
this . _queue . unshift ( request ) ;
} else {
this . _queue . push ( request ) ;
}
if ( ! this . _curReq
&& this . state !== 'disconnected'
&& this . state !== 'upgrading' ) {
// defer until next tick for requests like APPEND and FETCH where access to
// the request object is needed immediately after enqueueing
process . nextTick ( function ( ) { self . _processQueue ( ) ; } ) ;
} else if ( this . _curReq
&& this . _curReq . type === 'IDLE'
&& this . _sock
&& this . _sock . writable
&& this . _idle . enabled ) {
this . _idle . enabled = false ;
this . _cancelKeepaliveTimer ( ) ;
if ( this . _idle . started > 0 ) {
// we've seen the continuation for our IDLE
this . debug && this . debug ( '=> DONE' ) ;
this . _sock . write ( 'DONE' + CRLF ) ;
}
}
} ;
Connection . prototype . _enqueueAsync = Promise . promisify ( Connection . prototype . _enqueue ) ;
Connection . prototype . _enqueue2 = function ( command , options = { } ) {
let insertInFront = defaultValue ( options . insertInFront , false ) ;
if ( command . toCommandString != null ) {
let string = command . toCommandString ( ) ;
return this . _enqueueAsync ( string , insertInFront ) ;
} else {
// TODO: Use `unreachable`
throw unreachable ( ` Must use a command template string ` ) ;
}
} ;
Connection . parseHeader = parseHeader ; // from Parser.js
module . exports = Connection ;
// utilities -------------------------------------------------------------------
function escape ( str ) {
return str . replace ( RE _BACKSLASH , '\\\\' ) . replace ( RE _DBLQUOTE , '\\"' ) ;
}
function validateUIDList ( uids , noThrow ) {
for ( var i = 0 , len = uids . length , intval ; i < len ; ++ i ) {
if ( typeof uids [ i ] === 'string' ) {
if ( uids [ i ] === '*' || uids [ i ] === '*:*' ) {
if ( len > 1 )
uids = [ '*' ] ;
break ;
} else if ( RE _NUM _RANGE . test ( uids [ i ] ) )
continue ;
}
intval = parseInt ( '' + uids [ i ] , 10 ) ;
if ( isNaN ( intval ) ) {
var err = new Error ( 'UID/seqno must be an integer, "*", or a range: '
+ uids [ i ] ) ;
if ( noThrow )
return err ;
else
throw err ;
} else if ( intval <= 0 ) {
var err = new Error ( 'UID/seqno must be greater than zero' ) ;
if ( noThrow )
return err ;
else
throw err ;
} else if ( typeof uids [ i ] !== 'number' ) {
uids [ i ] = intval ;
}
}
}
function hasNonASCII ( str ) {
for ( var i = 0 , len = str . length ; i < len ; ++ i ) {
if ( str . charCodeAt ( i ) > 0x7F )
return true ;
}
return false ;
}
function buildString ( str ) {
if ( typeof str !== 'string' )
str = '' + str ;
if ( hasNonASCII ( str ) ) {
var buf = Buffer . from ( str , 'utf8' ) ;
return '{' + buf . length + '}\r\n' + buf . toString ( 'binary' ) ;
} else
return '"' + escape ( str ) + '"' ;
}
function buildSearchQuery ( options , extensions , info , isOrChild ) {
var searchargs = '' , err , val ;
for ( var i = 0 , len = options . length ; i < len ; ++ i ) {
var criteria = ( isOrChild ? options : options [ i ] ) ,
args = null ,
modifier = ( isOrChild ? '' : ' ' ) ;
if ( typeof criteria === 'string' )
criteria = criteria . toUpperCase ( ) ;
else if ( Array . isArray ( criteria ) ) {
if ( criteria . length > 1 )
args = criteria . slice ( 1 ) ;
if ( criteria . length > 0 )
criteria = criteria [ 0 ] . toUpperCase ( ) ;
} else
throw new Error ( 'Unexpected search option data type. '
+ 'Expected string or array. Got: ' + typeof criteria ) ;
if ( criteria === 'OR' ) {
if ( args . length !== 2 )
throw new Error ( 'OR must have exactly two arguments' ) ;
if ( isOrChild )
searchargs += 'OR (' ;
else
searchargs += ' OR (' ;
searchargs += buildSearchQuery ( args [ 0 ] , extensions , info , true ) ;
searchargs += ') (' ;
searchargs += buildSearchQuery ( args [ 1 ] , extensions , info , true ) ;
searchargs += ')' ;
} else {
if ( criteria [ 0 ] === '!' ) {
modifier += 'NOT ' ;
criteria = criteria . substr ( 1 ) ;
}
switch ( criteria ) {
// -- Standard criteria --
case 'ALL' :
case 'ANSWERED' :
case 'DELETED' :
case 'DRAFT' :
case 'FLAGGED' :
case 'NEW' :
case 'SEEN' :
case 'RECENT' :
case 'OLD' :
case 'UNANSWERED' :
case 'UNDELETED' :
case 'UNDRAFT' :
case 'UNFLAGGED' :
case 'UNSEEN' :
searchargs += modifier + criteria ;
break ;
case 'BCC' :
case 'BODY' :
case 'CC' :
case 'FROM' :
case 'SUBJECT' :
case 'TEXT' :
case 'TO' :
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
val = buildString ( args [ 0 ] ) ;
if ( info && val [ 0 ] === '{' )
info . hasUTF8 = true ;
searchargs += modifier + criteria + ' ' + val ;
break ;
case 'BEFORE' :
case 'ON' :
case 'SENTBEFORE' :
case 'SENTON' :
case 'SENTSINCE' :
case 'SINCE' :
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
else if ( ! ( args [ 0 ] instanceof Date ) ) {
if ( ( args [ 0 ] = new Date ( args [ 0 ] ) ) . toString ( ) === 'Invalid Date' )
throw new Error ( 'Search option argument must be a Date object'
+ ' or a parseable date string' ) ;
}
searchargs += modifier + criteria + ' ' + args [ 0 ] . getDate ( ) + '-'
+ MONTHS [ args [ 0 ] . getMonth ( ) ] + '-'
+ args [ 0 ] . getFullYear ( ) ;
break ;
case 'KEYWORD' :
case 'UNKEYWORD' :
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
searchargs += modifier + criteria + ' ' + args [ 0 ] ;
break ;
case 'LARGER' :
case 'SMALLER' :
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
var num = parseInt ( args [ 0 ] , 10 ) ;
if ( isNaN ( num ) )
throw new Error ( 'Search option argument must be a number' ) ;
searchargs += modifier + criteria + ' ' + args [ 0 ] ;
break ;
case 'HEADER' :
if ( ! args || args . length !== 2 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
val = buildString ( args [ 1 ] ) ;
if ( info && val [ 0 ] === '{' )
info . hasUTF8 = true ;
searchargs += modifier + criteria + ' "' + escape ( '' + args [ 0 ] )
+ '" ' + val ;
break ;
case 'UID' :
if ( ! args )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
validateUIDList ( args ) ;
if ( args . length === 0 )
throw new Error ( 'Empty uid list' ) ;
searchargs += modifier + criteria + ' ' + args . join ( ',' ) ;
break ;
// Extensions ==========================================================
case 'X-GM-MSGID' : // Gmail unique message ID
case 'X-GM-THRID' : // Gmail thread ID
if ( extensions . indexOf ( 'X-GM-EXT-1' ) === - 1 )
throw new Error ( 'IMAP extension not available for: ' + criteria ) ;
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
else {
val = '' + args [ 0 ] ;
if ( ! ( RE _INTEGER . test ( args [ 0 ] ) ) )
throw new Error ( 'Invalid value' ) ;
}
searchargs += modifier + criteria + ' ' + val ;
break ;
case 'X-GM-RAW' : // Gmail search syntax
if ( extensions . indexOf ( 'X-GM-EXT-1' ) === - 1 )
throw new Error ( 'IMAP extension not available for: ' + criteria ) ;
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
val = buildString ( args [ 0 ] ) ;
if ( info && val [ 0 ] === '{' )
info . hasUTF8 = true ;
searchargs += modifier + criteria + ' ' + val ;
break ;
case 'X-GM-LABELS' : // Gmail labels
if ( extensions . indexOf ( 'X-GM-EXT-1' ) === - 1 )
throw new Error ( 'IMAP extension not available for: ' + criteria ) ;
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
searchargs += modifier + criteria + ' ' + args [ 0 ] ;
break ;
case 'MODSEQ' :
if ( extensions . indexOf ( 'CONDSTORE' ) === - 1 )
throw new Error ( 'IMAP extension not available for: ' + criteria ) ;
if ( ! args || args . length !== 1 )
throw new Error ( 'Incorrect number of arguments for search option: '
+ criteria ) ;
searchargs += modifier + criteria + ' ' + args [ 0 ] ;
break ;
default :
// last hope it's a seqno set
// http://tools.ietf.org/html/rfc3501#section-6.4.4
var seqnos = ( args ? [ criteria ] . concat ( args ) : [ criteria ] ) ;
if ( ! validateUIDList ( seqnos , true ) ) {
if ( seqnos . length === 0 )
throw new Error ( 'Empty sequence number list' ) ;
searchargs += modifier + seqnos . join ( ',' ) ;
} else
throw new Error ( 'Unexpected search option: ' + criteria ) ;
}
}
if ( isOrChild )
break ;
}
return searchargs ;
}
// Pulled from assert.deepEqual:
var pSlice = Array . prototype . slice ;
function _deepEqual ( actual , expected ) {
// 7.1. All identical values are equivalent, as determined by ===.
if ( actual === expected ) {
return true ;
} else if ( Buffer . isBuffer ( actual ) && Buffer . isBuffer ( expected ) ) {
if ( actual . length !== expected . length ) return false ;
for ( var i = 0 ; i < actual . length ; i ++ ) {
if ( actual [ i ] !== expected [ i ] ) return false ;
}
return true ;
// 7.2. If the expected value is a Date object, the actual value is
// equivalent if it is also a Date object that refers to the same time.
} else if ( actual instanceof Date && expected instanceof Date ) {
return actual . getTime ( ) === expected . getTime ( ) ;
// 7.3 If the expected value is a RegExp object, the actual value is
// equivalent if it is also a RegExp object with the same source and
// properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
} else if ( actual instanceof RegExp && expected instanceof RegExp ) {
return actual . source === expected . source &&
actual . global === expected . global &&
actual . multiline === expected . multiline &&
actual . lastIndex === expected . lastIndex &&
actual . ignoreCase === expected . ignoreCase ;
// 7.4. Other pairs that do not both pass typeof value == 'object',
// equivalence is determined by ==.
} else if ( typeof actual !== 'object' && typeof expected !== 'object' ) {
return actual == expected ;
// 7.5 For all other Object pairs, including Array objects, equivalence is
// determined by having the same number of owned properties (as verified
// with Object.prototype.hasOwnProperty.call), the same set of keys
// (although not necessarily the same order), equivalent values for every
// corresponding key, and an identical 'prototype' property. Note: this
// accounts for both named and indexed properties on Arrays.
} else {
return objEquiv ( actual , expected ) ;
}
}
function isUndefinedOrNull ( value ) {
return value === null || value === undefined ;
}
function isArguments ( object ) {
return Object . prototype . toString . call ( object ) === '[object Arguments]' ;
}
function objEquiv ( a , b ) {
var ka , kb , key , i ;
if ( isUndefinedOrNull ( a ) || isUndefinedOrNull ( b ) )
return false ;
// an identical 'prototype' property.
if ( a . prototype !== b . prototype ) return false ;
//~~~I've managed to break Object.keys through screwy arguments passing.
// Converting to array solves the problem.
if ( isArguments ( a ) ) {
if ( ! isArguments ( b ) ) {
return false ;
}
a = pSlice . call ( a ) ;
b = pSlice . call ( b ) ;
return _deepEqual ( a , b ) ;
}
try {
ka = Object . keys ( a ) ;
kb = Object . keys ( b ) ;
} catch ( e ) { //happens when one is a string literal and the other isn't
return false ;
}
// having the same number of owned properties (keys incorporates
// hasOwnProperty)
if ( ka . length !== kb . length )
return false ;
//the same set of keys (although not necessarily the same order),
ka . sort ( ) ;
kb . sort ( ) ;
//~~~cheap key test
for ( i = ka . length - 1 ; i >= 0 ; i -- ) {
if ( ka [ i ] != kb [ i ] )
return false ;
}
//equivalent values for every corresponding key, and
//~~~possibly expensive deep test
for ( i = ka . length - 1 ; i >= 0 ; i -- ) {
key = ka [ i ] ;
if ( ! _deepEqual ( a [ key ] , b [ key ] ) ) return false ;
}
return true ;
}