define( [ 'logic', 'mix', '../jobmixins', '../drafts/jobs', 'activesync/codepages/AirSync', 'activesync/codepages/Email', 'activesync/codepages/Move', 'module', 'require', 'exports' ], function( logic, mix, $jobmixins, draftsJobs, $AirSync, $Email, $Move, $module, require, exports ) { 'use strict'; var $wbxml; function lazyConnection(cbIndex, fn, failString) { return function lazyRun() { var args = Array.slice(arguments), errback = args[cbIndex], self = this; require(['wbxml'], function (wbxml) { if (!$wbxml) { $wbxml = wbxml; } self.account.withConnection(errback, function () { fn.apply(self, args); }, failString); }); }; } function ActiveSyncJobDriver(account, state) { this.account = account; // XXX for simplicity for now, let's assume that ActiveSync GUID's are // maintained across folder moves. this.resilientServerIds = true; this._heldMutexReleasers = []; logic.defineScope(this, 'ActiveSyncJobDriver', { accountId: this.account.id }); this._state = state; // (we only need to use one as a proxy for initialization) if (!state.hasOwnProperty('suidToServerId')) { state.suidToServerId = {}; state.moveMap = {}; } this._stateDelta = { serverIdMap: null, moveMap: null, }; } exports.ActiveSyncJobDriver = ActiveSyncJobDriver; ActiveSyncJobDriver.prototype = { ////////////////////////////////////////////////////////////////////////////// // helpers postJobCleanup: $jobmixins.postJobCleanup, allJobsDone: $jobmixins.allJobsDone, _accessFolderForMutation: function(folderId, needConn, callback, deathback, label) { var storage = this.account.getFolderStorageForFolderId(folderId), self = this; storage.runMutexed(label, function(releaseMutex) { self._heldMutexReleasers.push(releaseMutex); var syncer = storage.folderSyncer; if (needConn) { self.account.withConnection(callback, function () { try { callback(syncer.folderConn, storage); } catch (ex) { logic(self, 'callbackErr', { ex: ex }); } }); } else { try { callback(syncer.folderConn, storage); } catch (ex) { logic(self, 'callbackErr', { ex: ex }); } } }); }, _partitionAndAccessFoldersSequentially: $jobmixins._partitionAndAccessFoldersSequentially, ////////////////////////////////////////////////////////////////////////////// // modtags local_do_modtags: $jobmixins.local_do_modtags, do_modtags: lazyConnection(1, function(op, jobDoneCallback, undo) { // Note: this method is derived from the IMAP implementation. var addTags = undo ? op.removeTags : op.addTags, removeTags = undo ? op.addTags : op.removeTags; function getMark(tag) { if (addTags && addTags.indexOf(tag) !== -1) return true; if (removeTags && removeTags.indexOf(tag) !== -1) return false; return undefined; } var markRead = getMark('\\Seen'); var markFlagged = getMark('\\Flagged'); var as = $AirSync.Tags; var em = $Email.Tags; var aggrErr = null; this._partitionAndAccessFoldersSequentially( op.messages, true, function perFolder(folderConn, storage, serverIds, namers, callWhenDone) { var modsToGo = 0; function tagsModded(err) { if (err) { console.error('failure modifying tags', err); aggrErr = 'unknown'; return; } op.progress += (undo ? -serverIds.length : serverIds.length); if (--modsToGo === 0) callWhenDone(); } // Filter out any offline headers, since the server naturally can't do // anything for them. If this means we have no headers at all, just bail // out. serverIds = serverIds.filter(function(srvid) { return !!srvid; }); if (!serverIds.length) { callWhenDone(); return; } folderConn.performMutation( function withWriter(w) { for (var i = 0; i < serverIds.length; i++) { w.stag(as.Change) .tag(as.ServerId, serverIds[i]) .stag(as.ApplicationData); if (markRead !== undefined) w.tag(em.Read, markRead ? '1' : '0'); if (markFlagged !== undefined) w.stag(em.Flag) .tag(em.Status, markFlagged ? '2' : '0') .etag(); w.etag(as.ApplicationData) .etag(as.Change); } }, function mutationPerformed(err) { if (err) aggrErr = err; callWhenDone(); }); }, function allDone() { jobDoneCallback(aggrErr); }, function deadConn() { aggrErr = 'aborted-retry'; }, /* reverse if we're undoing */ undo, 'modtags'); }), check_modtags: function(op, callback) { callback(null, 'idempotent'); }, local_undo_modtags: $jobmixins.local_undo_modtags, undo_modtags: function(op, callback) { this.do_modtags(op, callback, true); }, ////////////////////////////////////////////////////////////////////////////// // move local_do_move: $jobmixins.local_do_move, do_move: lazyConnection(1, function(op, jobDoneCallback) { /* * The ActiveSync command for this does not produce or consume SyncKeys. * As such, we don't need to acquire mutexes for the source folders for * synchronization correctness, although it is helpful for ordering * purposes and reducing confusion. * * For the target folder a similar logic exists as long as the server-issued * GUID's are resilient against folder moves. However, we do require in * all cases that before synchronizing the target folder that we make sure * all move operations to the folder have completed so we message doesn't * disappear and then show up again. XXX we are not currently enforcing this * yet. */ var aggrErr = null, account = this.account, targetFolderStorage = this.account.getFolderStorageForFolderId( op.targetFolder); var mo = $Move.Tags; this._partitionAndAccessFoldersSequentially( op.messages, true, function perFolder(folderConn, storage, serverIds, namers, callWhenDone) { // Filter out any offline headers, since the server naturally can't do // anything for them. If this means we have no headers at all, just bail // out. serverIds = serverIds.filter(function(srvid) { return !!srvid; }); if (!serverIds.length) { callWhenDone(); return; } // Filter out any offline headers, since the server naturally can't do // anything for them. If this means we have no headers at all, just bail // out. serverIds = serverIds.filter(function(srvid) { return !!srvid; }); if (!serverIds.length) { callWhenDone(); return; } var w = new $wbxml.Writer('1.3', 1, 'UTF-8'); w.stag(mo.MoveItems); for (var i = 0; i < serverIds.length; i++) { w.stag(mo.Move) .tag(mo.SrcMsgId, serverIds[i]) .tag(mo.SrcFldId, storage.folderMeta.serverId) .tag(mo.DstFldId, targetFolderStorage.folderMeta.serverId) .etag(mo.Move); } w.etag(mo.MoveItems); account.conn.postCommand(w, function(err, response) { if (err) { aggrErr = err; console.error('failure moving messages:', err); } callWhenDone(); }); }, function allDone() { jobDoneCallback(aggrErr, null, true); }, function deadConn() { aggrErr = 'aborted-retry'; }, false, 'move'); }), check_move: function(op, jobDoneCallback) { }, local_undo_move: $jobmixins.local_undo_move, undo_move: function(op, jobDoneCallback) { }, ////////////////////////////////////////////////////////////////////////////// // delete local_do_delete: $jobmixins.local_do_delete, do_delete: lazyConnection(1, function(op, jobDoneCallback) { var aggrErr = null; var as = $AirSync.Tags; var em = $Email.Tags; this._partitionAndAccessFoldersSequentially( op.messages, true, function perFolder(folderConn, storage, serverIds, namers, callWhenDone) { // Filter out any offline headers, since the server naturally can't do // anything for them. If this means we have no headers at all, just bail // out. serverIds = serverIds.filter(function(srvid) { return !!srvid; }); if (!serverIds.length) { callWhenDone(); return; } folderConn.performMutation( function withWriter(w) { for (var i = 0; i < serverIds.length; i++) { w.stag(as.Delete) .tag(as.ServerId, serverIds[i]) .etag(as.Delete); } }, function mutationPerformed(err) { if (err) { aggrErr = err; console.error('failure deleting messages:', err); } callWhenDone(); }); }, function allDone() { jobDoneCallback(aggrErr, null, true); }, function deadConn() { aggrErr = 'aborted-retry'; }, false, 'delete'); }), check_delete: function(op, callback) { callback(null, 'idempotent'); }, local_undo_delete: $jobmixins.local_undo_delete, // TODO implement undo_delete: function(op, callback) { callback('moot'); }, ////////////////////////////////////////////////////////////////////////////// // syncFolderList // // Synchronize our folder list. This should always be an idempotent operation // that makes no sense to undo/redo/etc. local_do_syncFolderList: function(op, doneCallback) { doneCallback(null); }, do_syncFolderList: lazyConnection(1, function(op, doneCallback) { var account = this.account, self = this; // The inbox needs to be resynchronized if there was no server id and we // have active slices displaying the contents of the folder. (No server id // means the sync will not happen.) var inboxFolder = account.getFirstFolderWithType('inbox'), inboxStorage; if (inboxFolder && inboxFolder.serverId === null) inboxStorage = account.getFolderStorageForFolderId(inboxFolder.id); account.syncFolderList(function(err) { if (!err) account.meta.lastFolderSyncAt = Date.now(); // save if it worked doneCallback(err ? 'aborted-retry' : null, null, !err); if (inboxStorage && inboxStorage.hasActiveSlices) { if (!err) { console.log("Refreshing fake inbox"); inboxStorage.resetAndRefreshActiveSlices(); } // XXX: If we do have an error here, we should probably report // syncfailed on the slices to let the user retry. However, what needs // retrying is syncFolderList, not syncing the messages in a folder. // Since that's complicated to handle, and syncFolderList will retry // automatically, we can ignore that case for now. } }); }, 'aborted-retry'), check_syncFolderList: function(op, doneCallback) { doneCallback(null, 'coherent-notyet'); }, local_undo_syncFolderList: function(op, doneCallback) { doneCallback('moot'); }, undo_syncFolderList: function(op, doneCallback) { doneCallback('moot'); }, ////////////////////////////////////////////////////////////////////////////// // downloadBodies: Download the bodies from a list of messages local_do_downloadBodies: $jobmixins.local_do_downloadBodies, do_downloadBodies: $jobmixins.do_downloadBodies, check_downloadBodies: $jobmixins.check_downloadBodies, ////////////////////////////////////////////////////////////////////////////// // downloadBodyReps: Download the bodies from a single message local_do_downloadBodyReps: $jobmixins.local_do_downloadBodyReps, do_downloadBodyReps: $jobmixins.do_downloadBodyReps, check_downloadBodyReps: $jobmixins.check_downloadBodyReps, ////////////////////////////////////////////////////////////////////////////// // download: Download one or more attachments from a single message local_do_download: $jobmixins.local_do_download, do_download: $jobmixins.do_download, check_download: $jobmixins.check_download, local_undo_download: $jobmixins.local_undo_download, undo_download: $jobmixins.undo_download, ////////////////////////////////////////////////////////////////////////////// local_do_sendOutboxMessages: $jobmixins.local_do_sendOutboxMessages, do_sendOutboxMessages: $jobmixins.do_sendOutboxMessages, check_sendOutboxMessages: $jobmixins.check_sendOutboxMessages, local_undo_sendOutboxMessages: $jobmixins.local_undo_sendOutboxMessages, undo_sendOutboxMessages: $jobmixins.undo_sendOutboxMessages, local_do_setOutboxSyncEnabled: $jobmixins.local_do_setOutboxSyncEnabled, // upgrade: perfom necessary upgrades when the db version changes local_do_upgradeDB: $jobmixins.local_do_upgradeDB, ////////////////////////////////////////////////////////////////////////////// // purgeExcessMessages is a NOP for activesync local_do_purgeExcessMessages: function(op, doneCallback) { doneCallback(null); }, do_purgeExcessMessages: function(op, doneCallback) { doneCallback(null); }, check_purgeExcessMessages: function(op, doneCallback) { return 'idempotent'; }, local_undo_purgeExcessMessages: function(op, doneCallback) { doneCallback(null); }, undo_purgeExcessMessages: function(op, doneCallback) { doneCallback(null); }, ////////////////////////////////////////////////////////////////////////////// }; mix(ActiveSyncJobDriver.prototype, draftsJobs.draftsMixins); }); // end define