From 961a82c416b2b4221c4fbac6da87e334695853bc Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 11 Mar 2014 18:57:03 +0100 Subject: [PATCH] [WO-267] move sync code into its own module --- src/js/app-controller.js | 6 +- src/js/dao/email-dao.js | 820 +------------ src/js/dao/email-sync.js | 811 +++++++++++++ test/new-unit/app-controller-test.js | 4 + test/new-unit/email-dao-test.js | 1483 +---------------------- test/new-unit/email-sync-test.js | 1614 ++++++++++++++++++++++++++ test/new-unit/main.js | 1 + 7 files changed, 2507 insertions(+), 2232 deletions(-) create mode 100644 src/js/dao/email-sync.js create mode 100644 test/new-unit/email-sync-test.js diff --git a/src/js/app-controller.js b/src/js/app-controller.js index aac37f3..27cc70c 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -8,6 +8,7 @@ define(function(require) { mailreader = require('mailreader'), PgpMailer = require('pgpmailer'), EmailDAO = require('js/dao/email-dao'), + EmailSync = require('js/dao/email-sync'), RestDAO = require('js/dao/rest-dao'), PublicKeyDAO = require('js/dao/publickey-dao'), LawnchairDAO = require('js/dao/lawnchair-dao'), @@ -335,7 +336,7 @@ define(function(require) { }; self.buildModules = function() { - var lawnchairDao, restDao, pubkeyDao, emailDao, keychain, pgp, userStorage, pgpbuilder; + var lawnchairDao, restDao, pubkeyDao, emailDao, emailSync, keychain, pgp, userStorage, pgpbuilder; // start the mailreader's worker thread mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); @@ -350,7 +351,8 @@ define(function(require) { self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao); self._crypto = pgp = new PGP(); self._pgpbuilder = pgpbuilder = new PgpBuilder(); - self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); + emailSync = new EmailSync(keychain, userStorage); + self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync); self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); self._updateHandler = new UpdateHandler(self._appConfigStore, userStorage); }; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index d42bef6..ce706fa 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -3,15 +3,15 @@ define(function(require) { var util = require('cryptoLib/util'), _ = require('underscore'), - str = require('js/app-config').string, - config = require('js/app-config').config; + str = require('js/app-config').string; - var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader) { + var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader, emailSync) { this._keychain = keychain; this._crypto = crypto; this._devicestorage = devicestorage; this._pgpbuilder = pgpbuilder; this._mailreader = mailreader; + this._emailSync = emailSync; }; // @@ -47,6 +47,19 @@ define(function(require) { } keypair = storedKeypair; + initEmailSync(); + }); + } + + function initEmailSync() { + self._emailSync.init({ + account: self._account + }, function(err) { + if (err) { + callback(err); + return; + } + initFolders(); }); } @@ -79,8 +92,20 @@ define(function(require) { } }; - // connect to newly created imap client - self._imapLogin(function(err) { + // notify emailSync + self._emailSync.onConnect({ + imapClient: self._imapClient + }, function(err) { + if (err) { + callback(err); + return; + } + + // connect to newly created imap client + self._imapLogin(onLogin); + }); + + function onLogin(err) { if (err) { callback(err); return; @@ -104,18 +129,20 @@ define(function(require) { } self._account.folders = folders; + callback(); }); - }); + } }; EmailDAO.prototype.onDisconnect = function(options, callback) { // set status to online this._account.online = false; this._imapClient = undefined; - self._pgpMailer = undefined; + this._pgpMailer = undefined; - callback(); + // notify emailSync + this._emailSync.onDisconnect(null, callback); }; EmailDAO.prototype.unlock = function(options, callback) { @@ -218,638 +245,12 @@ define(function(require) { } }; - /** - * Syncs outbox content from disk to memory, not vice-versa - */ EmailDAO.prototype.syncOutbox = function(options, callback) { - var self = this; - - // check busy status - if (self._account.busy) { - callback({ - errMsg: 'Sync aborted: Previous sync still in progress', - code: 409 - }); - return; - } - - // make sure two syncs for the same folder don't interfere - self._account.busy = true; - - var folder = _.findWhere(self._account.folders, { - path: options.folder - }); - - folder.messages = folder.messages || []; - - self._localListMessages({ - folder: folder.path - }, function(err, storedMessages) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // calculate the diffs between memory and disk - var storedIds = _.pluck(storedMessages, 'id'), - inMemoryIds = _.pluck(folder.messages, 'id'), - newIds = _.difference(storedIds, inMemoryIds), - removedIds = _.difference(inMemoryIds, storedIds); - - // which messages are new on the disk that are not yet in memory? - var newMessages = _.filter(storedMessages, function(msg) { - return _.contains(newIds, msg.id); - }); - - // which messages are no longer on disk, i.e. have been sent - var removedMessages = _.filter(folder.messages, function(msg) { - return _.contains(removedIds, msg.id); - }); - - // add the new messages to memory - newMessages.forEach(function(newMessage) { - folder.messages.push(newMessage); - }); - - // remove the sent messages from memory - removedMessages.forEach(function(removedMessage) { - var index = folder.messages.indexOf(removedMessage); - folder.messages.splice(index, 1); - }); - - // update the folder count and we're done. - folder.count = folder.messages.length; - self._account.busy = false; - - callback(); - }); + this._emailSync.syncOutbox(options, callback); }; EmailDAO.prototype.sync = function(options, callback) { - /* - * Here's how delta sync works: - * - * First, we sync the messages between memory and local storage, based on their uid - * delta1: storage > memory => we deleted messages, remove from remote and memory - * delta2: memory > storage => we added messages, push to remote <<< not supported yet - * - * Second, we check the delta for the flags - * deltaF2: memory > storage => we changed flags, sync them to the remote and memory - * - * Third, we go on to sync between imap and memory, again based on uid - * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage - * delta4: imap > memory => we have new messages available, fetch to memory and storage - * - * Fourth, we pull changes in the flags downstream - * deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory - */ - - var self = this; - - // validate options - if (!options.folder) { - callback({ - errMsg: 'Invalid options!' - }); - return; - } - - // check busy status - if (self._account.busy) { - callback({ - errMsg: 'Sync aborted: Previous sync still in progress', - code: 409 - }); - return; - } - - // make sure two syncs for the same folder don't interfere - self._account.busy = true; - - var folder = _.findWhere(self._account.folders, { - path: options.folder - }); - - /* - * if the folder is not initialized with the messages from the memory, we need to fill it first, otherwise the delta sync obviously breaks. - * initial filling from local storage is an exception from the normal sync. after reading from local storage, do imap sync - */ - var isFolderInitialized = !! folder.messages; - if (!isFolderInitialized) { - initFolderMessages(); - return; - } - - doLocalDelta(); - - /* - * pre-fill the memory with the messages stored on the hard disk - */ - function initFolderMessages() { - folder.messages = []; - self._localListMessages({ - folder: folder.path - }, function(err, storedMessages) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - storedMessages.forEach(function(storedMessage) { - // remove the body to not load unnecessary data to memory - delete storedMessage.body; - - folder.messages.push(storedMessage); - }); - - callback(); - doImapDelta(); - }); - } - - /* - * compares the messages in memory to the messages on the disk - */ - function doLocalDelta() { - self._localListMessages({ - folder: folder.path - }, function(err, storedMessages) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - doDelta1(); - - /* - * delta1: - * storage contains messages that are not present in memory => we deleted messages from the memory, so remove the messages from the remote and the disk - */ - function doDelta1() { - var inMemoryUids = _.pluck(folder.messages, 'uid'), - storedMessageUids = _.pluck(storedMessages, 'uid'), - delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids - - // if we're we are done here - if (_.isEmpty(delta1)) { - doDeltaF2(); - return; - } - - var after = _.after(delta1.length, function() { - doDeltaF2(); - }); - - // delta1 contains uids of messages on the disk - delta1.forEach(function(inMemoryUid) { - var deleteMe = { - folder: folder.path, - uid: inMemoryUid - }; - - self._imapDeleteMessage(deleteMe, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - self._localDeleteMessage(deleteMe, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - after(); - }); - }); - }); - } - - /* - * deltaF2: - * memory contains messages that have flags other than those in storage => we changed flags, sync them to the remote and memory - */ - function doDeltaF2() { - var deltaF2 = checkFlags(folder.messages, storedMessages); // deltaF2 contains the message objects, we need those to sync the flags - - if (_.isEmpty(deltaF2)) { - callback(); - doImapDelta(); - return; - } - - var after = _.after(deltaF2.length, function() { - callback(); - doImapDelta(); - }); - - // deltaF2 contains references to the in-memory messages - deltaF2.forEach(function(inMemoryMessage) { - self._imapMark({ - folder: folder.path, - uid: inMemoryMessage.uid, - unread: inMemoryMessage.unread, - answered: inMemoryMessage.answered - }, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - var storedMessage = _.findWhere(storedMessages, { - uid: inMemoryMessage.uid - }); - - storedMessage.unread = inMemoryMessage.unread; - storedMessage.answered = inMemoryMessage.answered; - - self._localStoreMessages({ - folder: folder.path, - emails: [storedMessage] - }, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - after(); - }); - }); - }); - } - }); - } - - /* - * compare the messages on the imap server to the in memory messages - */ - function doImapDelta() { - self._imapSearch({ - folder: folder.path - }, function(err, inImapUids) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - doDelta3(); - - /* - * delta3: - * memory contains messages that are not present on the imap => we deleted messages directly from the remote, remove from memory and storage - */ - function doDelta3() { - var inMemoryUids = _.pluck(folder.messages, 'uid'), - delta3 = _.difference(inMemoryUids, inImapUids); - - if (_.isEmpty(delta3)) { - doDelta4(); - return; - } - - var after = _.after(delta3.length, function() { - doDelta4(); - }); - - // delta3 contains uids of the in-memory messages that have been deleted from the remote - delta3.forEach(function(inMemoryUid) { - // remove from local storage - self._localDeleteMessage({ - folder: folder.path, - uid: inMemoryUid - }, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // remove from memory - var inMemoryMessage = _.findWhere(folder.messages, function(msg) { - return msg.uid === inMemoryUid; - }); - folder.messages.splice(folder.messages.indexOf(inMemoryMessage), 1); - - after(); - }); - }); - } - - /* - * delta4: - * imap contains messages that are not present in memory => we have new messages available, fetch downstream to memory and storage - */ - function doDelta4() { - var inMemoryUids = _.pluck(folder.messages, 'uid'), - delta4 = _.difference(inImapUids, inMemoryUids); - - // eliminate uids smaller than the biggest local uid, i.e. just fetch everything - // that came in AFTER the most recent email we have in memory. Keep in mind that - // uids are strictly ascending, so there can't be a NEW mail in the mailbox with a - // uid smaller than anything we've encountered before. - if (!_.isEmpty(inMemoryUids)) { - var maxInMemoryUid = Math.max.apply(null, inMemoryUids); // apply works with separate arguments rather than an array - - // eliminate everything prior to maxInMemoryUid, i.e. everything that was already synced - delta4 = _.filter(delta4, function(uid) { - return uid > maxInMemoryUid; - }); - } - - // no delta, we're done here - if (_.isEmpty(delta4)) { - doDeltaF4(); - return; - } - - // list the messages starting from the lowest new uid to the highest new uid - self._imapListMessages({ - folder: folder.path, - firstUid: Math.min.apply(null, delta4), - lastUid: Math.max.apply(null, delta4) - }, function(err, messages) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // if there are verification messages in the synced messages, handle it - var verificationMessages = _.filter(messages, function(message) { - return message.subject === (str.subjectPrefix + str.verificationSubject); - }); - - // if there are verification messages, continue after we've tried to verify - if (verificationMessages.length > 0) { - var after = _.after(verificationMessages.length, storeHeaders); - - verificationMessages.forEach(function(verificationMessage) { - handleVerification(verificationMessage, function(err, isValid) { - // if it was NOT a valid verification mail, do nothing - if (!isValid) { - after(); - return; - } - - // if an error occurred and the mail was a valid verification mail, display the error, but - // keep the mail in the list so the user can see it and verify manually - if (err) { - callback(err); - after(); - return; - } - - // if verification worked, we remove the mail from the list. - messages.splice(messages.indexOf(verificationMessage), 1); - after(); - }); - }); - return; - } - - // no verification messages, just proceed as usual - storeHeaders(); - - function storeHeaders() { - // no delta, we're done here - if (_.isEmpty(messages)) { - doDeltaF4(); - return; - } - - // persist the encrypted message to the local storage - self._localStoreMessages({ - folder: folder.path, - emails: messages - }, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // if persisting worked, add them to the messages array - folder.messages = folder.messages.concat(messages); - doDeltaF4(); - }); - } - }); - } - }); - - /** - * deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory - */ - function doDeltaF4() { - var answeredUids, unreadUids, - deltaF4 = []; - - getUnreadUids(); - - // find all the relevant unread mails - function getUnreadUids() { - self._imapSearch({ - folder: folder.path, - unread: true - }, function(err, uids) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // we're done here, let's get all the answered mails - unreadUids = uids; - getAnsweredUids(); - }); - } - - // find all the relevant answered mails - function getAnsweredUids() { - // find all the relevant answered mails - self._imapSearch({ - folder: folder.path, - answered: true - }, function(err, uids) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // we're done here, let's update what we have in memory and persist that! - answeredUids = uids; - updateFlags(); - }); - - } - - function updateFlags() { - folder.messages.forEach(function(msg) { - // if the message's uid is among the uids that should be unread, - // AND the message is not unread, we clearly have to change that - var shouldBeUnread = _.contains(unreadUids, msg.uid); - if (msg.unread === shouldBeUnread) { - // everything is in order, we're good here - return; - } - - msg.unread = shouldBeUnread; - deltaF4.push(msg); - }); - - folder.messages.forEach(function(msg) { - // if the message's uid is among the uids that should be answered, - // AND the message is not answered, we clearly have to change that - var shouldBeAnswered = _.contains(answeredUids, msg.uid); - if (msg.answered === shouldBeAnswered) { - // everything is in order, we're good here - return; - } - - msg.answered = shouldBeAnswered; - deltaF4.push(msg); - }); - - // maybe a mail had BOTH flags wrong, so let's create - // a duplicate-free version of deltaF4 - deltaF4 = _.uniq(deltaF4); - - // everything up to date? fine, we're done! - if (_.isEmpty(deltaF4)) { - finishSync(); - return; - } - - var after = _.after(deltaF4.length, function() { - // we're doing updating everything - finishSync(); - }); - - // alright, so let's sync the corrected messages - deltaF4.forEach(function(inMemoryMessage) { - // do a short round trip to the database to avoid re-encrypting, - // instead use the encrypted object in the storage - self._localListMessages({ - folder: folder.path, - uid: inMemoryMessage.uid - }, function(err, storedMessages) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - var storedMessage = storedMessages[0]; - storedMessage.unread = inMemoryMessage.unread; - storedMessage.answered = inMemoryMessage.answered; - - // persist the modified object - self._localStoreMessages({ - folder: folder.path, - emails: [storedMessage] - }, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // and we're done. - after(); - }); - }); - - }); - } - } - } - - function finishSync() { - // whereas normal folders show the unread messages count only, - // the outbox shows the total count - // after all the tags are up to date, let's adjust the unread mail count - folder.count = _.filter(folder.messages, function(msg) { - return msg.unread === true; - }).length; - - // allow the next sync to take place - self._account.busy = false; - callback(); - } - - /* - * checks if there are some flags that have changed in a and b - */ - function checkFlags(a, b) { - var i, aI, bI, - delta = []; - - // find the delta - for (i = a.length - 1; i >= 0; i--) { - aI = a[i]; - bI = _.findWhere(b, { - uid: aI.uid - }); - if (bI && (aI.unread !== bI.unread || aI.answered !== bI.answered)) { - delta.push(aI); - } - } - - return delta; - } - - function handleVerification(message, localCallback) { - self._imapStreamText({ - folder: options.folder, - message: message - }, function(error) { - // we could not stream the text to determine if the verification was valid or not - // so handle it as if it were valid - if (error) { - localCallback(error, true); - return; - } - - var verificationUrlPrefix = config.cloudUrl + config.verificationUrl, - uuid = message.body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength), - isValidUuid = new RegExp('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}').test(uuid); - - // there's no valid uuid in the message, so forget about it - if (!isValidUuid) { - localCallback(null, false); - return; - } - - // there's a valid uuid in the message, so try to verify it - self._keychain.verifyPublicKey(uuid, function(err) { - if (err) { - localCallback({ - errMsg: 'Verifying your public key failed: ' + err.errMsg - }, true); - return; - } - - // public key has been verified, delete the message - self._imapDeleteMessage({ - folder: options.folder, - uid: message.uid - }, function() { - // if we could successfully not delete the message or not doesn't matter. - // just don't show it in whiteout and keep quiet about it - localCallback(null, true); - }); - }); - }); - } + this._emailSync.sync(options, callback); }; /** @@ -879,7 +280,7 @@ define(function(require) { // if possible, read the message body from the device function readFromDevice() { - self._localListMessages({ + self._emailSync._localListMessages({ folder: folder, uid: message.uid }, function(err, localMessages) { @@ -907,7 +308,7 @@ define(function(require) { // if reading the message body from the device was unsuccessful, // stream the message from the imap server function streamFromImap() { - self._imapStreamText({ + self._emailSync._imapStreamText({ folder: folder, message: message }, function(error) { @@ -921,7 +322,7 @@ define(function(require) { // do not write the object from the object used by angular to the disk, instead // do a short round trip and write back the unpolluted object - self._localListMessages({ + self._emailSync._localListMessages({ folder: folder, uid: message.uid }, function(error, storedMessages) { @@ -932,7 +333,7 @@ define(function(require) { storedMessages[0].body = message.body; - self._localStoreMessages({ + self._emailSync._localStoreMessages({ folder: folder, emails: storedMessages }, function(error) { @@ -1097,54 +498,8 @@ define(function(require) { // Internal API // - // Local Storage API - - EmailDAO.prototype._localListMessages = function(options, callback) { - var dbType = 'email_' + options.folder; - if (typeof options.uid !== 'undefined') { - dbType = dbType + '_' + options.uid; - } - this._devicestorage.listItems(dbType, 0, null, callback); - }; - - EmailDAO.prototype._localStoreMessages = function(options, callback) { - var dbType = 'email_' + options.folder; - this._devicestorage.storeList(options.emails, dbType, callback); - }; - - EmailDAO.prototype._localDeleteMessage = function(options, callback) { - if (!options.folder || !options.uid) { - callback({ - errMsg: 'Invalid options!' - }); - return; - } - var dbType = 'email_' + options.folder + '_' + options.uid; - this._devicestorage.removeList(dbType, callback); - }; - // IMAP API - /** - * Mark imap messages as un-/read or un-/answered - */ - EmailDAO.prototype._imapMark = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - this._imapClient.updateFlags({ - path: options.folder, - uid: options.uid, - unread: options.unread, - answered: options.answered - }, callback); - }; - /** * Login the imap client */ @@ -1176,103 +531,10 @@ define(function(require) { this._imapClient.logout(callback); }; - /** - * Returns the relevant messages corresponding to the search terms in the options - * @param {String} options.folder The folder's path - * @param {Boolean} options.answered (optional) Mails with or without the \Answered flag set. - * @param {Boolean} options.unread (optional) Mails with or without the \Seen flag set. - * @param {Function} callback(error, uids) invoked with the uids of messages matching the search terms, or an error object if an error occurred - */ - EmailDAO.prototype._imapSearch = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - var o = { - path: options.folder - }; - - if (typeof options.answered !== 'undefined') { - o.answered = options.answered; - } - if (typeof options.unread !== 'undefined') { - o.unread = options.unread; - } - - this._imapClient.search(o, callback); - }; - - EmailDAO.prototype._imapDeleteMessage = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - this._imapClient.deleteMessage({ - path: options.folder, - uid: options.uid - }, callback); - }; - EmailDAO.prototype._imapParseMessageBlock = function(options, callback) { this._mailreader.parseRfc(options, callback); }; - /** - * Get an email messsage without the body - * @param {String} options.folder The folder - * @param {Number} options.firstUid The lower bound of the uid (inclusive) - * @param {Number} options.lastUid The upper bound of the uid range (inclusive) - * @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata - */ - EmailDAO.prototype._imapListMessages = function(options, callback) { - var self = this; - - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - self._imapClient.listMessagesByUid({ - path: options.folder, - firstUid: options.firstUid, - lastUid: options.lastUid - }, callback); - }; - - /** - * Stream an email messsage's body - * @param {String} options.folder The folder - * @param {Object} options.message The message, as retrieved by _imapListMessages - * @param {Function} callback (error, message) The callback when the imap client is done streaming message text content - */ - EmailDAO.prototype._imapStreamText = function(options, callback) { - var self = this; - - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - self._imapClient.getBody({ - path: options.folder, - message: options.message - }, callback); - }; - /** * List the folders in the user's IMAP mailbox. */ diff --git a/src/js/dao/email-sync.js b/src/js/dao/email-sync.js new file mode 100644 index 0000000..fc9c29a --- /dev/null +++ b/src/js/dao/email-sync.js @@ -0,0 +1,811 @@ +define(function(require) { + 'use strict'; + + var _ = require('underscore'), + config = require('js/app-config').config, + str = require('js/app-config').string; + + var EmailSync = function(keychain, devicestorage) { + this._keychain = keychain; + this._devicestorage = devicestorage; + }; + + EmailSync.prototype.init = function(options, callback) { + this._account = options.account; + + callback(); + }; + + EmailSync.prototype.onConnect = function(options, callback) { + this._imapClient = options.imapClient; + + callback(); + }; + + EmailSync.prototype.onDisconnect = function(options, callback) { + this._imapClient = undefined; + + callback(); + }; + + /** + * Syncs outbox content from disk to memory, not vice-versa + */ + EmailSync.prototype.syncOutbox = function(options, callback) { + var self = this; + + // check busy status + if (self._account.busy) { + callback({ + errMsg: 'Sync aborted: Previous sync still in progress', + code: 409 + }); + return; + } + + // make sure two syncs for the same folder don't interfere + self._account.busy = true; + + var folder = _.findWhere(self._account.folders, { + path: options.folder + }); + + folder.messages = folder.messages || []; + + self._localListMessages({ + folder: folder.path + }, function(err, storedMessages) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // calculate the diffs between memory and disk + var storedIds = _.pluck(storedMessages, 'id'), + inMemoryIds = _.pluck(folder.messages, 'id'), + newIds = _.difference(storedIds, inMemoryIds), + removedIds = _.difference(inMemoryIds, storedIds); + + // which messages are new on the disk that are not yet in memory? + var newMessages = _.filter(storedMessages, function(msg) { + return _.contains(newIds, msg.id); + }); + + // which messages are no longer on disk, i.e. have been sent + var removedMessages = _.filter(folder.messages, function(msg) { + return _.contains(removedIds, msg.id); + }); + + // add the new messages to memory + newMessages.forEach(function(newMessage) { + folder.messages.push(newMessage); + }); + + // remove the sent messages from memory + removedMessages.forEach(function(removedMessage) { + var index = folder.messages.indexOf(removedMessage); + folder.messages.splice(index, 1); + }); + + // update the folder count and we're done. + folder.count = folder.messages.length; + self._account.busy = false; + + callback(); + }); + }; + + EmailSync.prototype.sync = function(options, callback) { + /* + * Here's how delta sync works: + * + * First, we sync the messages between memory and local storage, based on their uid + * delta1: storage > memory => we deleted messages, remove from remote and memory + * delta2: memory > storage => we added messages, push to remote <<< not supported yet + * + * Second, we check the delta for the flags + * deltaF2: memory > storage => we changed flags, sync them to the remote and memory + * + * Third, we go on to sync between imap and memory, again based on uid + * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage + * delta4: imap > memory => we have new messages available, fetch to memory and storage + * + * Fourth, we pull changes in the flags downstream + * deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory + */ + + var self = this; + + // validate options + if (!options.folder) { + callback({ + errMsg: 'Invalid options!' + }); + return; + } + + // check busy status + if (self._account.busy) { + callback({ + errMsg: 'Sync aborted: Previous sync still in progress', + code: 409 + }); + return; + } + + // make sure two syncs for the same folder don't interfere + self._account.busy = true; + + var folder = _.findWhere(self._account.folders, { + path: options.folder + }); + + /* + * if the folder is not initialized with the messages from the memory, we need to fill it first, otherwise the delta sync obviously breaks. + * initial filling from local storage is an exception from the normal sync. after reading from local storage, do imap sync + */ + var isFolderInitialized = !! folder.messages; + if (!isFolderInitialized) { + initFolderMessages(); + return; + } + + doLocalDelta(); + + /* + * pre-fill the memory with the messages stored on the hard disk + */ + function initFolderMessages() { + folder.messages = []; + self._localListMessages({ + folder: folder.path + }, function(err, storedMessages) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + storedMessages.forEach(function(storedMessage) { + // remove the body to not load unnecessary data to memory + delete storedMessage.body; + + folder.messages.push(storedMessage); + }); + + callback(); + doImapDelta(); + }); + } + + /* + * compares the messages in memory to the messages on the disk + */ + function doLocalDelta() { + self._localListMessages({ + folder: folder.path + }, function(err, storedMessages) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + doDelta1(); + + /* + * delta1: + * storage contains messages that are not present in memory => we deleted messages from the memory, so remove the messages from the remote and the disk + */ + function doDelta1() { + var inMemoryUids = _.pluck(folder.messages, 'uid'), + storedMessageUids = _.pluck(storedMessages, 'uid'), + delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids + + // if we're we are done here + if (_.isEmpty(delta1)) { + doDeltaF2(); + return; + } + + var after = _.after(delta1.length, function() { + doDeltaF2(); + }); + + // delta1 contains uids of messages on the disk + delta1.forEach(function(inMemoryUid) { + var deleteMe = { + folder: folder.path, + uid: inMemoryUid + }; + + self._imapDeleteMessage(deleteMe, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + self._localDeleteMessage(deleteMe, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + after(); + }); + }); + }); + } + + /* + * deltaF2: + * memory contains messages that have flags other than those in storage => we changed flags, sync them to the remote and memory + */ + function doDeltaF2() { + var deltaF2 = checkFlags(folder.messages, storedMessages); // deltaF2 contains the message objects, we need those to sync the flags + + if (_.isEmpty(deltaF2)) { + callback(); + doImapDelta(); + return; + } + + var after = _.after(deltaF2.length, function() { + callback(); + doImapDelta(); + }); + + // deltaF2 contains references to the in-memory messages + deltaF2.forEach(function(inMemoryMessage) { + self._imapMark({ + folder: folder.path, + uid: inMemoryMessage.uid, + unread: inMemoryMessage.unread, + answered: inMemoryMessage.answered + }, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + var storedMessage = _.findWhere(storedMessages, { + uid: inMemoryMessage.uid + }); + + storedMessage.unread = inMemoryMessage.unread; + storedMessage.answered = inMemoryMessage.answered; + + self._localStoreMessages({ + folder: folder.path, + emails: [storedMessage] + }, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + after(); + }); + }); + }); + } + }); + } + + /* + * compare the messages on the imap server to the in memory messages + */ + function doImapDelta() { + self._imapSearch({ + folder: folder.path + }, function(err, inImapUids) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + doDelta3(); + + /* + * delta3: + * memory contains messages that are not present on the imap => we deleted messages directly from the remote, remove from memory and storage + */ + function doDelta3() { + var inMemoryUids = _.pluck(folder.messages, 'uid'), + delta3 = _.difference(inMemoryUids, inImapUids); + + if (_.isEmpty(delta3)) { + doDelta4(); + return; + } + + var after = _.after(delta3.length, function() { + doDelta4(); + }); + + // delta3 contains uids of the in-memory messages that have been deleted from the remote + delta3.forEach(function(inMemoryUid) { + // remove from local storage + self._localDeleteMessage({ + folder: folder.path, + uid: inMemoryUid + }, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // remove from memory + var inMemoryMessage = _.findWhere(folder.messages, function(msg) { + return msg.uid === inMemoryUid; + }); + folder.messages.splice(folder.messages.indexOf(inMemoryMessage), 1); + + after(); + }); + }); + } + + /* + * delta4: + * imap contains messages that are not present in memory => we have new messages available, fetch downstream to memory and storage + */ + function doDelta4() { + var inMemoryUids = _.pluck(folder.messages, 'uid'), + delta4 = _.difference(inImapUids, inMemoryUids); + + // eliminate uids smaller than the biggest local uid, i.e. just fetch everything + // that came in AFTER the most recent email we have in memory. Keep in mind that + // uids are strictly ascending, so there can't be a NEW mail in the mailbox with a + // uid smaller than anything we've encountered before. + if (!_.isEmpty(inMemoryUids)) { + var maxInMemoryUid = Math.max.apply(null, inMemoryUids); // apply works with separate arguments rather than an array + + // eliminate everything prior to maxInMemoryUid, i.e. everything that was already synced + delta4 = _.filter(delta4, function(uid) { + return uid > maxInMemoryUid; + }); + } + + // no delta, we're done here + if (_.isEmpty(delta4)) { + doDeltaF4(); + return; + } + + // list the messages starting from the lowest new uid to the highest new uid + self._imapListMessages({ + folder: folder.path, + firstUid: Math.min.apply(null, delta4), + lastUid: Math.max.apply(null, delta4) + }, function(err, messages) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // if there are verification messages in the synced messages, handle it + var verificationMessages = _.filter(messages, function(message) { + return message.subject === (str.subjectPrefix + str.verificationSubject); + }); + + // if there are verification messages, continue after we've tried to verify + if (verificationMessages.length > 0) { + var after = _.after(verificationMessages.length, storeHeaders); + + verificationMessages.forEach(function(verificationMessage) { + handleVerification(verificationMessage, function(err, isValid) { + // if it was NOT a valid verification mail, do nothing + if (!isValid) { + after(); + return; + } + + // if an error occurred and the mail was a valid verification mail, display the error, but + // keep the mail in the list so the user can see it and verify manually + if (err) { + callback(err); + after(); + return; + } + + // if verification worked, we remove the mail from the list. + messages.splice(messages.indexOf(verificationMessage), 1); + after(); + }); + }); + return; + } + + // no verification messages, just proceed as usual + storeHeaders(); + + function storeHeaders() { + // no delta, we're done here + if (_.isEmpty(messages)) { + doDeltaF4(); + return; + } + + // persist the encrypted message to the local storage + self._localStoreMessages({ + folder: folder.path, + emails: messages + }, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // if persisting worked, add them to the messages array + folder.messages = folder.messages.concat(messages); + doDeltaF4(); + }); + } + }); + } + }); + + /** + * deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory + */ + function doDeltaF4() { + var answeredUids, unreadUids, + deltaF4 = []; + + getUnreadUids(); + + // find all the relevant unread mails + function getUnreadUids() { + self._imapSearch({ + folder: folder.path, + unread: true + }, function(err, uids) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // we're done here, let's get all the answered mails + unreadUids = uids; + getAnsweredUids(); + }); + } + + // find all the relevant answered mails + function getAnsweredUids() { + // find all the relevant answered mails + self._imapSearch({ + folder: folder.path, + answered: true + }, function(err, uids) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // we're done here, let's update what we have in memory and persist that! + answeredUids = uids; + updateFlags(); + }); + + } + + function updateFlags() { + folder.messages.forEach(function(msg) { + // if the message's uid is among the uids that should be unread, + // AND the message is not unread, we clearly have to change that + var shouldBeUnread = _.contains(unreadUids, msg.uid); + if (msg.unread === shouldBeUnread) { + // everything is in order, we're good here + return; + } + + msg.unread = shouldBeUnread; + deltaF4.push(msg); + }); + + folder.messages.forEach(function(msg) { + // if the message's uid is among the uids that should be answered, + // AND the message is not answered, we clearly have to change that + var shouldBeAnswered = _.contains(answeredUids, msg.uid); + if (msg.answered === shouldBeAnswered) { + // everything is in order, we're good here + return; + } + + msg.answered = shouldBeAnswered; + deltaF4.push(msg); + }); + + // maybe a mail had BOTH flags wrong, so let's create + // a duplicate-free version of deltaF4 + deltaF4 = _.uniq(deltaF4); + + // everything up to date? fine, we're done! + if (_.isEmpty(deltaF4)) { + finishSync(); + return; + } + + var after = _.after(deltaF4.length, function() { + // we're doing updating everything + finishSync(); + }); + + // alright, so let's sync the corrected messages + deltaF4.forEach(function(inMemoryMessage) { + // do a short round trip to the database to avoid re-encrypting, + // instead use the encrypted object in the storage + self._localListMessages({ + folder: folder.path, + uid: inMemoryMessage.uid + }, function(err, storedMessages) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + var storedMessage = storedMessages[0]; + storedMessage.unread = inMemoryMessage.unread; + storedMessage.answered = inMemoryMessage.answered; + + // persist the modified object + self._localStoreMessages({ + folder: folder.path, + emails: [storedMessage] + }, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // and we're done. + after(); + }); + }); + + }); + } + } + } + + function finishSync() { + // whereas normal folders show the unread messages count only, + // the outbox shows the total count + // after all the tags are up to date, let's adjust the unread mail count + folder.count = _.filter(folder.messages, function(msg) { + return msg.unread === true; + }).length; + + // allow the next sync to take place + self._account.busy = false; + callback(); + } + + /* + * checks if there are some flags that have changed in a and b + */ + function checkFlags(a, b) { + var i, aI, bI, + delta = []; + + // find the delta + for (i = a.length - 1; i >= 0; i--) { + aI = a[i]; + bI = _.findWhere(b, { + uid: aI.uid + }); + if (bI && (aI.unread !== bI.unread || aI.answered !== bI.answered)) { + delta.push(aI); + } + } + + return delta; + } + + function handleVerification(message, localCallback) { + self._imapStreamText({ + folder: options.folder, + message: message + }, function(error) { + // we could not stream the text to determine if the verification was valid or not + // so handle it as if it were valid + if (error) { + localCallback(error, true); + return; + } + + var verificationUrlPrefix = config.cloudUrl + config.verificationUrl, + uuid = message.body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength), + isValidUuid = new RegExp('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}').test(uuid); + + // there's no valid uuid in the message, so forget about it + if (!isValidUuid) { + localCallback(null, false); + return; + } + + // there's a valid uuid in the message, so try to verify it + self._keychain.verifyPublicKey(uuid, function(err) { + if (err) { + localCallback({ + errMsg: 'Verifying your public key failed: ' + err.errMsg + }, true); + return; + } + + // public key has been verified, delete the message + self._imapDeleteMessage({ + folder: options.folder, + uid: message.uid + }, function() { + // if we could successfully not delete the message or not doesn't matter. + // just don't show it in whiteout and keep quiet about it + localCallback(null, true); + }); + }); + }); + } + }; + + // + // Internal APIs + // + + // Local Storage API + + EmailSync.prototype._localListMessages = function(options, callback) { + var dbType = 'email_' + options.folder; + if (typeof options.uid !== 'undefined') { + dbType = dbType + '_' + options.uid; + } + this._devicestorage.listItems(dbType, 0, null, callback); + }; + + EmailSync.prototype._localStoreMessages = function(options, callback) { + var dbType = 'email_' + options.folder; + this._devicestorage.storeList(options.emails, dbType, callback); + }; + + EmailSync.prototype._localDeleteMessage = function(options, callback) { + if (!options.folder || !options.uid) { + callback({ + errMsg: 'Invalid options!' + }); + return; + } + var dbType = 'email_' + options.folder + '_' + options.uid; + this._devicestorage.removeList(dbType, callback); + }; + + // IMAP API + + /** + * Mark imap messages as un-/read or un-/answered + */ + EmailSync.prototype._imapMark = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + this._imapClient.updateFlags({ + path: options.folder, + uid: options.uid, + unread: options.unread, + answered: options.answered + }, callback); + }; + + /** + * Returns the relevant messages corresponding to the search terms in the options + * @param {String} options.folder The folder's path + * @param {Boolean} options.answered (optional) Mails with or without the \Answered flag set. + * @param {Boolean} options.unread (optional) Mails with or without the \Seen flag set. + * @param {Function} callback(error, uids) invoked with the uids of messages matching the search terms, or an error object if an error occurred + */ + EmailSync.prototype._imapSearch = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + var o = { + path: options.folder + }; + + if (typeof options.answered !== 'undefined') { + o.answered = options.answered; + } + if (typeof options.unread !== 'undefined') { + o.unread = options.unread; + } + + this._imapClient.search(o, callback); + }; + + EmailSync.prototype._imapDeleteMessage = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + this._imapClient.deleteMessage({ + path: options.folder, + uid: options.uid + }, callback); + }; + + /** + * Get an email messsage without the body + * @param {String} options.folder The folder + * @param {Number} options.firstUid The lower bound of the uid (inclusive) + * @param {Number} options.lastUid The upper bound of the uid range (inclusive) + * @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata + */ + EmailSync.prototype._imapListMessages = function(options, callback) { + var self = this; + + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + self._imapClient.listMessagesByUid({ + path: options.folder, + firstUid: options.firstUid, + lastUid: options.lastUid + }, callback); + }; + + /** + * Stream an email messsage's body + * @param {String} options.folder The folder + * @param {Object} options.message The message, as retrieved by _imapListMessages + * @param {Function} callback (error, message) The callback when the imap client is done streaming message text content + */ + EmailSync.prototype._imapStreamText = function(options, callback) { + var self = this; + + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + self._imapClient.getBody({ + path: options.folder, + message: options.message + }, callback); + }; + + return EmailSync; +}); \ No newline at end of file diff --git a/test/new-unit/app-controller-test.js b/test/new-unit/app-controller-test.js index c23754b..5a4f05f 100644 --- a/test/new-unit/app-controller-test.js +++ b/test/new-unit/app-controller-test.js @@ -218,10 +218,14 @@ define(function(require) { describe('buildModules', function() { it('should work', function() { controller.buildModules(); + expect(controller._userStorage).to.exist; + expect(controller._invitationDao).to.exist; expect(controller._keychain).to.exist; expect(controller._crypto).to.exist; + expect(controller._pgpbuilder).to.exist; expect(controller._emailDao).to.exist; expect(controller._outboxBo).to.exist; + expect(controller._updateHandler).to.exist; }); }); diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 6648e76..a426ba4 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -2,6 +2,7 @@ define(function(require) { 'use strict'; var EmailDAO = require('js/dao/email-dao'), + EmailSync = require('js/dao/email-sync'), KeychainDAO = require('js/dao/keychain-dao'), ImapClient = require('imap-client'), PgpMailer = require('pgpmailer'), @@ -15,7 +16,7 @@ define(function(require) { chai.Assertion.includeStack = true; describe('Email DAO unit tests', function() { - var dao, keychainStub, imapClientStub, pgpMailerStub, pgpBuilderStub, pgpStub, devicestorageStub; + var emailSync, dao, keychainStub, imapClientStub, pgpMailerStub, pgpBuilderStub, pgpStub, devicestorageStub; var emailAddress, passphrase, asymKeySize, mockkeyId, dummyEncryptedMail, dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid, @@ -117,7 +118,8 @@ define(function(require) { pgpStub = sinon.createStubInstance(PGP); devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); - dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader); + emailSync = new EmailSync(keychainStub, devicestorageStub); + dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader, emailSync); dao._account = account; expect(dao._keychain).to.equal(keychainStub); @@ -637,254 +639,6 @@ define(function(require) { }); }); - describe('_imapSearch', function() { - it('should fail when disconnected', function(done) { - dao.onDisconnect(null, function(err) { - expect(err).to.not.exist; - - dao._imapSearch({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); - }); - }); - - it('should work', function(done) { - var path = 'FOLDAAAA'; - - imapClientStub.search.withArgs({ - path: path - }).yields(); - - dao._imapSearch({ - folder: path - }, done); - }); - it('should work', function(done) { - var path = 'FOLDAAAA'; - - imapClientStub.search.withArgs({ - path: path, - answered: true - }).yields(); - - dao._imapSearch({ - folder: path, - answered: true - }, done); - }); - it('should work', function(done) { - var path = 'FOLDAAAA'; - - imapClientStub.search.withArgs({ - path: path, - unread: true - }).yields(); - - dao._imapSearch({ - folder: path, - unread: true - }, done); - }); - }); - - describe('_imapDeleteMessage', function() { - it('should fail when disconnected', function(done) { - dao.onDisconnect(null, function(err) { - expect(err).to.not.exist; - - dao._imapDeleteMessage({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); - }); - }); - - it('should work', function(done) { - var path = 'FOLDAAAA', - uid = 1337; - - imapClientStub.deleteMessage.withArgs({ - path: path, - uid: uid - }).yields(); - - dao._imapDeleteMessage({ - folder: path, - uid: uid - }, done); - }); - }); - - describe('_imapListMessages', function() { - it('should work', function(done) { - var path = 'FOLDAAAA', - firstUid = 1337, - lastUid = 1339; - - imapClientStub.listMessagesByUid.withArgs({ - path: path, - firstUid: firstUid, - lastUid: lastUid - }).yields(null, []); - - dao._imapListMessages({ - folder: path, - firstUid: firstUid, - lastUid: lastUid - }, function(err, msgs) { - expect(err).to.not.exist; - expect(msgs).to.exist; - - expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; - - done(); - }); - }); - - it('should not work when listMessagesByUid fails', function(done) { - var path = 'FOLDAAAA', - firstUid = 1337, - lastUid = 1339; - - imapClientStub.listMessagesByUid.yields({}); - - dao._imapListMessages({ - folder: path, - firstUid: firstUid, - lastUid: lastUid - }, function(err, msgs) { - expect(err).to.exist; - expect(msgs).to.not.exist; - - expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when disconnected', function(done) { - dao.onDisconnect(null, function(err) { - expect(err).to.not.exist; - - dao._imapListMessages({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); - }); - }); - }); - - describe('_imapStreamText', function() { - it('should work', function(done) { - var path = 'FOLDAAAA'; - - imapClientStub.getBody.withArgs({ - path: path, - message: {} - }).yields(null, {}); - - dao._imapStreamText({ - folder: path, - message: {} - }, function(err, msg) { - expect(err).to.not.exist; - expect(msg).to.exist; - - expect(imapClientStub.getBody.calledOnce).to.be.true; - - done(); - }); - }); - - it('should not work when getBody fails', function(done) { - var path = 'FOLDAAAA'; - - imapClientStub.getBody.yields({}); - - dao._imapStreamText({ - folder: path, - message: {} - }, function(err, msg) { - expect(err).to.exist; - expect(msg).to.not.exist; - - expect(imapClientStub.getBody.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when disconnected', function(done) { - dao.onDisconnect(null, function(err) { - expect(err).to.not.exist; - - dao._imapStreamText({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); - }); - }); - }); - - describe('_localListMessages', function() { - it('should work without uid', function(done) { - var folder = 'FOLDAAAA'; - devicestorageStub.listItems.withArgs('email_' + folder, 0, null).yields(); - - dao._localListMessages({ - folder: folder - }, done); - }); - - it('should work with uid', function(done) { - var folder = 'FOLDAAAA', - uid = 123; - devicestorageStub.listItems.withArgs('email_' + folder + '_' + uid, 0, null).yields(); - - dao._localListMessages({ - folder: folder, - uid: uid - }, done); - }); - }); - - describe('_localStoreMessages', function() { - it('should work', function(done) { - var folder = 'FOLDAAAA', - emails = [{}]; - devicestorageStub.storeList.withArgs(emails, 'email_' + folder).yields(); - - dao._localStoreMessages({ - folder: folder, - emails: emails - }, done); - }); - }); - - describe('_localDeleteMessage', function() { - it('should work', function(done) { - var folder = 'FOLDAAAA', - uid = 1337; - devicestorageStub.removeList.withArgs('email_' + folder + '_' + uid).yields(); - - dao._localDeleteMessage({ - folder: folder, - uid: uid - }, done); - }); - - it('should fail when uid is missing', function(done) { - var folder = 'FOLDAAAA'; - - dao._localDeleteMessage({ - folder: folder - }, function(err) { - expect(err).to.exist; - done(); - }); - }); - }); describe('getBody', function() { it('should not do anything if the message already has content', function() { @@ -909,7 +663,7 @@ define(function(require) { uid: uid }; - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [{ @@ -945,7 +699,7 @@ define(function(require) { uid: uid }; - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [{ @@ -981,17 +735,17 @@ define(function(require) { uid: uid }; - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [message]); - localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({ folder: folder, emails: [message] }).yieldsAsync(); - imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { expect(opts).to.deep.equal({ folder: folder, message: message @@ -1032,17 +786,17 @@ define(function(require) { uid: uid }; - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [message]); - localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({ folder: folder, emails: [message] }).yieldsAsync(); - imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { expect(opts).to.deep.equal({ folder: folder, message: message @@ -1084,10 +838,10 @@ define(function(require) { uid: uid }; - localListStub = sinon.stub(dao, '_localListMessages').yieldsAsync(null, [message]); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yieldsAsync({}); + localListStub = sinon.stub(emailSync, '_localListMessages').yieldsAsync(null, [message]); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yieldsAsync({}); - imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { message.body = body; cb(); }); @@ -1117,14 +871,14 @@ define(function(require) { uid: uid }; - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [{}]); + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [{}]); - imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { message.body = body; cb({}); }); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); dao.getBody({ message: message, @@ -1326,1189 +1080,21 @@ define(function(require) { describe('sync', function() { - it('should initially fill from local', function(done) { - var folder, localListStub, invocations, imapSearchStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder - }]; - - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages.length).to.equal(1); - expect(dao._account.folders[0].messages[0].uid).to.equal(dummyEncryptedMail.uid); - expect(dao._account.folders[0].messages[0].body).to.not.exist; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - - done(); - }); - }); - - it('should not work when busy', function(done) { - dao._account.busy = true; + it('should call emailSync api', function(done) { + sinon.stub(emailSync, 'sync').withArgs({ + folder: 'OOGA' + }).yields(); dao.sync({ folder: 'OOGA' }, function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should not work without providing a folder', function(done) { - dao.sync({}, function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should not work when initial setup errors', function(done) { - var folder, localListStub; - - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(localListStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should be up to date', function(done) { - var folder, localListStub, imapSearchStub, invocations; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid - 10, dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - done(); - }); - }); - - it('should error while searching on imap', function(done) { - var folder, localListStub, imapSearchStub, invocations; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [dummyEncryptedMail]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields({}); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should error while listing local messages', function(done) { - var folder, localListStub; - - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(localListStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should remove messages from the remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, localDeleteStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [dummyEncryptedMail]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields(); - localDeleteStub = sinon.stub(dao, '_localDeleteMessage').yields(); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(localDeleteStub.calledOnce).to.be.true; - expect(imapDeleteStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should error while removing messages from local', function(done) { - var invocations, folder, localListStub, imapSearchStub, localDeleteStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [dummyEncryptedMail]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields(); - localDeleteStub = sinon.stub(dao, '_localDeleteMessage').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(localDeleteStub.calledOnce).to.be.true; - expect(imapDeleteStub.calledOnce).to.be.true; - expect(imapSearchStub.called).to.be.false; - done(); - }); - }); - - it('should error while removing messages from the remote', function(done) { - var folder, localListStub, imapSearchStub, localDeleteStub, imapDeleteStub; - - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [dummyEncryptedMail]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); - localDeleteStub = sinon.stub(dao, '_localDeleteMessage'); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapDeleteStub.calledOnce).to.be.true; - expect(localDeleteStub.called).to.be.false; - expect(imapSearchStub.called).to.be.false; - - done(); - }); - }); - - it('should delete messages locally if not present on remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, localDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - localDeleteStub = sinon.stub(dao, '_localDeleteMessage').withArgs({ - folder: folder, - uid: dummyEncryptedMail.uid - }).yields(); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(localDeleteStub.calledOnce).to.be.true; - done(); - }); - - }); - - it('should error while deleting locally if not present on remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, localDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [dummyEncryptedMail]); - localDeleteStub = sinon.stub(dao, '_localDeleteMessage').yields({}); - imapSearchStub = sinon.stub(dao, '_imapSearch').withArgs({ - folder: folder - }).yields(null, []); - - - dao.sync({ - folder: folder - }, function(err) { - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - expect(localDeleteStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should fetch messages downstream from the remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - delete dummyEncryptedMail.body; - - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, []); - - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - - imapListMessagesStub = sinon.stub(dao, '_imapListMessages'); - imapListMessagesStub.withArgs({ - folder: folder, - firstUid: dummyEncryptedMail.uid, - lastUid: dummyEncryptedMail.uid - }).yields(null, [dummyEncryptedMail]); - - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - localStoreStub.withArgs({ - folder: folder, - emails: [dummyEncryptedMail] - }).yields(); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages.length).to.equal(1); - expect(dao._account.folders[0].messages[0].uid).to.equal(dummyEncryptedMail.uid); - expect(dao._account.folders[0].messages[0].body).to.not.exist; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should error while storing messages from the remote locally', function(done) { - var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - delete dummyEncryptedMail.body; - - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, []); - - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.yields(null, [dummyEncryptedMail.uid]); - - imapListMessagesStub = sinon.stub(dao, '_imapListMessages'); - imapListMessagesStub.yields(null, [dummyEncryptedMail]); - - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - localStoreStub.yields({}); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages.length).to.equal(0); - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should verify an authentication mail', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - - imapSearchStub = sinon.stub(dao, '_imapSearch'); - - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [verificationMail.uid]); - - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - - imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); - - imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); - - keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); - - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').withArgs({ - folder: folder, - uid: verificationMail.uid - }).yields(); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(imapDeleteStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.false; - - done(); - }); - }); - - it('should not care abouta failed deletion of an authentication mail', function(done) { - var invocations, folder, localListStub, localStoreStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [verificationMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); - imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); - keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); - - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - localStoreStub.withArgs({ - folder: folder, - emails: [verificationMail] - }).yields(); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.not.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(localStoreStub.called).to.be.false; - expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(imapDeleteStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail during verifying authentication', function(done) { - var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapGetStub, imapListMessagesStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [verificationMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); - imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); - keychainStub.verifyPublicKey.withArgs(verificationUuid).yields({ - errMsg: 'fubar' - }); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); - - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - localStoreStub.withArgs({ - folder: folder, - emails: [verificationMail] - }).yields(); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - if (invocations === 1) { - expect(err).to.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.not.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(imapDeleteStub.called).to.be.false; - - done(); - }); - }); - - it('should not bother about corrupted authentication mails', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [corruptedVerificationMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - - localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ - folder: folder, - emails: [corruptedVerificationMail] - }).yields(); - - - imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [corruptedVerificationMail]); - imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); - keychainStub.verifyPublicKey.withArgs(corruptedVerificationUuid).yields({ - errMsg: 'fubar' - }); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.called).to.be.false; - expect(imapDeleteStub.called).to.be.false; - - - done(); - }); - }); - - it('should sync tags from memory to imap and storage', function(done) { - var folder, localListStub, imapSearchStub, invocations, - markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inImap.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [inImap.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, [inImap.uid]); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - markStub = sinon.stub(dao, '_imapMark').withArgs({ - folder: folder, - uid: dummyDecryptedMail.uid, - unread: dummyDecryptedMail.unread, - answered: dummyDecryptedMail.answered - }).yields(); - localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ - folder: folder, - emails: [inStorage] - }).yields(); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(markStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - - expect(inStorage.unread).to.equal(dummyDecryptedMail.unread); - expect(inStorage.answered).to.equal(dummyDecryptedMail.answered); - - done(); - }); - }); - - it('should error while syncing unread tags from memory to storage', function(done) { - var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inImap.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - markStub = sinon.stub(dao, '_imapMark').yields(); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(markStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(imapSearchStub.called).to.be.false; - done(); - }); - }); - - it('should error while syncing answered tags from memory to storage', function(done) { - var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inImap.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - markStub = sinon.stub(dao, '_imapMark').yields(); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(markStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(imapSearchStub.called).to.be.false; - done(); - }); - }); - - it('should error while syncing tags from memory to imap', function(done) { - var folder, localListStub, imapSearchStub, invocations, - markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inImap.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - markStub = sinon.stub(dao, '_imapMark').yields({}); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(markStub.calledOnce).to.be.true; - expect(localStoreStub.called).to.be.false; - expect(imapSearchStub.called).to.be.false; - done(); - }); - }); - - it('should sync tags from imap to memory and storage', function(done) { - var folder, localListStub, imapSearchStub, invocations, - markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inStorage.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - markStub = sinon.stub(dao, '_imapMark'); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledTwice).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(markStub.called).to.be.false; - expect(localStoreStub.calledOnce).to.be.true; - - expect(dummyDecryptedMail.unread).to.equal(false); - expect(inStorage.unread).to.equal(false); - - done(); - }); - }); - - it('should error while searching for unread tags on imap', function(done) { - var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inStorage.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields({}); - markStub = sinon.stub(dao, '_imapMark'); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(markStub.called).to.be.false; - expect(imapSearchStub.calledTwice).to.be.true; - expect(localStoreStub.called).to.be.false; - - expect(inStorage.unread).to.equal(true); - expect(dummyDecryptedMail.unread).to.equal(true); // the live object has not been touched! - - done(); - }); - }); - - it('should error while searching for answered tags on imap', function(done) { - var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inStorage.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields({}); - markStub = sinon.stub(dao, '_imapMark'); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(markStub.called).to.be.false; - expect(imapSearchStub.calledThrice).to.be.true; - expect(localStoreStub.called).to.be.false; - - expect(inStorage.unread).to.equal(true); - expect(dummyDecryptedMail.unread).to.equal(true); // the live object has not been touched! - - done(); - }); - }); - - it('should error while syncing tags from imap to storage', function(done) { - var folder, localListStub, imapSearchStub, invocations, - markStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [dummyDecryptedMail] - }]; - - var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); - dummyDecryptedMail.unread = inStorage.unread = true; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - markStub = sinon.stub(dao, '_imapMark'); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0]).to.not.be.empty; - expect(localListStub.calledTwice).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(markStub.called).to.be.false; - expect(localStoreStub.calledOnce).to.be.true; - - done(); - }); - }); - }); - - describe('mark', function() { - it('should work', function(done) { - imapClientStub.updateFlags.withArgs({ - path: 'asdf', - uid: 1, - unread: false, - answered: false - }).yields(); - - dao._imapMark({ - folder: 'asdf', - uid: 1, - unread: false, - answered: false - }, function(err) { - expect(imapClientStub.updateFlags.calledOnce).to.be.true; expect(err).to.not.exist; done(); }); }); }); + describe('sendPlaintext', function() { it('should work', function(done) { pgpMailerStub.send.withArgs({ @@ -2537,6 +1123,7 @@ define(function(require) { }); }); + describe('sendEncrypted', function() { it('should work', function(done) { var publicKeys = ["PUBLIC KEY"]; @@ -2576,6 +1163,7 @@ define(function(require) { }); }); + describe('encrypt', function() { it('should encrypt', function(done) { pgpBuilderStub.encrypt.yields(); @@ -2587,28 +1175,21 @@ define(function(require) { }); }); - describe('syncOutbox', function() { - it('should sync the outbox', function(done) { - var folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder - }]; - var localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail]); + describe('syncOutbox', function() { + it('should call emailSync api', function(done) { + sinon.stub(emailSync, 'syncOutbox').withArgs({ + folder: 'OOGA' + }).yields(); dao.syncOutbox({ - folder: folder + folder: 'OOGA' }, function(err) { expect(err).to.not.exist; - expect(localListStub.calledOnce).to.be.true; - expect(dao._account.folders[0].messages.length).to.equal(1); - done(); }); }); }); + }); }); \ No newline at end of file diff --git a/test/new-unit/email-sync-test.js b/test/new-unit/email-sync-test.js new file mode 100644 index 0000000..340ab67 --- /dev/null +++ b/test/new-unit/email-sync-test.js @@ -0,0 +1,1614 @@ +define(function(require) { + 'use strict'; + + var EmailSync = require('js/dao/email-sync'), + KeychainDAO = require('js/dao/keychain-dao'), + ImapClient = require('imap-client'), + DeviceStorageDAO = require('js/dao/devicestorage-dao'), + expect = chai.expect; + + chai.Assertion.includeStack = true; + + describe('Email Sync unit tests', function() { + var emailSync, keychainStub, imapClientStub, devicestorageStub; + + var emailAddress, mockkeyId, dummyEncryptedMail, + dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid, + corruptedVerificationMail, corruptedVerificationUuid, + nonWhitelistedMail; + + beforeEach(function(done) { + emailAddress = 'asdf@asdf.com'; + mockkeyId = 1234; + dummyEncryptedMail = { + uid: 1234, + from: [{ + address: 'asd@asd.de' + }], + to: [{ + address: 'qwe@qwe.de' + }], + subject: 'qweasd', + body: '-----BEGIN PGP MESSAGE-----\nasd\n-----END PGP MESSAGE-----', + unread: false, + answered: false + }; + verificationUuid = '9A858952-17EE-4273-9E74-D309EAFDFAFB'; + verificationMail = { + from: [{ + name: 'Whiteout Test', + address: 'whiteout.test@t-online.de' + }], // sender address + to: [{ + address: 'safewithme.testuser@gmail.com' + }], // list of receivers + subject: "[whiteout] New public key uploaded", // Subject line + body: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid, // plaintext body + unread: false, + answered: false + }; + corruptedVerificationUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!'; + corruptedVerificationMail = { + from: [{ + name: 'Whiteout Test', + address: 'whiteout.test@t-online.de' + }], // sender address + to: [{ + address: 'safewithme.testuser@gmail.com' + }], // list of receivers + subject: "[whiteout] New public key uploaded", // Subject line + body: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + corruptedVerificationUuid, // plaintext body + unread: false, + answered: false + }; + dummyDecryptedMail = { + uid: 1234, + from: [{ + address: 'asd@asd.de' + }], + to: [{ + address: 'qwe@qwe.de' + }], + subject: 'qweasd', + body: 'Content-Type: multipart/signed;\r\n boundary="Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2";\r\n protocol="application/pgp-signature";\r\n micalg=pgp-sha512\r\n\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2\r\nContent-Type: multipart/mixed;\r\n boundary="Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA"\r\n\r\n\r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/plain;\r\n charset=us-ascii\r\n\r\nasdasd \r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA\r\nContent-Disposition: attachment;\r\n filename=dummy.txt\r\nContent-Type: text/plain;\r\n name="dummy.txt"\r\nContent-Transfer-Encoding: 7bit\r\n\r\noaudbcoaurbvosuabvlasdjbfalwubjvawvb\r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA--\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2\r\nContent-Transfer-Encoding: 7bit\r\nContent-Disposition: attachment;\r\n filename=signature.asc\r\nContent-Type: application/pgp-signature;\r\n name=signature.asc\r\nContent-Description: Message signed with OpenPGP using GPGMail\r\n\r\n-----BEGIN PGP SIGNATURE-----\r\nComment: GPGTools - https://gpgtools.org\r\n\r\niQEcBAEBCgAGBQJS2kO1AAoJEDzmUwH7XO/cP+YH/2PSBxX1ZZd83Uf9qBGDY807\r\niHOdgPFXm64YjSnohO7XsPcnmihqP1ipS2aaCXFC3/Vgb9nc4isQFS+i1VdPwfuR\r\n1Pd2l3dC4/nD4xO9h/W6JW7Yd24NS5TJD5cA7LYwQ8LF+rOzByMatiTMmecAUCe8\r\nEEalEjuogojk4IacA8dg/bfLqQu9E+0GYUJBcI97dx/0jZ0qMOxbWOQLsJ3DnUnV\r\nOad7pAIbHEO6T0EBsH7TyTj4RRHkP6SKE0mm6ZYUC7KCk2Z3MtkASTxUrnqW5qZ5\r\noaXUO9GEc8KZcmbCdhZY2Y5h+dmucaO0jpbeSKkvtYyD4KZrSvt7NTb/0dSLh4Y=\r\n=G8km\r\n-----END PGP SIGNATURE-----\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2--\r\n', + unread: false, + answered: false, + }; + nonWhitelistedMail = { + uid: 1234, + from: [{ + address: 'asd@asd.de' + }], + to: [{ + address: 'qwe@qwe.de' + }], + subject: 'qweasd', + body: 'asd' + }; + mockKeyPair = { + publicKey: { + _id: mockkeyId, + userId: emailAddress, + publicKey: 'publicpublicpublicpublic' + }, + privateKey: { + _id: mockkeyId, + userId: emailAddress, + encryptedKey: 'privateprivateprivateprivate' + } + }; + account = { + emailAddress: emailAddress, + busy: false + }; + + keychainStub = sinon.createStubInstance(KeychainDAO); + imapClientStub = sinon.createStubInstance(ImapClient); + devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); + + emailSync = new EmailSync(keychainStub, devicestorageStub); + + expect(emailSync._keychain).to.equal(keychainStub); + expect(emailSync._devicestorage).to.equal(devicestorageStub); + + // init + emailSync.init({ + account: account + }, function(err) { + expect(err).to.not.exist; + expect(emailSync._account).to.equal(account); + + // connect + expect(emailSync._imapClient).to.not.exist; + expect(emailSync._smtpClient).to.not.exist; + expect(emailSync._account.online).to.be.undefined; + emailSync._account.folders = []; + imapClientStub.login.yields(); + + // this is set in the emailDao.onConnect + emailSync._account.online = true; + + emailSync.onConnect({ + imapClient: imapClientStub + }, function(err) { + expect(err).to.not.exist; + expect(emailSync._account.online).to.be.true; + expect(emailSync._imapClient).to.equal(emailSync._imapClient); + expect(emailSync._smtpClient).to.equal(emailSync._smtpClient); + done(); + }); + }); + }); + + afterEach(function(done) { + // this is set in the emailDao.onDisconnect + emailSync._account.online = false; + + emailSync.onDisconnect(null, function(err) { + expect(err).to.not.exist; + expect(emailSync._account.online).to.be.false; + expect(emailSync._imapClient).to.not.exist; + done(); + }); + }); + + + describe('_imapSearch', function() { + it('should fail when disconnected', function(done) { + // this is set in the emailDao.onDisconnect + emailSync._account.online = false; + + emailSync._imapSearch({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + + it('should work', function(done) { + var path = 'FOLDAAAA'; + + imapClientStub.search.withArgs({ + path: path + }).yields(); + + emailSync._imapSearch({ + folder: path + }, done); + }); + it('should work', function(done) { + var path = 'FOLDAAAA'; + + imapClientStub.search.withArgs({ + path: path, + answered: true + }).yields(); + + emailSync._imapSearch({ + folder: path, + answered: true + }, done); + }); + it('should work', function(done) { + var path = 'FOLDAAAA'; + + imapClientStub.search.withArgs({ + path: path, + unread: true + }).yields(); + + emailSync._imapSearch({ + folder: path, + unread: true + }, done); + }); + }); + + describe('_imapDeleteMessage', function() { + it('should fail when disconnected', function(done) { + // this is set in the emailDao.onDisconnect + emailSync._account.online = false; + + emailSync._imapDeleteMessage({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + + it('should work', function(done) { + var path = 'FOLDAAAA', + uid = 1337; + + imapClientStub.deleteMessage.withArgs({ + path: path, + uid: uid + }).yields(); + + emailSync._imapDeleteMessage({ + folder: path, + uid: uid + }, done); + }); + }); + + describe('_imapListMessages', function() { + it('should work', function(done) { + var path = 'FOLDAAAA', + firstUid = 1337, + lastUid = 1339; + + imapClientStub.listMessagesByUid.withArgs({ + path: path, + firstUid: firstUid, + lastUid: lastUid + }).yields(null, []); + + emailSync._imapListMessages({ + folder: path, + firstUid: firstUid, + lastUid: lastUid + }, function(err, msgs) { + expect(err).to.not.exist; + expect(msgs).to.exist; + + expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; + + done(); + }); + }); + + it('should not work when listMessagesByUid fails', function(done) { + var path = 'FOLDAAAA', + firstUid = 1337, + lastUid = 1339; + + imapClientStub.listMessagesByUid.yields({}); + + emailSync._imapListMessages({ + folder: path, + firstUid: firstUid, + lastUid: lastUid + }, function(err, msgs) { + expect(err).to.exist; + expect(msgs).to.not.exist; + + expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when disconnected', function(done) { + // this is set in the emailDao.onDisconnect + emailSync._account.online = false; + + emailSync._imapListMessages({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + }); + + describe('_imapStreamText', function() { + it('should work', function(done) { + var path = 'FOLDAAAA'; + + imapClientStub.getBody.withArgs({ + path: path, + message: {} + }).yields(null, {}); + + emailSync._imapStreamText({ + folder: path, + message: {} + }, function(err, msg) { + expect(err).to.not.exist; + expect(msg).to.exist; + + expect(imapClientStub.getBody.calledOnce).to.be.true; + + done(); + }); + }); + + it('should not work when getBody fails', function(done) { + var path = 'FOLDAAAA'; + + imapClientStub.getBody.yields({}); + + emailSync._imapStreamText({ + folder: path, + message: {} + }, function(err, msg) { + expect(err).to.exist; + expect(msg).to.not.exist; + + expect(imapClientStub.getBody.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when disconnected', function(done) { + // this is set in the emailDao.onDisconnect + emailSync._account.online = false; + + emailSync._imapStreamText({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + }); + + describe('_localListMessages', function() { + it('should work without uid', function(done) { + var folder = 'FOLDAAAA'; + devicestorageStub.listItems.withArgs('email_' + folder, 0, null).yields(); + + emailSync._localListMessages({ + folder: folder + }, done); + }); + + it('should work with uid', function(done) { + var folder = 'FOLDAAAA', + uid = 123; + devicestorageStub.listItems.withArgs('email_' + folder + '_' + uid, 0, null).yields(); + + emailSync._localListMessages({ + folder: folder, + uid: uid + }, done); + }); + }); + + describe('_localStoreMessages', function() { + it('should work', function(done) { + var folder = 'FOLDAAAA', + emails = [{}]; + devicestorageStub.storeList.withArgs(emails, 'email_' + folder).yields(); + + emailSync._localStoreMessages({ + folder: folder, + emails: emails + }, done); + }); + }); + + describe('_localDeleteMessage', function() { + it('should work', function(done) { + var folder = 'FOLDAAAA', + uid = 1337; + devicestorageStub.removeList.withArgs('email_' + folder + '_' + uid).yields(); + + emailSync._localDeleteMessage({ + folder: folder, + uid: uid + }, done); + }); + + it('should fail when uid is missing', function(done) { + var folder = 'FOLDAAAA'; + + emailSync._localDeleteMessage({ + folder: folder + }, function(err) { + expect(err).to.exist; + done(); + }); + }); + }); + + + describe('sync', function() { + it('should initially fill from local', function(done) { + var folder, localListStub, invocations, imapSearchStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages.length).to.equal(1); + expect(emailSync._account.folders[0].messages[0].uid).to.equal(dummyEncryptedMail.uid); + expect(emailSync._account.folders[0].messages[0].body).to.not.exist; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + + done(); + }); + }); + + it('should not work when busy', function(done) { + emailSync._account.busy = true; + + emailSync.sync({ + folder: 'OOGA' + }, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should not work without providing a folder', function(done) { + emailSync.sync({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should not work when initial setup errors', function(done) { + var folder, localListStub; + + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.exist; + + expect(emailSync._account.busy).to.be.false; + expect(localListStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should be up to date', function(done) { + var folder, localListStub, imapSearchStub, invocations; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid - 10, dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + done(); + }); + }); + + it('should error while searching on imap', function(done) { + var folder, localListStub, imapSearchStub, invocations; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [dummyEncryptedMail]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should error while listing local messages', function(done) { + var folder, localListStub; + + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.exist; + + expect(emailSync._account.busy).to.be.false; + expect(localListStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should remove messages from the remote', function(done) { + var invocations, folder, localListStub, imapSearchStub, localDeleteStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [dummyEncryptedMail]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + + imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields(); + localDeleteStub = sinon.stub(emailSync, '_localDeleteMessage').yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(localDeleteStub.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should error while removing messages from local', function(done) { + var invocations, folder, localListStub, imapSearchStub, localDeleteStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [dummyEncryptedMail]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields(); + localDeleteStub = sinon.stub(emailSync, '_localDeleteMessage').yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.exist; + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(localDeleteStub.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + expect(imapSearchStub.called).to.be.false; + done(); + }); + }); + + it('should error while removing messages from the remote', function(done) { + var folder, localListStub, imapSearchStub, localDeleteStub, imapDeleteStub; + + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [dummyEncryptedMail]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({}); + localDeleteStub = sinon.stub(emailSync, '_localDeleteMessage'); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.exist; + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + expect(localDeleteStub.called).to.be.false; + expect(imapSearchStub.called).to.be.false; + + done(); + }); + }); + + it('should delete messages locally if not present on remote', function(done) { + var invocations, folder, localListStub, imapSearchStub, localDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + localDeleteStub = sinon.stub(emailSync, '_localDeleteMessage').withArgs({ + folder: folder, + uid: dummyEncryptedMail.uid + }).yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(localDeleteStub.calledOnce).to.be.true; + done(); + }); + + }); + + it('should error while deleting locally if not present on remote', function(done) { + var invocations, folder, localListStub, imapSearchStub, localDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [dummyEncryptedMail]); + localDeleteStub = sinon.stub(emailSync, '_localDeleteMessage').yields({}); + imapSearchStub = sinon.stub(emailSync, '_imapSearch').withArgs({ + folder: folder + }).yields(null, []); + + + emailSync.sync({ + folder: folder + }, function(err) { + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledOnce).to.be.true; + expect(localDeleteStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should fetch messages downstream from the remote', function(done) { + var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + delete dummyEncryptedMail.body; + + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + folder: folder + }).yields(null, []); + + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + + imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages'); + imapListMessagesStub.withArgs({ + folder: folder, + firstUid: dummyEncryptedMail.uid, + lastUid: dummyEncryptedMail.uid + }).yields(null, [dummyEncryptedMail]); + + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + localStoreStub.withArgs({ + folder: folder, + emails: [dummyEncryptedMail] + }).yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages.length).to.equal(1); + expect(emailSync._account.folders[0].messages[0].uid).to.equal(dummyEncryptedMail.uid); + expect(emailSync._account.folders[0].messages[0].body).to.not.exist; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should error while storing messages from the remote locally', function(done) { + var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + delete dummyEncryptedMail.body; + + localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + folder: folder + }).yields(null, []); + + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.yields(null, [dummyEncryptedMail.uid]); + + imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages'); + imapListMessagesStub.yields(null, [dummyEncryptedMail]); + + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + localStoreStub.yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages.length).to.equal(0); + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should verify an authentication mail', function(done) { + var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, []); + + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [verificationMail.uid]); + + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + + imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]); + + imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); + + keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); + + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + + imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').withArgs({ + folder: folder, + uid: verificationMail.uid + }).yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.false; + + done(); + }); + }); + + it('should not care abouta failed deletion of an authentication mail', function(done) { + var invocations, folder, localListStub, localStoreStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, []); + + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [verificationMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); + keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); + imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({}); + + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + localStoreStub.withArgs({ + folder: folder, + emails: [verificationMail] + }).yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(localStoreStub.called).to.be.false; + expect(imapSearchStub.calledThrice).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail during verifying authentication', function(done) { + var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapGetStub, imapListMessagesStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, []); + + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [verificationMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); + keychainStub.verifyPublicKey.withArgs(verificationUuid).yields({ + errMsg: 'fubar' + }); + imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({}); + + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + localStoreStub.withArgs({ + folder: folder, + emails: [verificationMail] + }).yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + if (invocations === 1) { + expect(err).to.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(imapDeleteStub.called).to.be.false; + + done(); + }); + }); + + it('should not bother about corrupted authentication mails', function(done) { + var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, []); + + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [corruptedVerificationMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({ + folder: folder, + emails: [corruptedVerificationMail] + }).yields(); + + + imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [corruptedVerificationMail]); + imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); + keychainStub.verifyPublicKey.withArgs(corruptedVerificationUuid).yields({ + errMsg: 'fubar' + }); + imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0].messages).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.called).to.be.false; + expect(imapDeleteStub.called).to.be.false; + + + done(); + }); + }); + + it('should sync tags from memory to imap and storage', function(done) { + var folder, localListStub, imapSearchStub, invocations, + markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inImap.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [inImap.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, [inImap.uid]); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + markStub = sinon.stub(emailSync, '_imapMark').withArgs({ + folder: folder, + uid: dummyDecryptedMail.uid, + unread: dummyDecryptedMail.unread, + answered: dummyDecryptedMail.answered + }).yields(); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({ + folder: folder, + emails: [inStorage] + }).yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(markStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + expect(inStorage.unread).to.equal(dummyDecryptedMail.unread); + expect(inStorage.answered).to.equal(dummyDecryptedMail.answered); + + done(); + }); + }); + + it('should error while syncing unread tags from memory to storage', function(done) { + var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inImap.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + markStub = sinon.stub(emailSync, '_imapMark').yields(); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.exist; + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(markStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + expect(imapSearchStub.called).to.be.false; + done(); + }); + }); + + it('should error while syncing answered tags from memory to storage', function(done) { + var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inImap.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + markStub = sinon.stub(emailSync, '_imapMark').yields(); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.exist; + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(markStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + expect(imapSearchStub.called).to.be.false; + done(); + }); + }); + + it('should error while syncing tags from memory to imap', function(done) { + var folder, localListStub, imapSearchStub, invocations, + markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inImap.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + markStub = sinon.stub(emailSync, '_imapMark').yields({}); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.exist; + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(markStub.calledOnce).to.be.true; + expect(localStoreStub.called).to.be.false; + expect(imapSearchStub.called).to.be.false; + done(); + }); + }); + + it('should sync tags from imap to memory and storage', function(done) { + var folder, localListStub, imapSearchStub, invocations, + markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inStorage.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + markStub = sinon.stub(emailSync, '_imapMark'); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yields(); + + emailSync.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledTwice).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(markStub.called).to.be.false; + expect(localStoreStub.calledOnce).to.be.true; + + expect(dummyDecryptedMail.unread).to.equal(false); + expect(inStorage.unread).to.equal(false); + + done(); + }); + }); + + it('should error while searching for unread tags on imap', function(done) { + var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inStorage.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields({}); + markStub = sinon.stub(emailSync, '_imapMark'); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + + emailSync.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(markStub.called).to.be.false; + expect(imapSearchStub.calledTwice).to.be.true; + expect(localStoreStub.called).to.be.false; + + expect(inStorage.unread).to.equal(true); + expect(dummyDecryptedMail.unread).to.equal(true); // the live object has not been touched! + + done(); + }); + }); + + it('should error while searching for answered tags on imap', function(done) { + var folder, localListStub, imapSearchStub, invocations, markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inStorage.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields({}); + markStub = sinon.stub(emailSync, '_imapMark'); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + + emailSync.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(markStub.called).to.be.false; + expect(imapSearchStub.calledThrice).to.be.true; + expect(localStoreStub.called).to.be.false; + + expect(inStorage.unread).to.equal(true); + expect(dummyDecryptedMail.unread).to.equal(true); // the live object has not been touched! + + done(); + }); + }); + + it('should error while syncing tags from imap to storage', function(done) { + var folder, localListStub, imapSearchStub, invocations, + markStub, localStoreStub; + + invocations = 0; + folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder, + messages: [dummyDecryptedMail] + }]; + + var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail)); + dummyDecryptedMail.unread = inStorage.unread = true; + + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [inStorage]); + imapSearchStub = sinon.stub(emailSync, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + markStub = sinon.stub(emailSync, '_imapMark'); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yields({}); + + emailSync.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(emailSync._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(emailSync._account.busy).to.be.false; + expect(emailSync._account.folders[0]).to.not.be.empty; + expect(localListStub.calledTwice).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(markStub.called).to.be.false; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + }); + }); + + + describe('syncOutbox', function() { + it('should sync the outbox', function(done) { + var folder = 'FOLDAAAA'; + emailSync._account.folders = [{ + type: 'Folder', + path: folder + }]; + + var localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail]); + + emailSync.syncOutbox({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + expect(localListStub.calledOnce).to.be.true; + expect(emailSync._account.folders[0].messages.length).to.equal(1); + + done(); + }); + }); + }); + + + describe('mark', function() { + it('should work', function(done) { + imapClientStub.updateFlags.withArgs({ + path: 'asdf', + uid: 1, + unread: false, + answered: false + }).yields(); + + emailSync._imapMark({ + folder: 'asdf', + uid: 1, + unread: false, + answered: false + }, function(err) { + expect(imapClientStub.updateFlags.calledOnce).to.be.true; + expect(err).to.not.exist; + done(); + }); + }); + }); + + + }); +}); \ No newline at end of file diff --git a/test/new-unit/main.js b/test/new-unit/main.js index e8776f8..7cb53c6 100644 --- a/test/new-unit/main.js +++ b/test/new-unit/main.js @@ -29,6 +29,7 @@ function startTests() { require( [ 'test/new-unit/email-dao-test', + 'test/new-unit/email-sync-test', 'test/new-unit/app-controller-test', 'test/new-unit/pgp-test', 'test/new-unit/rest-dao-test',