@ -2,10 +2,14 @@
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 { 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" ) ;
var tls = require ( 'tls' ) ,
Socket = require ( 'net' ) . Socket ,
@ -1234,25 +1238,166 @@ Object.defineProperty(Connection.prototype, 'seq', { get: function() {
} ;
} } ) ;
function createCommandHandlers ( rules ) {
let untaggedHandlers = new Map ( ) ;
let taggedHandlers = new Map ( ) ;
for ( let [ types , options ] of Object . entries ( rules ) ) {
let parsedTypes = types
. split ( /\s*\s*/ )
. map ( ( type ) => type . toUpperCase ( ) ) ;
for ( let type of parsedTypes ) {
if ( options . untagged != null ) {
untaggedHandlers . set ( type , options . untagged ) ;
}
if ( options . tagged != null ) {
taggedHandlers . set ( type , options . tagged ) ;
}
}
}
// REFACTOR: Eventually remove `.call(this` hackery
return {
canHandleUntagged : function ( request ) {
return untaggedHandlers . has ( request . type . toUpperCase ( ) ) ;
} ,
canHandleTagged : function ( request ) {
return taggedHandlers . has ( request . type . toUpperCase ( ) ) ;
} ,
handleUntagged : function ( request , data ) {
let handler = untaggedHandlers . get ( request . type . toUpperCase ( ) ) ;
return handler . call ( this , request , data ) ;
} ,
handleTagged : function ( request , data ) {
let handler = taggedHandlers . get ( request . type . toUpperCase ( ) ) ;
return handler . call ( this , request , data ) ;
}
} ;
}
let commandHandlers = createCommandHandlers ( {
"LIST, XLIST, LSUB" : {
untagged : function ( request , { payload } ) {
if ( request . delimiter === undefined ) {
request . delimiter = payload . delimiter ;
} else {
if ( request . boxBuilder == null ) {
request . boxBuilder = createBoxTreeBuilder ( ) ;
}
request . _curReq . 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" : {
untagged : function ( request , { payload } ) {
// https://datatracker.ietf.org/doc/html/rfc2971
// Used for communicating server/client name, version, etc.
request . responseData . serverVersion = payload ;
request . legacyArgs . push ( payload ) ;
}
}
} ) ;
// type: type,
// num: num,
// 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
Connection . prototype . _resUntagged = function ( { type , num , textCode , text : payload } ) {
// 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 ;
if ( type === 'bye' ) {
if ( this . _curReq != null && commandHandlers . canHandleUntagged ( this . _curReq ) ) {
// FIXME: Include other fields
commandHandlers . handleUntagged . call ( this , this . _curReq , { sequenceNumber , payload } ) ;
} else if ( type === 'bye' ) {
this . _sock . end ( ) ;
} else if ( type === 'namespace' ) {
this . namespaces = payload ;
} else if ( type === 'id' ) {
this . _curReq . cbargs . push ( payload ) ;
} else if ( type === 'capability' ) {
this . _caps = payload . map ( ( v ) => v . toUpperCase ( ) ) ;
} else if ( type === 'preauth' ) {
this . state = 'authenticated' ;
} else if ( type === 'sort' || type === 'thread' || type === 'esearch' ) {
} else if ( type === 'expunge' ) {
if ( this . _box ) {
if ( this . _box . messages . total > 0 ) {
this . _box . messages . total -= 1 ;
}
this . emit ( 'expunge' , sequenceNumber ) ;
}
} 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 === "esearch" ) {
// https://datatracker.ietf.org/doc/html/rfc4731 / https://datatracker.ietf.org/doc/html/rfc7377
Object . assign ( this . _curReq . responseData , payload ) ; // Protocol-defined attributes. TODO: Improve the key names for this? Or is there extensibility?
this . _curReq . cbargs . push ( payload ) ;
} else if ( type === "sort" ) {
// https://datatracker.ietf.org/doc/html/rfc5256
this . _curReq . responseData . UIDs = payload ;
this . _curReq . cbargs . push ( payload ) ;
} else if ( type === 'thread' ) {
// https://datatracker.ietf.org/doc/html/rfc5256
this . _curReq . responseData . threads = payload ; // FIXME: Work out the exact format
this . _curReq . cbargs . push ( payload ) ;
} else if ( type === 'search' ) {
if ( payload . results !== undefined ) {
@ -1263,23 +1408,24 @@ Connection.prototype._resUntagged = function({ type, num, textCode, text: payloa
this . _curReq . cbargs . push ( payload ) ;
}
} else if ( type === 'quota' ) {
var cbargs = this . _curReq . cbargs ;
if ( ! cbargs . length ) {
cbargs . push ( [ ] ) ;
}
let responseData = this . _curReq . responseData ;
if ( responseData . quota == null ) { responseData . quota = [ ] ; } ;
responseData . quota . push ( payload ) ;
let cbargs = this . _curReq . cbargs ;
if ( cbargs . length === 0 ) { cbargs . push ( [ ] ) ; }
cbargs [ 0 ] . push ( payload ) ;
} else if ( type === 'recent' ) {
if ( ! this . _box && RE _OPENBOX . test ( this . _curReq . type ) ) {
this . _ create CurrentBox( ) ;
this . _ reset CurrentBox( ) ;
}
if ( this . _box ) {
this . _box . messages . new = num;
this . _box . messages . new = seque nceN umber ;
}
} else if ( type === 'flags' ) {
if ( ! this . _box && RE _OPENBOX . test ( this . _curReq . type ) ) {
this . _ create CurrentBox( ) ;
this . _ reset CurrentBox( ) ;
}
if ( this . _box ) {
@ -1296,124 +1442,17 @@ Connection.prototype._resUntagged = function({ type, num, textCode, text: payloa
}
} else if ( type === 'exists' ) {
if ( ! this . _box && RE _OPENBOX . test ( this . _curReq . type ) ) {
this . _ create CurrentBox( ) ;
this . _ reset CurrentBox( ) ;
}
if ( this . _box ) {
var prev = this . _box . messages . total , now = num;
var prev = this . _box . messages . total , now = seque nceN umber ;
this . _box . messages . total = now ;
if ( now > prev && this . state === 'authenticated' ) {
this . _box . messages . new = now - prev ;
this . emit ( 'mail' , this . _box . messages . new ) ;
}
}
} else if ( type === 'expunge' ) {
if ( this . _box ) {
if ( this . _box . messages . total > 0 ) {
-- this . _box . messages . total ;
}
this . emit ( 'expunge' , num ) ;
}
} else if ( type === 'ok' ) {
if ( this . state === 'connected' && ! this . _curReq ) {
this . _login ( ) ;
} else if ( typeof textCode === 'string' && textCode . toUpperCase ( ) === 'ALERT' ) {
this . emit ( 'alert' , payload ) ;
}
else if ( this . _curReq
&& textCode
&& ( RE _OPENBOX . test ( this . _curReq . type ) ) ) {
// we're opening a mailbox
if ( ! this . _box ) {
this . _createCurrentBox ( ) ;
}
if ( textCode . key ) {
destinationKey = textCode . key . toUpperCase ( ) ;
} else {
destinationKey = textCode ;
}
if ( destinationKey === 'UIDVALIDITY' ) {
this . _box . uidvalidity = textCode . val ;
} else if ( destinationKey === 'UIDNEXT' ) {
this . _box . uidnext = textCode . val ;
} else if ( destinationKey === 'HIGHESTMODSEQ' ) {
this . _box . highestmodseq = '' + textCode . val ;
} else if ( destinationKey === 'PERMANENTFLAGS' ) {
var idx , permFlags , keywords ;
this . _box . permFlags = permFlags = textCode . 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 textCode === 'string'
&& textCode . toUpperCase ( ) === 'UIDVALIDITY' )
this . emit ( 'uidvalidity' , payload ) ;
} else if ( type === 'list' || type === 'lsub' || type === 'xlist' ) {
if ( this . delimiter === undefined ) {
this . delimiter = payload . delimiter ;
} else {
if ( this . _curReq . cbargs . length === 0 ) {
this . _curReq . cbargs . push ( { } ) ;
}
box = {
attribs : payload . flags ,
delimiter : payload . delimiter ,
children : null ,
parent : null
} ;
for ( i = 0 , len = SPECIAL _USE _ATTRIBUTES . length ; i < len ; ++ i ) {
if ( box . attribs . indexOf ( SPECIAL _USE _ATTRIBUTES [ i ] ) > - 1 ) {
box . special _use _attrib = SPECIAL _USE _ATTRIBUTES [ i ] ;
}
}
var name = payload . name ,
curChildren = this . _curReq . cbargs [ 0 ] ;
if ( box . delimiter ) {
var path = name . split ( box . delimiter ) ,
parent = null ;
name = path . pop ( ) ;
for ( i = 0 , len = path . length ; i < len ; ++ i ) {
if ( ! curChildren [ path [ i ] ] ) {
curChildren [ path [ i ] ] = { } ;
}
if ( ! curChildren [ path [ i ] ] . children ) {
curChildren [ path [ i ] ] . children = { } ;
}
parent = curChildren [ path [ i ] ] ;
curChildren = curChildren [ path [ i ] ] . children ;
}
box . parent = parent ;
}
if ( curChildren [ name ] ) {
box . children = curChildren [ name ] . children ;
}
curChildren [ name ] = box ;
}
} else if ( type === 'status' ) {
let attrs = defaultValue ( payload . attrs , { } ) ;
@ -1437,57 +1476,59 @@ Connection.prototype._resUntagged = function({ type, num, textCode, text: payloa
} 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 ( num) ;
let task = this . _curReq . fetchCache . get ( seque nceN umber ) ;
// FIXME: Refactor, probably make the task itself an event emitter
if ( task == null ) {
task = this . _curReq . fetchCache . create ( num, this . _curReq . fetching . slice ( ) ) ;
this . _curReq . bodyEmitter . emit ( 'message' , task . emitter , num) ;
task = this . _curReq . fetchCache . create ( seque nceN umber , this . _curReq . fetching . slice ( ) ) ;
this . _curReq . bodyEmitter . emit ( 'message' , task . emitter , seque nceN umber ) ;
}
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' , num, payload ) ;
this . emit ( 'update' , seque nceN umber , payload ) ;
}
}
} ;
Connection . prototype . _resTagged = function ( info ) {
var req = this . _curReq ;
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 ( req != null ) {
if ( req uest != null ) {
var err ;
this . _curReq = undefined ;
if ( info. type === 'no' || info . type === 'bad' ) {
// TODO: Can info. text be an empty string?
let errorText = defaultValue ( info. text , req . oauthError ) ;
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 : info. type,
text : info. text Code,
type : type,
text : response Code,
source : "protocol"
} ) ;
} else if ( this . _box != null ) {
if ( req . type === 'EXAMINE' || req . type === 'SELECT' ) {
if ( req uest . type === 'EXAMINE' || req uest . type === 'SELECT' ) {
this . _box . readOnly = (
typeof info. text Code === 'string'
&& info. text Code. toUpperCase ( ) === 'READ-ONLY'
typeof response Code === 'string'
&& response Code. 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 ( req . fullcmd ) && req . cbargs . length === 0 ) {
req . cbargs . push ( [ ] ) ;
if ( RE _UIDCMD _HASRESULTS . test ( req uest . fullcmd ) && req uest . cbargs . length === 0 ) {
req uest . cbargs . push ( [ ] ) ;
}
}
if ( req . bodyEmitter ) {
var bodyEmitter = req . bodyEmitter ;
if ( req uest . bodyEmitter != null ) {
var bodyEmitter = req uest . bodyEmitter ;
if ( err ) {
bodyEmitter . emit ( 'error' , err ) ;
@ -1497,19 +1538,48 @@ Connection.prototype._resTagged = function(info) {
bodyEmitter . emit ( 'end' ) ;
} ) ;
} else {
req . cbargs . unshift ( err ) ;
if ( info . textCode && info . textCode . key ) {
var key = info . textCode . key . toUpperCase ( ) ;
if ( key === 'APPENDUID' ) { // [uidvalidity, newUID]
req . cbargs . push ( info . textCode . val [ 1 ] ) ;
} else if ( key === 'COPYUID' ) { // [uidvalidity, sourceUIDs, destUIDs]
req . cbargs . push ( info . textCode . val [ 2 ] ) ;
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 ] ;
}
} ) ;
}
if ( req . cb != null ) {
req . cb . apply ( this , req . cbargs ) ;
if ( commandHandlers . canHandleTagged ( request ) ) {
// FIXME: Add other fields with a sensible name
commandHandlers . handleTagged . call ( this , request , { payload } ) ;
}
// 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
] ) ;
}
}
@ -1528,7 +1598,7 @@ Connection.prototype._resTagged = function(info) {
}
} ;
Connection . prototype . _ create CurrentBox = function ( ) {
Connection . prototype . _ reset CurrentBox = function ( ) {
this . _box = {
name : '' ,
flags : [ ] ,
@ -1744,25 +1814,31 @@ Connection.prototype._sockWriteAppendData = function(appendData)
this . _sock . write ( CRLF ) ;
} ;
Connection . prototype . _enqueue = function ( fullcmd , promote , cb ) {
Connection . prototype . _enqueue = function ( fullcmd , promote , cb , newAPI ) {
// TODO: Remove variability
if ( typeof promote === 'function' ) {
cb = promote ;
promote = false ;
}
var info = {
type : fullcmd . match ( RE _CMD ) [ 1 ] ,
fullcmd : fullcmd ,
cb : cb ,
cbargs : [ ]
} ,
self = this ;
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 ( info ) ;
this . _queue . unshift ( request ) ;
} else {
this . _queue . push ( info ) ;
this . _queue . push ( request ) ;
}
if ( ! this . _curReq
@ -1796,7 +1872,7 @@ Connection.prototype._enqueue2 = function (command, options = {}) {
return this . _enqueueAsync ( string , insertInFront ) ;
} else {
// TODO: Use `unreachable`
throw new Error ( ` Must use a command template string ` ) ;
throw unreachable ( ` Must use a command template string ` ) ;
}
} ;