var sys = require('sys'), net = require('net'), EventEmitter = require('events').EventEmitter; var empty = function() {}, CRLF = "\r\n", debug=empty/*sys.debug*/, STATES = { NOCONNECT: 0, NOAUTH: 1, AUTH: 2, BOXSELECTING: 3, BOXSELECTED: 4 }; function ImapConnection (options) { this._options = { username: '', password: '', host: 'localhost', port: 143, secure: false }; this._state = { status: STATES.NOCONNECT, conn: null, curId: 0, requests: [], numCapRecvs: 0, isReady: false, isIdle: true, delim: '/', tmrKeepalive: null, tmoKeepalive: 10000, curData: '', fetchData: { flags: [], date: null, headers: null, body: null, structure: null, _total: 0 }, box: { _uidnext: 0, _uidvalidity: 0, _flags: [], permFlags: [], name: null, messages: { total: 0, new: 0 }} }; this._capabilities = []; this._options = extend(true, this._options, options); }; sys.inherits(ImapConnection, EventEmitter); exports.ImapConnection = ImapConnection; ImapConnection.prototype.connect = function(loginCb) { var self = this; var fnInit = function() { // First get pre-auth capabilities, including server-supported auth mechanisms self._send('CAPABILITY', function() { // Next attempt to login self._login(function(err) { if (err) { loginCb(err); return; } // Lastly, get the mailbox hierarchy delimiter/separator used by the server self._send('LIST "" ""', loginCb); }); }); }; this._reset(); this._state.conn = net.createConnection(this._options.port, this._options.host); if (this._options.secure) { this._state.conn.setSecure(); this._state.conn.on('secure', function() { debug('Secure connection made.'); }); } this._state.conn.setKeepAlive(true); this._state.conn.setEncoding('utf8'); this._state.conn.on('connect', function() { debug('Connected to host.'); self._state.conn.write(''); self._state.status = STATES.NOAUTH; }); this._state.conn.on('ready', function() { fnInit(); }); this._state.conn.on('data', function(data) { var literalData = ''; debug('RECEIVED: ' + data); if (data.indexOf(CRLF) === -1) { if (self._state.curData) self._state.curData += data; else self._state.curData = data; return; } if (self._state.curData) data = self._state.curData + data; self._state.curData = undefined; // Don't mess with incoming data if it's part of a literal if (/\{(\d+)\}$/.test(data.substr(0, data.indexOf(CRLF)))) { var result = /\{(\d+)\}$/.exec(data.substr(0, data.indexOf(CRLF))); self._state.fetchData._total = parseInt(result[1]); } if (self._state.fetchData._total > 0) { if (data.length - (data.indexOf(CRLF)+2) <= self._state.fetchData._total) { self._state.curData = data; return; } literalData = data.substr(data.indexOf(CRLF) + 2, self._state.fetchData._total); data = data.substr(0, data.indexOf(CRLF)) + data.substr(data.indexOf(CRLF) + 2 + self._state.fetchData._total); self._state.fetchData._total = 0; } data = data.split(CRLF).filter(isNotEmpty); // Defer any extra server responses found in the incoming data if (data.length > 1) { data.slice(1).forEach(function(line) { process.nextTick(function() { self._state.conn.emit('data', line + CRLF); }); }); } data = data[0]; data = data.explode(' ', 3); if (data[0] === '+') { // Continuation // Should never happen .... } else if (data[0] === '*') { // Untagged server response if (self._state.status === STATES.NOAUTH) { if (data[1] === 'PREAUTH') { // no need to login, the server pre-authenticated us self._state.status = STATES.AUTH; if (self._state.numCapRecvs === 0) self._state.numCapRecvs = 1; } else if (data[1] === 'NO' || data[1] === 'BAD' || data[1] === 'BYE') { self._state.conn.end(); return; } if (!self._state.isReady) { self._state.isReady = true; self._state.conn.emit('ready'); } // Restrict the type of server responses when unauthenticated if (data[1] !== 'CAPABILITY' || data[1] !== 'ALERT') return; } switch (data[1]) { case 'CAPABILITY': if (self._state.numCapRecvs < 2) self._state.numCapRecvs++; self._capabilities = data[2].split(' ').map(up); break; case 'FLAGS': if (self._state.status === STATES.BOXSELECTING) self._state.box._flags = data[2].substr(1, data[2].length-2).split(' '); case 'OK': if ((result = /^\[ALERT\] (.*)$/i.exec(data[2])) !== null) self.emit('alert', result[1]); else if (self._state.status === STATES.BOXSELECTING) { var result; if ((result = /^\[UIDVALIDITY (\d+)\]$/i.exec(data[2])) !== null) self._state.box._uidvalidity = result[1]; else if ((result = /^\[UIDNEXT (\d+)\]$/i.exec(data[2])) !== null) self._state.box._uidnext = result[1]; else if ((result = /^\[PERMANENTFLAGS \((.*)\)\]$/i.exec(data[2])) !== null) self._state.box.permFlags = result[1].split(' '); } break; case 'SEARCH': self._state.box._lastSearch = data[2].split(' '); break; case 'LIST': var result; if ((result = /^\(\\Noselect\) "(.+)" ""$/.exec(data[2])) !== null) self._state.delim = result[1]; break; default: if (/^\d+$/.test(data[1])) { switch (data[2]) { case 'EXISTS': // mailbox total message count self._state.box.messages.total = parseInt(data[1]); break; case 'RECENT': // messages marked with the \Recent flag (i.e. new messages) self._state.box.messages.new = parseInt(data[1]); if (self._state.status !== STATES.BOXSELECTING) self.emit('mail', self._state.box.messages.new); // new mail notification break; case 'EXPUNGE': // confirms permanent deletion of a single message if (self._state.box.messages.total > 0) self._state.box.messages.total--; break; default: // Check for FETCH result if (/^FETCH /i.test(data[2])) parseFetch(data[2].substring(data[2].indexOf('(')+1, data[2].length-1), literalData, self._state.fetchData); break; } } } } else if (data[0].indexOf('A') === 0) { // Tagged server response //var id = data[0].substr(1); clearTimeout(self._state.tmrKeepalive); self._state.tmrKeepalive = setTimeout(self._idleCheck.bind(self), self._state.tmoKeepalive); if (self._state.status === STATES.BOXSELECTING) { if (data[1] === 'OK') self._state.status = STATES.BOXSELECTED; else { self._state.status = STATES.AUTH; self._resetBox(); } } if (typeof self._state.requests[0].callback === 'function') { var err = null; if (data[1] !== 'OK') { err = new Error('Error while executing request: ' + data[2]); err.type = data[1]; err.request = self._state.requests[0].command; self._state.requests[0].callback(err); } else if (self._state.status === STATES.BOXSELECTED) { if (data[2].indexOf('SEARCH') === 0) { var result = self._state.box._lastSearch; self._state.box._lastSearch = null; self._state.requests[0].callback(err, self._state.box, result); } else if (self._state.requests[0].command.indexOf('UID FETCH') === 0) self._state.requests[0].callback(err, self._state.box, self._state.fetchData); else self._state.requests[0].callback(err, self._state.box); } else self._state.requests[0].callback(err); } self._state.requests.shift(); process.nextTick(function() { self._send(); }); self._state.isIdle = true; self._resetFetch(); } else { // unknown response } }); this._state.conn.on('error', function(err) { debug('Encountered error: ' + err); }); this._state.conn.on('end', function() { self._reset(); debug('FIN packet received. Disconnecting...'); self.emit('end'); }); this._state.conn.on('error', function(err) { self.emit('error', err); debug('Error occurred: ' + err); }); this._state.conn.on('close', function(had_error) { self._reset(); debug('Connection forcefully closed.'); self.emit('close', had_error); }); }; ImapConnection.prototype.isAuthenticated = function() { return this._state.status >= STATES.AUTH; }; ImapConnection.prototype.logout = function(cb) { if (this._state.status >= STATES.NOAUTH) this._send('LOGOUT', cb); else throw new Error('Not connected'); }; ImapConnection.prototype.openBox = function(name, readOnly, cb) { if (this._state.status < STATES.AUTH) throw new Error('Not connected or authenticated'); else if (typeof name !== 'string') name = 'INBOX'; if (this._state.status === STATES.BOXSELECTED) this._resetBox(); if (typeof readOnly !== 'boolean') readOnly = false; cb = arguments[arguments.length-1]; this._state.status = STATES.BOXSELECTING; this._state.box.name = name; this._send((readOnly ? 'EXAMINE' : 'SELECT') + ' "' + escape(name) + '"', cb); }; ImapConnection.prototype.closeBox = function(cb) { // also deletes any messages in this box marked with \Deleted var self = this; if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); this._send('CLOSE', function(err) { if (!err) { self._state.status = STATES.AUTH; self._resetBox(); } cb(err); }); }; ImapConnection.prototype.search = function(options, cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (!Array.isArray(options)) throw new Error('Expected array for search options'); var searchargs = '', months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; for (var i=0,len=options.length; i 1) args = criteria.slice(1); if (criteria.length > 0) criteria = criteria[0].toUpperCase(); } else throw new Error('Unexpected search option data type. Expected string, number, or array. Got: ' + typeof criteria); if (criteria[0] === '!') { modifier += 'NOT '; criteria = criteria.substr(1); } switch(criteria) { 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); searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '"'; 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'); } 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]); 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); searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '" "' + escape(''+args[1]) + '"'; break; default: throw new Error('Unexpected search option: ' + criteria); } } this._send('UID SEARCH' + searchargs, cb); }; ImapConnection.prototype.fetch = function(uid, options, cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (arguments.length < 1) throw new Error('The message ID must be specified'); if (isNaN(parseInt(''+uid))) throw new Error('Message ID must be a number'); var defaults = { markSeen: false, request: { struct: true, headers: true, // \_______ at most one of these can be used for any given fetch request body: false // / } }, toFetch, bodyRange = ''; cb = arguments[arguments.length-1]; if (typeof options !== 'object') options = {}; options = extend(true, defaults, options); if (!Array.isArray(options.request.headers)) { if (Array.isArray(options.request.body)) { var rangeInfo; if (options.request.body.length !== 2) throw new Error("Expected Array of length 2 for body property for byte range"); else if (typeof options.request.body[1] !== 'string' || !(rangeInfo = /^([\d]+)\-([\d]+)$/.exec(options.request.body[1])) || parseInt(rangeInfo[1]) >= parseInt(rangeInfo[2])) throw new Error("Invalid body byte range format"); bodyRange = '<' + parseInt(rangeInfo[1]) + '.' + parseInt(rangeInfo[2]) + '>'; options.request.body = options.request.body[0]; } if (typeof options.request.headers === 'boolean' && options.request.headers === true) toFetch = 'HEADER'; // fetches headers only else if (typeof options.request.body === 'boolean' && options.request.body === true) toFetch = 'TEXT'; // fetches the whole entire message text (minus the headers), including all message parts else if (typeof options.request.body === 'string') { if (!/^([\d]+[\.]{0,1})*[\d]+$/.test(options.request.body)) throw new Error("Invalid body partID format"); toFetch = options.request.body; // specific message part identifier, e.g. '1', '2', '1.1', '1.2', etc } } else toFetch = 'HEADER.FIELDS (' + options.request.headers.join(' ').toUpperCase() + ')'; // fetch specific headers only this._resetFetch(); this._send('UID FETCH ' + uid + ' (FLAGS INTERNALDATE' + (options.request.struct ? ' BODYSTRUCTURE' : '') + (toFetch ? ' BODY' + (!options.markSeen ? '.PEEK' : '') + '[' + toFetch + ']' + bodyRange : '') + ')', cb); }; ImapConnection.prototype.removeDeleted = function(cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); cb = arguments[arguments.length-1]; this._send('EXPUNGE', cb); }; ImapConnection.prototype.addFlags = function(uid, flags, cb) { try { this._storeFlag(uid, flags, true, cb); } catch (err) { throw err; } }; ImapConnection.prototype.delFlags = function(uid, flags, cb) { try { this._storeFlag(uid, flags, false, cb); } catch (err) { throw err; } }; /****** Private Functions ******/ ImapConnection.prototype._storeFlag = function(uid, flags, isAdding, cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (typeof uid === 'undefined') throw new Error('The message ID must be specified'); if (isNaN(parseInt(''+uid))) throw new Error('Message ID must be a number'); if ((!Array.isArray(flags) && typeof flags !== 'string') || (Array.isArray(flags) && flags.length === 0)) throw new Error('Flags argument must be a string or a non-empty Array'); if (!Array.isArray(flags)) flags = [flags]; for (var i=0; i= STATES.AUTH) this._send('NOOP', undefined, true); }; ImapConnection.prototype._send = function(cmdstr, cb, bypass) { if (arguments.length > 0 && !bypass) this._state.requests.push({ command: cmdstr, callback: cb }); if ((arguments.length === 0 && this._state.requests.length > 0) || this._state.requests.length === 1 || bypass) { clearTimeout(this._state.tmrKeepalive); this._state.isIdle = false; var cmd = (bypass ? cmdstr : this._state.requests[0].command); this._state.conn.write('A' + ++this._state.curId + ' ' + cmd + CRLF); debug('SENT: A' + this._state.curId + ' ' + cmd); } }; /****** Utility Functions ******/ function parseFetch(str, literalData, fetchData) { // passed in str === "... {xxxx}" or "... {xxxx} ..." or just "..." // where ... is any number of key-value pairs // and {xxxx} is the byte count for the literalData describing the preceding item (almost always "BODY") var key, idxNext, isNil; while (str.length > 0) { key = str.substring(0, str.indexOf(' ')); str = str.substring(str.indexOf(' ')+1); isNil = (str.substr(0, 3) === 'NIL'); if (isNil) idxNext = 3; else { switch (key) { case 'UID': idxNext = str.indexOf(' ')+1; break; case 'INTERNALDATE': idxNext = str.indexOf('"', 1)+1; fetchData.date = str.substring(1, idxNext-1); break; case 'FLAGS': idxNext = str.indexOf(')')+1; fetchData.flags = str.substring(1, idxNext-1).split(' ').filter(isNotEmpty); break; case 'BODYSTRUCTURE': var inQuote = false, countParen = 0, lastIndex = -1; for (var i=1,len=str.length; i)?$/.exec(key); idxNext = str.indexOf("}")+1; if (result[1].indexOf('HEADER') === 0) { // either full or selective headers var headers = literalData.split(/\r\n(?=[\w])/), header; fetchData.headers = {}; for (var i=0,len=headers.length; i 0) { if (str[0] === '(') { var isKey = true, key; str = str.substr(1); extensionData.params = {}; while (str[0] !== ')') { lastIndex = getLastIdxQuoted(str); if (isKey) key = str.substring(1, lastIndex).toLowerCase(); else extensionData.params[key] = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); isKey = !isKey; } str = str.substr(1).trim(); } else str = str.substr(4); // [disposition] if (str.length > 0) { if (str.substr(0, 3) !== 'NIL') { extensionData.disposition = { type: null, params: null }; str = str.substr(1); lastIndex = getLastIdxQuoted(str); extensionData.disposition.type = str.substring(1, lastIndex).toLowerCase(); str = str.substr(lastIndex+1).trim(); if (str[0] === '(') { var isKey = true, key; str = str.substr(1); extensionData.disposition.params = {}; while (str[0] !== ')') { lastIndex = getLastIdxQuoted(str); if (isKey) key = str.substring(1, lastIndex).toLowerCase(); else extensionData.disposition.params[key] = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); isKey = !isKey; } str = str.substr(2).trim(); } else str = str.substr(4).trim(); } else str = str.substr(4); // [language] if (str.length > 0) { if (str.substr(0, 3) !== 'NIL') { lastIndex = getLastIdxQuoted(str); extensionData.language = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); } else str = str.substr(4); // [location] if (str.length > 0) { if (str.substr(0, 3) !== 'NIL') { lastIndex = getLastIdxQuoted(str); extensionData.location = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); } else str = str.substr(4); } } } } retVal.unshift(extensionData); } else { // single part var part = { partID: (prefix !== '' ? prefix : '1'), // the path identifier for this part, useful for fetching specific parts of a message type: { name: null, params: null }, // content type and parameters (NIL or otherwise) id: null, description: null, encoding: null, size: null, lines: null, // required -- NIL or otherwise md5: null, disposition: null, language: null, location: null // optional extension data that may be omitted entirely }, lastIndex = getLastIdxQuoted(str), contentTypeMain = str.substring(1, lastIndex), contentTypeSub; str = str.substr(lastIndex+1).trim(); lastIndex = getLastIdxQuoted(str); contentTypeSub = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); // content type part.type.name = contentTypeMain.toLowerCase() + '/' + contentTypeSub.toLowerCase(); // content type parameters if (str[0] === '(') { var isKey = true, key; str = str.substr(1); part.type.params = {}; while (str[0] !== ')') { lastIndex = getLastIdxQuoted(str); if (isKey) key = str.substring(1, lastIndex).toLowerCase(); else part.type.params[key] = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); isKey = !isKey; } str = str.substr(2); } else str = str.substr(4); // content id if (str.substr(0, 3) !== 'NIL') { lastIndex = getLastIdxQuoted(str); part.id = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); } else str = str.substr(4); // content description if (str.substr(0, 3) !== 'NIL') { lastIndex = getLastIdxQuoted(str); part.description = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); } else str = str.substr(4); // content encoding if (str.substr(0, 3) !== 'NIL') { lastIndex = getLastIdxQuoted(str); part.encoding = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); } else str = str.substr(4); // size of content encoded in bytes if (str.substr(0, 3) !== 'NIL') { lastIndex = 0; while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57) lastIndex++; part.size = parseInt(str.substring(0, lastIndex)); str = str.substr(lastIndex).trim(); } else str = str.substr(4); // [# of lines] if (part.type.name.indexOf('text/') === 0) { if (str.substr(0, 3) !== 'NIL') { lastIndex = 0; while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57) lastIndex++; part.lines = parseInt(str.substring(0, lastIndex)); str = str.substr(lastIndex).trim(); } else str = str.substr(4); } // [md5 hash of content] if (str.length > 0) { if (str.substr(0, 3) !== 'NIL') { lastIndex = getLastIdxQuoted(str); part.md5 = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); } else str = str.substr(4); // [disposition] if (str.length > 0) { if (str.substr(0, 3) !== 'NIL') { part.disposition = { type: null, params: null }; str = str.substr(1); lastIndex = getLastIdxQuoted(str); part.disposition.type = str.substring(1, lastIndex).toLowerCase(); str = str.substr(lastIndex+1).trim(); if (str[0] === '(') { var isKey = true, key; str = str.substr(1); part.disposition.params = {}; while (str[0] !== ')') { lastIndex = getLastIdxQuoted(str); if (isKey) key = str.substring(1, lastIndex).toLowerCase(); else part.disposition.params[key] = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); isKey = !isKey; } str = str.substr(2).trim(); } else str = str.substr(4).trim(); } else str = str.substr(4); // [language] if (str.length > 0) { if (str.substr(0, 3) !== 'NIL') { if (str[0] === '(') { part.language = []; str = str.substr(1); while (str[0] !== ')') { lastIndex = getLastIdxQuoted(str); part.language.push(str.substring(1, lastIndex)); str = str.substr(lastIndex+1).trim(); } } else { lastIndex = getLastIdxQuoted(str); part.language = [str.substring(1, lastIndex)]; str = str.substr(lastIndex+1).trim(); } } else str = str.substr(4); // [location] if (str.length > 0) { if (str.substr(0, 3) !== 'NIL') { lastIndex = getLastIdxQuoted(str); part.location = str.substring(1, lastIndex); str = str.substr(lastIndex+1).trim(); } else str = str.substr(4); } } } } retVal.push(part); } return retVal; } String.prototype.explode = function(delimiter, limit) { if (arguments.length < 2 || arguments[0] === undefined || arguments[1] === undefined || !delimiter || delimiter === '' || typeof delimiter === 'function' || typeof delimiter === 'object') return false; delimiter = (delimiter === true ? '1' : delimiter.toString()); if (!limit || limit === 0) return this.split(delimiter); else if (limit < 0) return false; else if (limit > 0) { var splitted = this.split(delimiter); var partA = splitted.splice(0, limit - 1); var partB = splitted.join(delimiter); partA.push(partB); return partA; } return false; } function isNotEmpty(str) { return str.trim().length > 0; } function escape(str) { return str.replace('\\', '\\\\').replace('"', '\"'); } function unescape(str) { return str.replace('\"', '"').replace('\\\\', '\\'); } function up(str) { return str.toUpperCase(); } function getLastIdxQuoted(str) { var index = -1, countQuote = 0; for (var i=0,len=str.length; i 0 && str[i-1] === "\\") continue; countQuote++; } if (countQuote === 2) { index = i; break; } } return index; } /** * Adopted from jquery's extend method. Under the terms of MIT License. * * http://code.jquery.com/jquery-1.4.2.js * * Modified by Brian White to use Array.isArray instead of the custom isArray method */ function extend() { // copy reference to target object var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !typeof target === 'function') { target = {}; } var isPlainObject = function( obj ) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) return false; var has_own_constructor = hasOwnProperty.call(obj, "constructor"); var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf"); // Not own constructor property must be Object if ( obj.constructor && !has_own_constructor && !has_is_property_of_method) return false; // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var last_key; for ( key in obj ) last_key = key; return typeof last_key === "undefined" || hasOwnProperty.call( obj, last_key ); }; for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( (options = arguments[ i ]) !== null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) continue; // Recurse if we're merging object literal values or arrays if ( deep && copy && ( isPlainObject(copy) || Array.isArray(copy) ) ) { var clone = src && ( isPlainObject(src) || Array.isArray(src) ) ? src : Array.isArray(copy) ? [] : {}; // Never move original objects, clone them target[ name ] = extend( deep, clone, copy ); // Don't bring in undefined values } else if ( typeof copy !== "undefined" ) target[ name ] = copy; } } } // Return the modified object return target; };